change python config, add jupyter and ein
This commit is contained in:
12
lisp/anaphora/anaphora-pkg.el
Normal file
12
lisp/anaphora/anaphora-pkg.el
Normal file
@@ -0,0 +1,12 @@
|
||||
(define-package "anaphora" "20240120.1744" "anaphoric macros providing implicit temp variables" 'nil :commit "a755afa7db7f3fa515f8dd2c0518113be0b027f6" :authors
|
||||
'(("Roland Walker" . "walker@pobox.com"))
|
||||
:maintainers
|
||||
'(("Roland Walker" . "walker@pobox.com"))
|
||||
:maintainer
|
||||
'("Roland Walker" . "walker@pobox.com")
|
||||
:keywords
|
||||
'("extensions")
|
||||
:url "http://github.com/rolandwalker/anaphora")
|
||||
;; Local Variables:
|
||||
;; no-byte-compile: t
|
||||
;; End:
|
||||
474
lisp/anaphora/anaphora.el
Normal file
474
lisp/anaphora/anaphora.el
Normal file
@@ -0,0 +1,474 @@
|
||||
;;; anaphora.el --- anaphoric macros providing implicit temp variables -*- lexical-binding: t -*-
|
||||
;;
|
||||
;; This code is in the public domain.
|
||||
;;
|
||||
;; Author: Roland Walker <walker@pobox.com>
|
||||
;; Homepage: http://github.com/rolandwalker/anaphora
|
||||
;; URL: http://raw.githubusercontent.com/rolandwalker/anaphora/master/anaphora.el
|
||||
;; Version: 1.0.4
|
||||
;; Last-Updated: 18 Jun 2018
|
||||
;; EmacsWiki: Anaphora
|
||||
;; Keywords: extensions
|
||||
;;
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; Quickstart
|
||||
;;
|
||||
;; (require 'anaphora)
|
||||
;;
|
||||
;; (awhen (big-long-calculation)
|
||||
;; (foo it) ; `it' is provided as
|
||||
;; (bar it)) ; a temporary variable
|
||||
;;
|
||||
;; ;; anonymous function to compute factorial using `self'
|
||||
;; (alambda (x) (if (= x 0) 1 (* x (self (1- x)))))
|
||||
;;
|
||||
;; ;; to fontify `it' and `self'
|
||||
;; (with-eval-after-load "lisp-mode"
|
||||
;; (anaphora-install-font-lock-keywords))
|
||||
;;
|
||||
;; Explanation
|
||||
;;
|
||||
;; Anaphoric expressions implicitly create one or more temporary
|
||||
;; variables which can be referred to during the expression. This
|
||||
;; technique can improve clarity in certain cases. It also enables
|
||||
;; recursion for anonymous functions.
|
||||
;;
|
||||
;; To use anaphora, place the anaphora.el library somewhere
|
||||
;; Emacs can find it, and add the following to your ~/.emacs file:
|
||||
;;
|
||||
;; (require 'anaphora)
|
||||
;;
|
||||
;; The following macros are made available
|
||||
;;
|
||||
;; `aand'
|
||||
;; `ablock'
|
||||
;; `acase'
|
||||
;; `acond'
|
||||
;; `aecase'
|
||||
;; `aetypecase'
|
||||
;; `apcase'
|
||||
;; `aif'
|
||||
;; `alambda'
|
||||
;; `alet'
|
||||
;; `aprog1'
|
||||
;; `aprog2'
|
||||
;; `atypecase'
|
||||
;; `awhen'
|
||||
;; `awhile'
|
||||
;; `a+'
|
||||
;; `a-'
|
||||
;; `a*'
|
||||
;; `a/'
|
||||
;;
|
||||
;; See Also
|
||||
;;
|
||||
;; M-x customize-group RET anaphora RET
|
||||
;; http://en.wikipedia.org/wiki/On_Lisp
|
||||
;; http://en.wikipedia.org/wiki/Anaphoric_macro
|
||||
;;
|
||||
;; Notes
|
||||
;;
|
||||
;; Partially based on examples from the book "On Lisp", by Paul
|
||||
;; Graham.
|
||||
;;
|
||||
;; Compatibility and Requirements
|
||||
;;
|
||||
;; GNU Emacs version 26.1 : yes
|
||||
;; GNU Emacs version 25.x : yes
|
||||
;; GNU Emacs version 24.x : yes
|
||||
;; GNU Emacs version 23.x : yes
|
||||
;; GNU Emacs version 22.x : yes
|
||||
;; GNU Emacs version 21.x and lower : unknown
|
||||
;;
|
||||
;; Bugs
|
||||
;;
|
||||
;; TODO
|
||||
;;
|
||||
;; better face for it and self
|
||||
;;
|
||||
;;; License
|
||||
;;
|
||||
;; All code contributed by the author to this library is placed in the
|
||||
;; public domain. It is the author's belief that the portions adapted
|
||||
;; from examples in "On Lisp" are in the public domain.
|
||||
;;
|
||||
;; Regardless of the copyright status of individual functions, all
|
||||
;; code herein is free software, and is provided without any express
|
||||
;; or implied warranties.
|
||||
;;
|
||||
;;; Code:
|
||||
;;
|
||||
|
||||
;;; requirements
|
||||
|
||||
;; for declare, labels, do, block, case, ecase, typecase, etypecase
|
||||
(require 'cl-lib)
|
||||
|
||||
;;; customizable variables
|
||||
|
||||
;;;###autoload
|
||||
(defgroup anaphora nil
|
||||
"Anaphoric macros providing implicit temp variables"
|
||||
:version "1.0.4"
|
||||
:link '(emacs-commentary-link :tag "Commentary" "anaphora")
|
||||
:link '(url-link :tag "GitHub" "http://github.com/rolandwalker/anaphora")
|
||||
:link '(url-link :tag "EmacsWiki" "http://emacswiki.org/emacs/Anaphora")
|
||||
:prefix "anaphora-"
|
||||
:group 'extensions)
|
||||
|
||||
;;;###autoload
|
||||
(defcustom anaphora-use-long-names-only nil
|
||||
"Use only long names such as `anaphoric-if' instead of traditional `aif'."
|
||||
:type 'boolean
|
||||
:group 'anaphora)
|
||||
|
||||
;;; font-lock
|
||||
|
||||
(defun anaphora-install-font-lock-keywords nil
|
||||
"Fontify keywords `it' and `self'."
|
||||
(font-lock-add-keywords 'emacs-lisp-mode `((,(concat "\\<" (regexp-opt '("it" "self") 'paren) "\\>")
|
||||
1 font-lock-variable-name-face)) 'append))
|
||||
|
||||
;;; aliases
|
||||
|
||||
;;;###autoload
|
||||
(progn
|
||||
(defun anaphora--install-traditional-aliases (&optional arg)
|
||||
"Install traditional short aliases for anaphoric macros.
|
||||
|
||||
With negative numeric ARG, remove traditional aliases."
|
||||
(let ((syms '(
|
||||
(if . t)
|
||||
(prog1 . t)
|
||||
(prog2 . t)
|
||||
(when . when)
|
||||
(while . t)
|
||||
(and . t)
|
||||
(cond . cond)
|
||||
(lambda . lambda)
|
||||
(block . block)
|
||||
(case . case)
|
||||
(ecase . ecase)
|
||||
(typecase . typecase)
|
||||
(etypecase . etypecase)
|
||||
(pcase . pcase)
|
||||
(let . let)
|
||||
(+ . t)
|
||||
(- . t)
|
||||
(* . t)
|
||||
(/ . t)
|
||||
)))
|
||||
(cond
|
||||
((and (numberp arg)
|
||||
(< arg 0))
|
||||
(dolist (cell syms)
|
||||
(when (ignore-errors
|
||||
(eq (symbol-function (intern-soft (format "a%s" (car cell))))
|
||||
(intern-soft (format "anaphoric-%s" (car cell)))))
|
||||
(fmakunbound (intern (format "a%s" (car cell)))))))
|
||||
(t
|
||||
(dolist (cell syms)
|
||||
(let* ((builtin (car cell))
|
||||
(traditional (intern (format "a%s" builtin)))
|
||||
(long (intern (format "anaphoric-%s" builtin))))
|
||||
(defalias traditional long)
|
||||
(put traditional 'lisp-indent-function
|
||||
(get builtin 'lisp-indent-function))
|
||||
(put traditional 'edebug-form-spec (cdr cell)))))))))
|
||||
|
||||
;;;###autoload
|
||||
(unless anaphora-use-long-names-only
|
||||
(anaphora--install-traditional-aliases))
|
||||
|
||||
;;; macros
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-if (cond then &rest else)
|
||||
"Like `if', but the result of evaluating COND is bound to `it'.
|
||||
|
||||
The variable `it' is available within THEN and ELSE.
|
||||
|
||||
COND, THEN, and ELSE are otherwise as documented for `if'."
|
||||
(declare (debug t)
|
||||
(indent 2))
|
||||
`(let ((it ,cond))
|
||||
(if it ,then ,@else)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-prog1 (first &rest body)
|
||||
"Like `prog1', but the result of evaluating FIRST is bound to `it'.
|
||||
|
||||
The variable `it' is available within BODY.
|
||||
|
||||
FIRST and BODY are otherwise as documented for `prog1'."
|
||||
(declare (debug t)
|
||||
(indent 1))
|
||||
`(let ((it ,first))
|
||||
(progn ,@body)
|
||||
it))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-prog2 (form1 form2 &rest body)
|
||||
"Like `prog2', but the result of evaluating FORM2 is bound to `it'.
|
||||
|
||||
The variable `it' is available within BODY.
|
||||
|
||||
FORM1, FORM2, and BODY are otherwise as documented for `prog2'."
|
||||
(declare (debug t)
|
||||
(indent 2))
|
||||
`(progn
|
||||
,form1
|
||||
(let ((it ,form2))
|
||||
(progn ,@body)
|
||||
it)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-when (cond &rest body)
|
||||
"Like `when', but the result of evaluating COND is bound to `it'.
|
||||
|
||||
The variable `it' is available within BODY.
|
||||
|
||||
COND and BODY are otherwise as documented for `when'."
|
||||
(declare (debug when)
|
||||
(indent 1))
|
||||
`(anaphoric-if ,cond
|
||||
(progn ,@body)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-while (test &rest body)
|
||||
"Like `while', but the result of evaluating TEST is bound to `it'.
|
||||
|
||||
The variable `it' is available within BODY.
|
||||
|
||||
TEST and BODY are otherwise as documented for `while'."
|
||||
(declare (debug t)
|
||||
(indent 1))
|
||||
`(do ((it ,test ,test))
|
||||
((not it))
|
||||
,@body))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-and (&rest conditions)
|
||||
"Like `and', but the result of the previous condition is bound to `it'.
|
||||
|
||||
The variable `it' is available within all CONDITIONS after the
|
||||
initial one.
|
||||
|
||||
CONDITIONS are otherwise as documented for `and'.
|
||||
|
||||
Note that some implementations of this macro bind only the first
|
||||
condition to `it', rather than each successive condition."
|
||||
(declare (debug t))
|
||||
(cond
|
||||
((null conditions)
|
||||
t)
|
||||
((null (cdr conditions))
|
||||
(car conditions))
|
||||
(t
|
||||
`(anaphoric-if ,(car conditions) (anaphoric-and ,@(cdr conditions))))))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-cond (&rest clauses)
|
||||
"Like `cond', but the result of each condition is bound to `it'.
|
||||
|
||||
The variable `it' is available within the remainder of each of CLAUSES.
|
||||
|
||||
CLAUSES are otherwise as documented for `cond'."
|
||||
(declare (debug cond))
|
||||
(if (null clauses)
|
||||
nil
|
||||
(let ((cl1 (car clauses))
|
||||
(sym (gensym)))
|
||||
`(let ((,sym ,(car cl1)))
|
||||
(if ,sym
|
||||
(if (null ',(cdr cl1))
|
||||
,sym
|
||||
(let ((it ,sym)) ,@(cdr cl1)))
|
||||
(anaphoric-cond ,@(cdr clauses)))))))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-lambda (args &rest body)
|
||||
"Like `lambda', but the function may refer to itself as `self'.
|
||||
|
||||
ARGS and BODY are otherwise as documented for `lambda'."
|
||||
(declare (debug lambda)
|
||||
(indent defun))
|
||||
`(cl-labels ((self ,args ,@body))
|
||||
#'self))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-block (name &rest body)
|
||||
"Like `block', but the result of the previous expression is bound to `it'.
|
||||
|
||||
The variable `it' is available within all expressions of BODY
|
||||
except the initial one.
|
||||
|
||||
NAME and BODY are otherwise as documented for `block'."
|
||||
(declare (debug block)
|
||||
(indent 1))
|
||||
`(cl-block ,name
|
||||
,(funcall (anaphoric-lambda (body)
|
||||
(cl-case (length body)
|
||||
(0 nil)
|
||||
(1 (car body))
|
||||
(t `(let ((it ,(car body)))
|
||||
,(self (cdr body))))))
|
||||
body)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-case (expr &rest clauses)
|
||||
"Like `case', but the result of evaluating EXPR is bound to `it'.
|
||||
|
||||
The variable `it' is available within CLAUSES.
|
||||
|
||||
EXPR and CLAUSES are otherwise as documented for `case'."
|
||||
(declare (debug case)
|
||||
(indent 1))
|
||||
`(let ((it ,expr))
|
||||
(cl-case it ,@clauses)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-ecase (expr &rest clauses)
|
||||
"Like `ecase', but the result of evaluating EXPR is bound to `it'.
|
||||
|
||||
The variable `it' is available within CLAUSES.
|
||||
|
||||
EXPR and CLAUSES are otherwise as documented for `ecase'."
|
||||
(declare (debug ecase)
|
||||
(indent 1))
|
||||
`(let ((it ,expr))
|
||||
(cl-ecase it ,@clauses)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-typecase (expr &rest clauses)
|
||||
"Like `typecase', but the result of evaluating EXPR is bound to `it'.
|
||||
|
||||
The variable `it' is available within CLAUSES.
|
||||
|
||||
EXPR and CLAUSES are otherwise as documented for `typecase'."
|
||||
(declare (debug typecase)
|
||||
(indent 1))
|
||||
`(let ((it ,expr))
|
||||
(cl-typecase it ,@clauses)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-etypecase (expr &rest clauses)
|
||||
"Like `etypecase', but result of evaluating EXPR is bound to `it'.
|
||||
|
||||
The variable `it' is available within CLAUSES.
|
||||
|
||||
EXPR and CLAUSES are otherwise as documented for `etypecase'."
|
||||
(declare (debug etypecase)
|
||||
(indent 1))
|
||||
`(let ((it ,expr))
|
||||
(cl-etypecase it ,@clauses)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-pcase (expr &rest clauses)
|
||||
"Like `pcase', but the result of evaluating EXPR is bound to `it'.
|
||||
|
||||
The variable `it' is available within CLAUSES.
|
||||
|
||||
EXPR and CLAUSES are otherwise as documented for `pcase'."
|
||||
(declare (debug pcase)
|
||||
(indent 1))
|
||||
`(let ((it ,expr))
|
||||
(pcase it ,@clauses)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-let (form &rest body)
|
||||
"Like `let', but the result of evaluating FORM is bound to `it'.
|
||||
|
||||
FORM and BODY are otherwise as documented for `let'."
|
||||
(declare (debug let)
|
||||
(indent 1))
|
||||
`(let ((it ,form))
|
||||
(progn ,@body)))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-+ (&rest numbers-or-markers)
|
||||
"Like `+', but the result of evaluating the previous expression is bound to `it'.
|
||||
|
||||
The variable `it' is available within all expressions after the
|
||||
initial one.
|
||||
|
||||
NUMBERS-OR-MARKERS are otherwise as documented for `+'."
|
||||
(declare (debug t))
|
||||
(cond
|
||||
((null numbers-or-markers)
|
||||
0)
|
||||
(t
|
||||
`(let ((it ,(car numbers-or-markers)))
|
||||
(+ it (anaphoric-+ ,@(cdr numbers-or-markers)))))))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-- (&optional number-or-marker &rest numbers-or-markers)
|
||||
"Like `-', but the result of evaluating the previous expression is bound to `it'.
|
||||
|
||||
The variable `it' is available within all expressions after the
|
||||
initial one.
|
||||
|
||||
NUMBER-OR-MARKER and NUMBERS-OR-MARKERS are otherwise as
|
||||
documented for `-'."
|
||||
(declare (debug t))
|
||||
(cond
|
||||
((null number-or-marker)
|
||||
0)
|
||||
((null numbers-or-markers)
|
||||
`(- ,number-or-marker))
|
||||
(t
|
||||
`(let ((it ,(car numbers-or-markers)))
|
||||
(- ,number-or-marker (+ it (anaphoric-+ ,@(cdr numbers-or-markers))))))))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-* (&rest numbers-or-markers)
|
||||
"Like `*', but the result of evaluating the previous expression is bound to `it'.
|
||||
|
||||
The variable `it' is available within all expressions after the
|
||||
initial one.
|
||||
|
||||
NUMBERS-OR-MARKERS are otherwise as documented for `*'."
|
||||
(declare (debug t))
|
||||
(cond
|
||||
((null numbers-or-markers)
|
||||
1)
|
||||
(t
|
||||
`(let ((it ,(car numbers-or-markers)))
|
||||
(* it (anaphoric-* ,@(cdr numbers-or-markers)))))))
|
||||
|
||||
;;;###autoload
|
||||
(defmacro anaphoric-/ (dividend divisor &rest divisors)
|
||||
"Like `/', but the result of evaluating the previous divisor is bound to `it'.
|
||||
|
||||
The variable `it' is available within all expressions after the
|
||||
first divisor.
|
||||
|
||||
DIVIDEND, DIVISOR, and DIVISORS are otherwise as documented for `/'."
|
||||
(declare (debug t))
|
||||
(cond
|
||||
((null divisors)
|
||||
`(/ ,dividend ,divisor))
|
||||
(t
|
||||
`(let ((it ,divisor))
|
||||
(/ ,dividend (* it (anaphoric-* ,@divisors)))))))
|
||||
|
||||
(provide 'anaphora)
|
||||
|
||||
;;
|
||||
;; Emacs
|
||||
;;
|
||||
;; Local Variables:
|
||||
;; indent-tabs-mode: nil
|
||||
;; mangle-whitespace: t
|
||||
;; require-final-newline: t
|
||||
;; coding: utf-8
|
||||
;; byte-compile-warnings: (not cl-functions redefine)
|
||||
;; End:
|
||||
;;
|
||||
;; LocalWords: Anaphora EXPR awhen COND ARGS alambda ecase typecase
|
||||
;; LocalWords: etypecase aprog aand acond ablock acase aecase alet
|
||||
;; LocalWords: atypecase aetypecase apcase
|
||||
;;
|
||||
|
||||
;;; anaphora.el ends here
|
||||
14
lisp/code-cells/code-cells-pkg.el
Normal file
14
lisp/code-cells/code-cells-pkg.el
Normal file
@@ -0,0 +1,14 @@
|
||||
(define-package "code-cells" "20231119.2138" "Lightweight notebooks with support for ipynb files"
|
||||
'((emacs "27.1"))
|
||||
:commit "44546ca256f3da29e3ac884e3d699c8455acbd6e" :authors
|
||||
'(("Augusto Stoffel" . "arstoffel@gmail.com"))
|
||||
:maintainers
|
||||
'(("Augusto Stoffel" . "arstoffel@gmail.com"))
|
||||
:maintainer
|
||||
'("Augusto Stoffel" . "arstoffel@gmail.com")
|
||||
:keywords
|
||||
'("convenience" "outlines")
|
||||
:url "https://github.com/astoff/code-cells.el")
|
||||
;; Local Variables:
|
||||
;; no-byte-compile: t
|
||||
;; End:
|
||||
442
lisp/code-cells/code-cells.el
Normal file
442
lisp/code-cells/code-cells.el
Normal file
@@ -0,0 +1,442 @@
|
||||
;;; code-cells.el --- Lightweight notebooks with support for ipynb files -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2022, 2023 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Augusto Stoffel <arstoffel@gmail.com>
|
||||
;; Keywords: convenience, outlines
|
||||
;; URL: https://github.com/astoff/code-cells.el
|
||||
;; Package-Requires: ((emacs "27.1"))
|
||||
;; Version: 0.4
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; With this package, you can efficiently navigate, edit and execute
|
||||
;; code split into cells according to certain magic comments. It also
|
||||
;; allows you to open ipynb notebook files directly in Emacs. They
|
||||
;; will be automatically converted to a script for editing, and
|
||||
;; converted back to notebook format when saving. An external tool,
|
||||
;; Jupytext by default, is required for this.
|
||||
;;
|
||||
;; A minor mode, `code-cells-mode', provides the following features:
|
||||
;;
|
||||
;; - Fontification of cell boundaries.
|
||||
;;
|
||||
;; - Keybindings for the cell navigation and evaluation commands,
|
||||
;; under the `C-c %' prefix.
|
||||
;;
|
||||
;; - Outline mode integration: cell headers have outline level
|
||||
;; determined by the number of percent signs or asterisks; within a
|
||||
;; cell, outline headings are as determined by the major mode, but
|
||||
;; they are demoted by an amount corresponding to the level of the
|
||||
;; containing cell. This provides code folding and hierarchical
|
||||
;; navigation, among other things, when `outline-minor-mode' is
|
||||
;; active.
|
||||
;;
|
||||
;; This minor mode is automatically activated when opening an ipynb
|
||||
;; file, but you can also activate it in any other buffer, either
|
||||
;; manually or through a hook.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'outline)
|
||||
(require 'pulse)
|
||||
(eval-when-compile
|
||||
(require 'cl-lib)
|
||||
(require 'let-alist)
|
||||
(require 'rx))
|
||||
|
||||
(defgroup code-cells nil
|
||||
"Utilities for code split into cells."
|
||||
:group 'convenience
|
||||
:prefix "code-cells-")
|
||||
|
||||
;;; Cell navigation
|
||||
|
||||
(defcustom code-cells-boundary-regexp
|
||||
(rx line-start
|
||||
(+ (syntax comment-start))
|
||||
(or (seq (* (syntax whitespace)) "%" (group-n 1 (+ "%")))
|
||||
(group-n 1 (+ "*"))
|
||||
(seq " In[" (* (any space digit)) "]:")))
|
||||
"Regular expression specifying cell boundaries.
|
||||
It should match at the beginning of a line. The length of the
|
||||
first capture determines the outline level."
|
||||
:type 'regexp
|
||||
:safe #'stringp)
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-forward-cell (&optional arg)
|
||||
"Move to the next cell boundary, or end of buffer.
|
||||
With ARG, repeat this that many times. If ARG is negative, move
|
||||
backward."
|
||||
(interactive "p")
|
||||
(let ((page-delimiter code-cells-boundary-regexp))
|
||||
(when (and (< 0 arg) (looking-at page-delimiter))
|
||||
(forward-char))
|
||||
(forward-page arg)
|
||||
(unless (eobp)
|
||||
(move-beginning-of-line 1))))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-backward-cell (&optional arg)
|
||||
"Move to the previous cell boundary, or beginning of buffer.
|
||||
With ARG, repeat this that many times. If ARG is negative, move
|
||||
forward."
|
||||
(interactive "p")
|
||||
(code-cells-forward-cell (- (or arg 1))))
|
||||
|
||||
(defun code-cells--bounds (&optional count use-region no-header)
|
||||
"Return the bounds of the current code cell, as a cons.
|
||||
|
||||
If COUNT is non-nil, return instead a region containing COUNT
|
||||
cells and starting or ending with the current cell, depending on
|
||||
the sign of COUNT.
|
||||
|
||||
If USE-REGION is non-nil and the region is active, return the
|
||||
region bounds instead.
|
||||
|
||||
If NO-HEADER is non-nil, do not include the cell boundary line."
|
||||
(if (and use-region (use-region-p))
|
||||
(list (region-beginning) (region-end))
|
||||
(setq count (or count 1))
|
||||
(save-excursion
|
||||
(let ((end (progn (code-cells-forward-cell (max count 1))
|
||||
(point))))
|
||||
(code-cells-backward-cell (abs count))
|
||||
(when no-header (forward-line))
|
||||
(list (point) end)))))
|
||||
|
||||
(defun code-cells--bounds-of-cell-relative-from (distance)
|
||||
"Return the bounds of the cell DISTANCE cells away from the current one."
|
||||
(save-excursion
|
||||
(when (/= 0 distance)
|
||||
;; Except when at the boundary, `(code-cells-forward-cell -1)' doesn't
|
||||
;; move out of current cell
|
||||
(unless (looking-at-p code-cells-boundary-regexp)
|
||||
(code-cells-backward-cell))
|
||||
(code-cells-forward-cell distance))
|
||||
(code-cells--bounds)))
|
||||
|
||||
(defun code-cells-move-cell-down (arg)
|
||||
"Move current code cell vertically ARG cells.
|
||||
Move up when ARG is negative and move down otherwise."
|
||||
(interactive "p")
|
||||
(pcase-let ((`(,current-beg ,current-end) (code-cells--bounds))
|
||||
(`(,next-beg ,next-end) (code-cells--bounds-of-cell-relative-from arg)))
|
||||
(unless (save-excursion
|
||||
(and (/= current-beg next-beg)
|
||||
(goto-char current-beg)
|
||||
(looking-at-p code-cells-boundary-regexp)
|
||||
(goto-char next-beg)
|
||||
(looking-at-p code-cells-boundary-regexp)))
|
||||
(user-error "Can't move cell"))
|
||||
(transpose-regions current-beg current-end next-beg next-end)))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-move-cell-up (&optional arg)
|
||||
"Move current code cell vertically up ARG cells."
|
||||
(interactive "p")
|
||||
(code-cells-move-cell-down (- arg)))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-mark-cell (&optional arg)
|
||||
"Put point at the beginning of this cell, mark at end.
|
||||
If ARG is non-nil, mark that many cells."
|
||||
(interactive "p")
|
||||
(pcase-let ((`(,start ,end) (code-cells--bounds arg)))
|
||||
(goto-char start)
|
||||
(push-mark end nil t)))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-comment-or-uncomment (&optional arg)
|
||||
"Comment or uncomment the current code cell.
|
||||
|
||||
ARG, if provided, is the number of comment characters to add or
|
||||
remove."
|
||||
(interactive "P")
|
||||
(pcase-let* ((`(,header ,end) (code-cells--bounds arg))
|
||||
(start (save-excursion
|
||||
(goto-char header)
|
||||
(forward-line)
|
||||
(point))))
|
||||
(comment-or-uncomment-region start end)))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-command (fun &rest options)
|
||||
"Return an anonymous command calling FUN on the current cell.
|
||||
|
||||
FUN is a function that takes two character positions as argument.
|
||||
Most interactive commands that act on a region are of this form
|
||||
and can be used here.
|
||||
|
||||
If OPTIONS contains the keyword :use-region, the command will act
|
||||
on the region instead of the current cell when appropriate.
|
||||
|
||||
If OPTIONS contains the keyword :pulse, provide visual feedback
|
||||
via `pulse-momentary-highlight-region'."
|
||||
(let ((use-region (car (memq :use-region options)))
|
||||
(pulse (car (memq :pulse options))))
|
||||
(lambda ()
|
||||
(interactive)
|
||||
(pcase-let ((`(,start ,end) (code-cells--bounds nil use-region)))
|
||||
(when pulse (pulse-momentary-highlight-region start end))
|
||||
(funcall fun start end)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-speed-key (command)
|
||||
"Return a speed key definition, suitable for passing to `define-key'.
|
||||
The resulting keybinding will only have any effect when the point
|
||||
is at the beginning of a cell heading, in which case it executes
|
||||
COMMAND."
|
||||
(list 'menu-item nil command
|
||||
:filter (lambda (d)
|
||||
(when (and (bolp)
|
||||
(looking-at code-cells-boundary-regexp))
|
||||
d))))
|
||||
|
||||
;;; Code evaluation
|
||||
|
||||
(defcustom code-cells-eval-region-commands
|
||||
`((drepl--current . drepl-eval-region)
|
||||
(jupyter-repl-interaction-mode . ,(apply-partially 'jupyter-eval-region nil))
|
||||
(python-mode . python-shell-send-region)
|
||||
(emacs-lisp-mode . eval-region)
|
||||
(lisp-interaction-mode . eval-region))
|
||||
"Alist of commands to evaluate a region.
|
||||
The keys are major or minor modes and the values are functions
|
||||
taking region bounds as argument."
|
||||
:type '(alist :key-type symbol :value-type symbol))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-eval (start end)
|
||||
"Evaluate code according to current modes.
|
||||
The first suitable function from `code-cells-eval-region-commands'
|
||||
is used to do the job.
|
||||
|
||||
Interactively, evaluate the region, if active, otherwise the
|
||||
current code cell. With a numeric prefix, evaluate that many
|
||||
code cells.
|
||||
|
||||
Called from Lisp, evaluate region between START and END."
|
||||
(interactive (code-cells--bounds (prefix-numeric-value current-prefix-arg)
|
||||
'use-region
|
||||
'no-header))
|
||||
(funcall
|
||||
(or (seq-some (pcase-lambda (`(,mode . ,fun))
|
||||
(when (or (and (boundp mode) (symbol-value mode))
|
||||
(derived-mode-p mode))
|
||||
fun))
|
||||
code-cells-eval-region-commands)
|
||||
(user-error
|
||||
"No entry for the current modes in `code-cells-eval-region-commands'"))
|
||||
start end)
|
||||
(pulse-momentary-highlight-region start end))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-eval-above (arg)
|
||||
"Evaluate this and all above cells.
|
||||
ARG (interactively, the prefix argument) specifies how many
|
||||
additional cells after point to include."
|
||||
(interactive "p")
|
||||
(code-cells-eval (point-min) (save-excursion
|
||||
(code-cells-forward-cell arg)
|
||||
(point))))
|
||||
|
||||
;;; Minor mode
|
||||
|
||||
(defvar-local code-cells--saved-vars nil
|
||||
"A place to save variables before activating `code-cells-mode'.")
|
||||
|
||||
(defun code-cells--outline-level ()
|
||||
"Compute the outline level, taking code cells into account.
|
||||
To be used as the value of the variable `outline-level'.
|
||||
|
||||
At a cell boundary, returns the cell outline level, as determined by
|
||||
`code-cells-boundary-regexp'. Otherwise, returns the sum of the
|
||||
outline level as determined by the major mode and the current cell
|
||||
level."
|
||||
(let* ((at-boundary (looking-at-p code-cells-boundary-regexp))
|
||||
(mm-level (if at-boundary
|
||||
0
|
||||
(funcall (car code-cells--saved-vars))))
|
||||
(cell-level (if (or at-boundary
|
||||
(save-excursion
|
||||
(re-search-backward
|
||||
code-cells-boundary-regexp nil t)))
|
||||
(if (match-string 1)
|
||||
(- (match-end 1) (match-beginning 1))
|
||||
1)
|
||||
0)))
|
||||
(+ cell-level mm-level)))
|
||||
|
||||
(defface code-cells-header-line '((t :extend t :overline t :inherit font-lock-comment-face))
|
||||
"Face used by `code-cells-mode' to highlight cell boundaries.")
|
||||
|
||||
(defun code-cells--font-lock-keywords ()
|
||||
"Font lock keywords to highlight cell boundaries."
|
||||
`((,(rx (regexp code-cells-boundary-regexp) (* any) "\n")
|
||||
0 'code-cells-header-line append)))
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode code-cells-mode
|
||||
"Minor mode for cell-oriented code."
|
||||
:keymap (make-sparse-keymap)
|
||||
(if code-cells-mode
|
||||
(progn
|
||||
(setq-local
|
||||
code-cells--saved-vars (list outline-level
|
||||
outline-regexp
|
||||
outline-heading-end-regexp
|
||||
paragraph-start)
|
||||
outline-level 'code-cells--outline-level
|
||||
outline-regexp (rx (or (regexp code-cells-boundary-regexp)
|
||||
(regexp outline-regexp)))
|
||||
outline-heading-end-regexp "\n"
|
||||
paragraph-separate (rx (or (regexp paragraph-separate)
|
||||
(regexp code-cells-boundary-regexp))))
|
||||
(font-lock-add-keywords nil (code-cells--font-lock-keywords)))
|
||||
(setq-local outline-level (pop code-cells--saved-vars)
|
||||
outline-regexp (pop code-cells--saved-vars)
|
||||
outline-heading-end-regexp (pop code-cells--saved-vars)
|
||||
paragraph-separate (pop code-cells--saved-vars))
|
||||
(font-lock-remove-keywords nil (code-cells--font-lock-keywords)))
|
||||
(font-lock-flush))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-mode-maybe ()
|
||||
"Turn on `code-cells-mode' if the buffer appears to contain cells.
|
||||
This function is useful when added to a major mode hook."
|
||||
(when (save-excursion
|
||||
(goto-char (point-min))
|
||||
(re-search-forward code-cells-boundary-regexp 5000 t))
|
||||
(code-cells-mode)))
|
||||
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key code-cells-mode-map "\C-c%" map)
|
||||
(define-key map ";" 'code-cells-comment-or-uncomment)
|
||||
(define-key map "@" 'code-cells-mark-cell)
|
||||
(define-key map "b" 'code-cells-backward-cell)
|
||||
(define-key map "f" 'code-cells-forward-cell)
|
||||
(define-key map "B" 'code-cells-move-cell-up)
|
||||
(define-key map "F" 'code-cells-move-cell-down)
|
||||
(define-key map "e" 'code-cells-eval))
|
||||
|
||||
;;; Jupyter notebook conversion
|
||||
|
||||
(defcustom code-cells-convert-ipynb-style
|
||||
'(("jupytext" "--to" "ipynb")
|
||||
("jupytext" "--to" "auto:percent")
|
||||
code-cells--guess-mode
|
||||
code-cells-convert-ipynb-hook)
|
||||
"Determines how to convert ipynb files for editing.
|
||||
The first two entries are lists of strings: the command name and
|
||||
arguments used, respectively, to convert to and from ipynb
|
||||
format.
|
||||
|
||||
The third entry is a function called with no arguments to
|
||||
determine the major mode to be called. The default setting tries
|
||||
to guess it from the notebook metadata.
|
||||
|
||||
The fourth entry, also optional, is a hook run after the new
|
||||
major mode is activated."
|
||||
:type '(list (repeat string) (repeat string) function symbol))
|
||||
|
||||
(defvar code-cells-convert-ipynb-hook '(code-cells-mode)
|
||||
"Hook used in the default `code-cells-convert-ipynb-style'.")
|
||||
|
||||
(defun code-cells--call-process (buffer command)
|
||||
"Pipe BUFFER through COMMAND, with output to the current buffer.
|
||||
Returns the process exit code. COMMAND is a list of strings, the
|
||||
program name followed by arguments."
|
||||
(unless (executable-find (car command))
|
||||
(error "Can't find %s" (car command)))
|
||||
(let ((logfile (make-temp-file "emacs-code-cells-")))
|
||||
(unwind-protect
|
||||
(prog1
|
||||
(apply #'call-process-region nil nil (car command) nil
|
||||
(list buffer logfile) nil
|
||||
(cdr command))
|
||||
(with-temp-buffer
|
||||
(insert-file-contents logfile)
|
||||
(unless (zerop (buffer-size))
|
||||
(lwarn 'code-cells :warning
|
||||
"Notebook conversion command %s said:\n%s"
|
||||
command
|
||||
(buffer-substring-no-properties
|
||||
(point-min) (point-max))))))
|
||||
(delete-file logfile))))
|
||||
|
||||
(defun code-cells--guess-mode ()
|
||||
"Guess major mode associated to the current ipynb buffer."
|
||||
(require 'json)
|
||||
(declare-function json-read "json.el")
|
||||
(goto-char (point-min))
|
||||
(let* ((nb (cl-letf ;; Skip over the possibly huge "cells" section
|
||||
(((symbol-function 'json-read-array) 'forward-sexp))
|
||||
(json-read)))
|
||||
(lang (let-alist nb
|
||||
(or .metadata.kernelspec.language
|
||||
.metadata.jupytext.main_language)))
|
||||
(mode (intern (concat lang "-mode"))))
|
||||
(alist-get mode (bound-and-true-p major-mode-remap-alist) mode)))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-convert-ipynb ()
|
||||
"Convert buffer from ipynb format to a regular script."
|
||||
(interactive)
|
||||
(let* ((mode (funcall (or (nth 2 code-cells-convert-ipynb-style)
|
||||
(progn ;For backwards compatibility with v0.3
|
||||
(lwarn 'code-cells :warning "\
|
||||
The third entry of `code-cells-convert-ipynb-style' should not be nil.")
|
||||
#'code-cells--guess-mode))))
|
||||
(exit (progn
|
||||
(goto-char (point-min))
|
||||
(code-cells--call-process t (nth 1 code-cells-convert-ipynb-style)))))
|
||||
(unless (zerop exit)
|
||||
(delete-region (point-min) (point))
|
||||
(error "Error converting notebook (exit code %s)" exit))
|
||||
(delete-region (point) (point-max))
|
||||
(goto-char (point-min))
|
||||
(set-buffer-modified-p nil)
|
||||
(add-hook 'write-file-functions #'code-cells-write-ipynb 80 t)
|
||||
(when (fboundp mode)
|
||||
(funcall mode)
|
||||
(run-hooks (nth 3 code-cells-convert-ipynb-style)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun code-cells-write-ipynb (&optional file)
|
||||
"Convert buffer to ipynb format and write to FILE.
|
||||
Interactively, asks for the file name. When called from Lisp,
|
||||
FILE defaults to the current buffer file name."
|
||||
(interactive "F")
|
||||
(let* ((file (or file buffer-file-name))
|
||||
(temp (generate-new-buffer " *cells--call-process output*"))
|
||||
(exit (code-cells--call-process temp (nth 0 code-cells-convert-ipynb-style))))
|
||||
(unless (eq 0 exit)
|
||||
(error "Error converting notebook (exit code %s)" exit))
|
||||
(with-current-buffer temp
|
||||
(write-region nil nil file)
|
||||
(kill-buffer))
|
||||
(when (eq file buffer-file-name)
|
||||
(set-buffer-modified-p nil)
|
||||
(set-visited-file-modtime))
|
||||
'job-done))
|
||||
|
||||
;;;###autoload
|
||||
(add-to-list 'auto-mode-alist '("\\.ipynb\\'" . code-cells-convert-ipynb))
|
||||
|
||||
(provide 'code-cells)
|
||||
;;; code-cells.el ends here
|
||||
14
lisp/deferred/deferred-pkg.el
Normal file
14
lisp/deferred/deferred-pkg.el
Normal file
@@ -0,0 +1,14 @@
|
||||
(define-package "deferred" "20170901.1330" "Simple asynchronous functions for emacs lisp"
|
||||
'((emacs "24.4"))
|
||||
:commit "2239671d94b38d92e9b28d4e12fd79814cfb9c16" :authors
|
||||
'(("SAKURAI Masashi <m.sakurai at kiwanami.net>"))
|
||||
:maintainers
|
||||
'(("SAKURAI Masashi <m.sakurai at kiwanami.net>"))
|
||||
:maintainer
|
||||
'("SAKURAI Masashi <m.sakurai at kiwanami.net>")
|
||||
:keywords
|
||||
'("deferred" "async")
|
||||
:url "https://github.com/kiwanami/emacs-deferred")
|
||||
;; Local Variables:
|
||||
;; no-byte-compile: t
|
||||
;; End:
|
||||
971
lisp/deferred/deferred.el
Normal file
971
lisp/deferred/deferred.el
Normal file
@@ -0,0 +1,971 @@
|
||||
;;; deferred.el --- Simple asynchronous functions for emacs lisp -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2010-2016 SAKURAI Masashi
|
||||
|
||||
;; Author: SAKURAI Masashi <m.sakurai at kiwanami.net>
|
||||
;; Version: 0.5.1
|
||||
;; Keywords: deferred, async
|
||||
;; Package-Requires: ((emacs "24.4"))
|
||||
;; URL: https://github.com/kiwanami/emacs-deferred
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; 'deferred.el' is a simple library for asynchronous tasks.
|
||||
;; [https://github.com/kiwanami/emacs-deferred]
|
||||
|
||||
;; The API is almost the same as JSDeferred written by cho45. See the
|
||||
;; JSDeferred and Mochikit.Async web sites for further documentations.
|
||||
;; [https://github.com/cho45/jsdeferred]
|
||||
;; [http://mochikit.com/doc/html/MochiKit/Async.html]
|
||||
|
||||
;; A good introduction document (JavaScript)
|
||||
;; [http://cho45.stfuawsc.com/jsdeferred/doc/intro.en.html]
|
||||
|
||||
;;; Samples:
|
||||
|
||||
;; ** HTTP Access
|
||||
|
||||
;; (require 'url)
|
||||
;; (deferred:$
|
||||
;; (deferred:url-retrieve "http://www.gnu.org")
|
||||
;; (deferred:nextc it
|
||||
;; (lambda (buf)
|
||||
;; (insert (with-current-buffer buf (buffer-string)))
|
||||
;; (kill-buffer buf))))
|
||||
|
||||
;; ** Invoking command tasks
|
||||
|
||||
;; (deferred:$
|
||||
;; (deferred:process "wget" "-O" "a.jpg" "http://www.gnu.org/software/emacs/tour/images/splash.png")
|
||||
;; (deferred:nextc it
|
||||
;; (lambda (x) (deferred:process "convert" "a.jpg" "-resize" "100x100" "jpg:b.jpg")))
|
||||
;; (deferred:nextc it
|
||||
;; (lambda (x)
|
||||
;; (insert-image (create-image (expand-file-name "b.jpg") 'jpeg nil)))))
|
||||
|
||||
;; See the readme for further API documentation.
|
||||
|
||||
;; ** Applications
|
||||
|
||||
;; *Inertial scrolling for Emacs
|
||||
;; [https://github.com/kiwanami/emacs-inertial-scroll]
|
||||
|
||||
;; This program makes simple multi-thread function, using
|
||||
;; deferred.el.
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'subr-x)
|
||||
|
||||
(declare-function pp-display-expression 'pp)
|
||||
|
||||
(defvar deferred:version nil "deferred.el version")
|
||||
(setq deferred:version "0.5.0")
|
||||
|
||||
;;; Code:
|
||||
|
||||
(defmacro deferred:aand (test &rest rest)
|
||||
"[internal] Anaphoric AND."
|
||||
(declare (debug ("test" form &rest form)))
|
||||
`(let ((it ,test))
|
||||
(if it ,(if rest `(deferred:aand ,@rest) 'it))))
|
||||
|
||||
(defmacro deferred:$ (&rest elements)
|
||||
"Anaphoric function chain macro for deferred chains."
|
||||
(declare (debug (&rest form)))
|
||||
`(let (it)
|
||||
,@(cl-loop for i in elements
|
||||
collect
|
||||
`(setq it ,i))
|
||||
it))
|
||||
|
||||
(defmacro deferred:lambda (args &rest body)
|
||||
"Anaphoric lambda macro for self recursion."
|
||||
(declare (debug ("args" form &rest form)))
|
||||
(let ((argsyms (cl-loop repeat (length args) collect (cl-gensym))))
|
||||
`(lambda (,@argsyms)
|
||||
(let (self)
|
||||
(setq self (lambda( ,@args ) ,@body))
|
||||
(funcall self ,@argsyms)))))
|
||||
|
||||
(cl-defmacro deferred:try (d &key catch finally)
|
||||
"Try-catch-finally macro. This macro simulates the
|
||||
try-catch-finally block asynchronously. CATCH and FINALLY can be
|
||||
nil. Because of asynchrony, this macro does not ensure that the
|
||||
task FINALLY should be called."
|
||||
(let ((chain
|
||||
(if catch `((deferred:error it ,catch)))))
|
||||
(when finally
|
||||
(setq chain (append chain `((deferred:watch it ,finally)))))
|
||||
`(deferred:$ ,d ,@chain)))
|
||||
|
||||
(defun deferred:setTimeout (f msec)
|
||||
"[internal] Timer function that emulates the `setTimeout' function in JS."
|
||||
(run-at-time (/ msec 1000.0) nil f))
|
||||
|
||||
(defun deferred:cancelTimeout (id)
|
||||
"[internal] Timer cancellation function that emulates the `cancelTimeout' function in JS."
|
||||
(cancel-timer id))
|
||||
|
||||
(defun deferred:run-with-idle-timer (sec f)
|
||||
"[internal] Wrapper function for run-with-idle-timer."
|
||||
(run-with-idle-timer sec nil f))
|
||||
|
||||
(defun deferred:call-lambda (f &optional arg)
|
||||
"[internal] Call a function with one or zero argument safely.
|
||||
The lambda function can define with zero and one argument."
|
||||
(condition-case err
|
||||
(funcall f arg)
|
||||
('wrong-number-of-arguments
|
||||
(display-warning 'deferred "\
|
||||
Callback that takes no argument may be specified.
|
||||
Passing callback with no argument is deprecated.
|
||||
Callback must take one argument.
|
||||
Or, this error is coming from somewhere inside of the callback: %S" err)
|
||||
(condition-case nil
|
||||
(funcall f)
|
||||
('wrong-number-of-arguments
|
||||
(signal 'wrong-number-of-arguments (cdr err))))))) ; return the first error
|
||||
|
||||
;; debug
|
||||
|
||||
(eval-and-compile
|
||||
(defvar deferred:debug nil "Debug output switch."))
|
||||
(defvar deferred:debug-count 0 "[internal] Debug output counter.")
|
||||
|
||||
(defmacro deferred:message (&rest args)
|
||||
"[internal] Debug log function."
|
||||
(when deferred:debug
|
||||
`(progn
|
||||
(with-current-buffer (get-buffer-create "*deferred:debug*")
|
||||
(save-excursion
|
||||
(goto-char (point-max))
|
||||
(insert (format "%5i %s\n" deferred:debug-count (format ,@args)))))
|
||||
(cl-incf deferred:debug-count))))
|
||||
|
||||
(defun deferred:message-mark ()
|
||||
"[internal] Debug log function."
|
||||
(interactive)
|
||||
(deferred:message "==================== mark ==== %s"
|
||||
(format-time-string "%H:%M:%S" (current-time))))
|
||||
|
||||
(defun deferred:pp (d)
|
||||
(require 'pp)
|
||||
(deferred:$
|
||||
(deferred:nextc d
|
||||
(lambda (x)
|
||||
(pp-display-expression x "*deferred:pp*")))
|
||||
(deferred:error it
|
||||
(lambda (e)
|
||||
(pp-display-expression e "*deferred:pp*")))
|
||||
(deferred:nextc it
|
||||
(lambda (_x) (pop-to-buffer "*deferred:pp*")))))
|
||||
|
||||
(defvar deferred:debug-on-signal nil
|
||||
"If non nil, the value `debug-on-signal' is substituted this
|
||||
value in the `condition-case' form in deferred
|
||||
implementations. Then, Emacs debugger can catch an error occurred
|
||||
in the asynchronous tasks.")
|
||||
|
||||
(defmacro deferred:condition-case (var protected-form &rest handlers)
|
||||
"[internal] Custom condition-case. See the comment for
|
||||
`deferred:debug-on-signal'."
|
||||
(declare (debug condition-case)
|
||||
(indent 2))
|
||||
`(let ((debug-on-signal
|
||||
(or debug-on-signal deferred:debug-on-signal)))
|
||||
(condition-case ,var
|
||||
,protected-form
|
||||
,@handlers)))
|
||||
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Back end functions of deferred tasks
|
||||
|
||||
(defvar deferred:tick-time 0.001
|
||||
"Waiting time between asynchronous tasks (second).
|
||||
The shorter waiting time increases the load of Emacs. The end
|
||||
user can tune this parameter. However, applications should not
|
||||
modify it because the applications run on various environments.")
|
||||
|
||||
(defvar deferred:queue nil
|
||||
"[internal] The execution queue of deferred objects.
|
||||
See the functions `deferred:post-task' and `deferred:worker'.")
|
||||
|
||||
(defmacro deferred:pack (a b c)
|
||||
`(cons ,a (cons ,b ,c)))
|
||||
|
||||
(defun deferred:schedule-worker ()
|
||||
"[internal] Schedule consuming a deferred task in the execution queue."
|
||||
(run-at-time deferred:tick-time nil 'deferred:worker))
|
||||
|
||||
(defun deferred:post-task (d which &optional arg)
|
||||
"[internal] Add a deferred object to the execution queue
|
||||
`deferred:queue' and schedule to execute.
|
||||
D is a deferred object. WHICH is a symbol, `ok' or `ng'. ARG is
|
||||
an argument value for execution of the deferred task."
|
||||
(push (deferred:pack d which arg) deferred:queue)
|
||||
(deferred:message "QUEUE-POST [%s]: %s"
|
||||
(length deferred:queue) (deferred:pack d which arg))
|
||||
(deferred:schedule-worker)
|
||||
d)
|
||||
|
||||
(defun deferred:clear-queue ()
|
||||
"Clear the execution queue. For test and debugging."
|
||||
(interactive)
|
||||
(deferred:message "QUEUE-CLEAR [%s -> 0]" (length deferred:queue))
|
||||
(setq deferred:queue nil))
|
||||
|
||||
(defun deferred:worker ()
|
||||
"[internal] Consume a deferred task.
|
||||
Mainly this function is called by timer asynchronously."
|
||||
(when deferred:queue
|
||||
(let* ((pack (car (last deferred:queue)))
|
||||
(d (car pack))
|
||||
(which (cadr pack))
|
||||
(arg (cddr pack)) value)
|
||||
(setq deferred:queue (nbutlast deferred:queue))
|
||||
(condition-case err
|
||||
(setq value (deferred:exec-task d which arg))
|
||||
(error
|
||||
(deferred:message "ERROR : %s" err)
|
||||
(message "deferred error : %s" err)))
|
||||
value)))
|
||||
|
||||
(defun deferred:flush-queue! ()
|
||||
"Call all deferred tasks synchronously. For test and debugging."
|
||||
(let (value)
|
||||
(while deferred:queue
|
||||
(setq value (deferred:worker)))
|
||||
value))
|
||||
|
||||
(defun deferred:sync! (d)
|
||||
"Wait for the given deferred task. For test and debugging.
|
||||
Error is raised if it is not processed within deferred chain D."
|
||||
(progn
|
||||
(let ((last-value 'deferred:undefined*)
|
||||
uncaught-error)
|
||||
(deferred:try
|
||||
(deferred:nextc d
|
||||
(lambda (x) (setq last-value x)))
|
||||
:catch
|
||||
(lambda (err) (setq uncaught-error err)))
|
||||
(while (and (eq 'deferred:undefined* last-value)
|
||||
(not uncaught-error))
|
||||
(sit-for 0.05)
|
||||
(sleep-for 0.05))
|
||||
(when uncaught-error
|
||||
(deferred:resignal uncaught-error))
|
||||
last-value)))
|
||||
|
||||
|
||||
|
||||
;; Struct: deferred
|
||||
;;
|
||||
;; callback : a callback function (default `deferred:default-callback')
|
||||
;; errorback : an errorback function (default `deferred:default-errorback')
|
||||
;; cancel : a canceling function (default `deferred:default-cancel')
|
||||
;; next : a next chained deferred object (default nil)
|
||||
;; status : if 'ok or 'ng, this deferred has a result (error) value. (default nil)
|
||||
;; value : saved value (default nil)
|
||||
;;
|
||||
(cl-defstruct deferred
|
||||
(callback 'deferred:default-callback)
|
||||
(errorback 'deferred:default-errorback)
|
||||
(cancel 'deferred:default-cancel)
|
||||
next status value)
|
||||
|
||||
(defun deferred:default-callback (i)
|
||||
"[internal] Default callback function."
|
||||
(identity i))
|
||||
|
||||
(defun deferred:default-errorback (err)
|
||||
"[internal] Default errorback function."
|
||||
(deferred:resignal err))
|
||||
|
||||
(defun deferred:resignal (err)
|
||||
"[internal] Safely resignal ERR as an Emacs condition.
|
||||
|
||||
If ERR is a cons (ERROR-SYMBOL . DATA) where ERROR-SYMBOL has an
|
||||
`error-conditions' property, it is re-signaled unchanged. If ERR
|
||||
is a string, it is signaled as a generic error using `error'.
|
||||
Otherwise, ERR is formatted into a string as if by `print' before
|
||||
raising with `error'."
|
||||
(cond ((and (listp err)
|
||||
(symbolp (car err))
|
||||
(get (car err) 'error-conditions))
|
||||
(signal (car err) (cdr err)))
|
||||
((stringp err)
|
||||
(error "%s" err))
|
||||
(t
|
||||
(error "%S" err))))
|
||||
|
||||
(defun deferred:default-cancel (d)
|
||||
"[internal] Default canceling function."
|
||||
(deferred:message "CANCEL : %s" d)
|
||||
(setf (deferred-callback d) 'deferred:default-callback)
|
||||
(setf (deferred-errorback d) 'deferred:default-errorback)
|
||||
(setf (deferred-next d) nil)
|
||||
d)
|
||||
|
||||
(defvar deferred:onerror nil
|
||||
"Default error handler. This value is nil or a function that
|
||||
have one argument for the error message.")
|
||||
|
||||
(defun deferred:exec-task (d which &optional arg)
|
||||
"[internal] Executing deferred task. If the deferred object has
|
||||
next deferred task or the return value is a deferred object, this
|
||||
function adds the task to the execution queue.
|
||||
D is a deferred object. WHICH is a symbol, `ok' or `ng'. ARG is
|
||||
an argument value for execution of the deferred task."
|
||||
(deferred:message "EXEC : %s / %s / %s" d which arg)
|
||||
(when (null d) (error "deferred:exec-task was given a nil."))
|
||||
(let ((callback (if (eq which 'ok)
|
||||
(deferred-callback d)
|
||||
(deferred-errorback d)))
|
||||
(next-deferred (deferred-next d)))
|
||||
(cond
|
||||
(callback
|
||||
(deferred:condition-case err
|
||||
(let ((value (deferred:call-lambda callback arg)))
|
||||
(cond
|
||||
((deferred-p value)
|
||||
(deferred:message "WAIT NEST : %s" value)
|
||||
(if next-deferred
|
||||
(deferred:set-next value next-deferred)
|
||||
value))
|
||||
(t
|
||||
(if next-deferred
|
||||
(deferred:post-task next-deferred 'ok value)
|
||||
(setf (deferred-status d) 'ok)
|
||||
(setf (deferred-value d) value)
|
||||
value))))
|
||||
(error
|
||||
(cond
|
||||
(next-deferred
|
||||
(deferred:post-task next-deferred 'ng err))
|
||||
(deferred:onerror
|
||||
(deferred:call-lambda deferred:onerror err))
|
||||
(t
|
||||
(deferred:message "ERROR : %S" err)
|
||||
(message "deferred error : %S" err)
|
||||
(setf (deferred-status d) 'ng)
|
||||
(setf (deferred-value d) err)
|
||||
err)))))
|
||||
(t ; <= (null callback)
|
||||
(cond
|
||||
(next-deferred
|
||||
(deferred:exec-task next-deferred which arg))
|
||||
((eq which 'ok) arg)
|
||||
(t ; (eq which 'ng)
|
||||
(deferred:resignal arg)))))))
|
||||
|
||||
(defun deferred:set-next (prev next)
|
||||
"[internal] Connect deferred objects."
|
||||
(setf (deferred-next prev) next)
|
||||
(cond
|
||||
((eq 'ok (deferred-status prev))
|
||||
(setf (deferred-status prev) nil)
|
||||
(let ((ret (deferred:exec-task
|
||||
next 'ok (deferred-value prev))))
|
||||
(if (deferred-p ret) ret
|
||||
next)))
|
||||
((eq 'ng (deferred-status prev))
|
||||
(setf (deferred-status prev) nil)
|
||||
(let ((ret (deferred:exec-task next 'ng (deferred-value prev))))
|
||||
(if (deferred-p ret) ret
|
||||
next)))
|
||||
(t
|
||||
next)))
|
||||
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Basic functions for deferred objects
|
||||
|
||||
(defun deferred:new (&optional callback)
|
||||
"Create a deferred object."
|
||||
(if callback
|
||||
(make-deferred :callback callback)
|
||||
(make-deferred)))
|
||||
|
||||
(defun deferred:callback (d &optional arg)
|
||||
"Start deferred chain with a callback message."
|
||||
(deferred:exec-task d 'ok arg))
|
||||
|
||||
(defun deferred:errorback (d &optional arg)
|
||||
"Start deferred chain with an errorback message."
|
||||
(deferred:exec-task d 'ng arg))
|
||||
|
||||
(defun deferred:callback-post (d &optional arg)
|
||||
"Add the deferred object to the execution queue."
|
||||
(deferred:post-task d 'ok arg))
|
||||
|
||||
(defun deferred:errorback-post (d &optional arg)
|
||||
"Add the deferred object to the execution queue."
|
||||
(deferred:post-task d 'ng arg))
|
||||
|
||||
(defun deferred:cancel (d)
|
||||
"Cancel all callbacks and deferred chain in the deferred object."
|
||||
(deferred:message "CANCEL : %s" d)
|
||||
(funcall (deferred-cancel d) d)
|
||||
d)
|
||||
|
||||
(defun deferred:status (d)
|
||||
"Return a current status of the deferred object. The returned value means following:
|
||||
`ok': the callback was called and waiting for next deferred.
|
||||
`ng': the errorback was called and waiting for next deferred.
|
||||
nil: The neither callback nor errorback was not called."
|
||||
(deferred-status d))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Basic utility functions
|
||||
|
||||
(defun deferred:succeed (&optional arg)
|
||||
"Create a synchronous deferred object."
|
||||
(let ((d (deferred:new)))
|
||||
(deferred:exec-task d 'ok arg)
|
||||
d))
|
||||
|
||||
(defun deferred:fail (&optional arg)
|
||||
"Create a synchronous deferred object."
|
||||
(let ((d (deferred:new)))
|
||||
(deferred:exec-task d 'ng arg)
|
||||
d))
|
||||
|
||||
(defun deferred:next (&optional callback arg)
|
||||
"Create a deferred object and schedule executing. This function
|
||||
is a short cut of following code:
|
||||
(deferred:callback-post (deferred:new callback))."
|
||||
(let ((d (if callback
|
||||
(make-deferred :callback callback)
|
||||
(make-deferred))))
|
||||
(deferred:callback-post d arg)
|
||||
d))
|
||||
|
||||
(defun deferred:nextc (d callback)
|
||||
"Create a deferred object with OK callback and connect it to the given deferred object."
|
||||
(let ((nd (make-deferred :callback callback)))
|
||||
(deferred:set-next d nd)))
|
||||
|
||||
(defun deferred:error (d callback)
|
||||
"Create a deferred object with errorback and connect it to the given deferred object."
|
||||
(let ((nd (make-deferred :errorback callback)))
|
||||
(deferred:set-next d nd)))
|
||||
|
||||
(defun deferred:watch (d callback)
|
||||
"Create a deferred object with watch task and connect it to the given deferred object.
|
||||
The watch task CALLBACK can not affect deferred chains with
|
||||
return values. This function is used in following purposes,
|
||||
simulation of try-finally block in asynchronous tasks, progress
|
||||
monitoring of tasks."
|
||||
(let* ((callback callback)
|
||||
(normal (lambda (x) (ignore-errors (deferred:call-lambda callback x)) x))
|
||||
(err (lambda (e)
|
||||
(ignore-errors (deferred:call-lambda callback e))
|
||||
(deferred:resignal e))))
|
||||
(let ((nd (make-deferred :callback normal :errorback err)))
|
||||
(deferred:set-next d nd))))
|
||||
|
||||
(defun deferred:wait (msec)
|
||||
"Return a deferred object scheduled at MSEC millisecond later."
|
||||
(let ((d (deferred:new)) (start-time (float-time)) timer)
|
||||
(deferred:message "WAIT : %s" msec)
|
||||
(setq timer (deferred:setTimeout
|
||||
(lambda ()
|
||||
(deferred:exec-task d 'ok
|
||||
(* 1000.0 (- (float-time) start-time)))
|
||||
nil) msec))
|
||||
(setf (deferred-cancel d)
|
||||
(lambda (x)
|
||||
(deferred:cancelTimeout timer)
|
||||
(deferred:default-cancel x)))
|
||||
d))
|
||||
|
||||
(defun deferred:wait-idle (msec)
|
||||
"Return a deferred object which will run when Emacs has been
|
||||
idle for MSEC millisecond."
|
||||
(let ((d (deferred:new)) (start-time (float-time)) timer)
|
||||
(deferred:message "WAIT-IDLE : %s" msec)
|
||||
(setq timer
|
||||
(deferred:run-with-idle-timer
|
||||
(/ msec 1000.0)
|
||||
(lambda ()
|
||||
(deferred:exec-task d 'ok
|
||||
(* 1000.0 (- (float-time) start-time)))
|
||||
nil)))
|
||||
(setf (deferred-cancel d)
|
||||
(lambda (x)
|
||||
(deferred:cancelTimeout timer)
|
||||
(deferred:default-cancel x)))
|
||||
d))
|
||||
|
||||
(defun deferred:call (f &rest args)
|
||||
"Call the given function asynchronously."
|
||||
(deferred:next
|
||||
(lambda (_x)
|
||||
(apply f args))))
|
||||
|
||||
(defun deferred:apply (f &optional args)
|
||||
"Call the given function asynchronously."
|
||||
(deferred:next
|
||||
(lambda (_x)
|
||||
(apply f args))))
|
||||
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Utility functions
|
||||
|
||||
(defun deferred:empty-p (times-or-seq)
|
||||
"[internal] Return non-nil if TIMES-OR-SEQ is the number zero or nil."
|
||||
(or (and (numberp times-or-seq) (<= times-or-seq 0))
|
||||
(and (sequencep times-or-seq) (= (length times-or-seq) 0))))
|
||||
|
||||
(defun deferred:loop (times-or-seq func)
|
||||
"Return a iteration deferred object."
|
||||
(deferred:message "LOOP : %s" times-or-seq)
|
||||
(if (deferred:empty-p times-or-seq) (deferred:next)
|
||||
(let* (items (rd
|
||||
(cond
|
||||
((numberp times-or-seq)
|
||||
(cl-loop for i from 0 below times-or-seq
|
||||
with ld = (deferred:next)
|
||||
do
|
||||
(push ld items)
|
||||
(setq ld
|
||||
(let ((i i))
|
||||
(deferred:nextc ld
|
||||
(lambda (_x) (deferred:call-lambda func i)))))
|
||||
finally return ld))
|
||||
((sequencep times-or-seq)
|
||||
(cl-loop for i in (append times-or-seq nil) ; seq->list
|
||||
with ld = (deferred:next)
|
||||
do
|
||||
(push ld items)
|
||||
(setq ld
|
||||
(let ((i i))
|
||||
(deferred:nextc ld
|
||||
(lambda (_x) (deferred:call-lambda func i)))))
|
||||
finally return ld)))))
|
||||
(setf (deferred-cancel rd)
|
||||
(lambda (x) (deferred:default-cancel x)
|
||||
(cl-loop for i in items
|
||||
do (deferred:cancel i))))
|
||||
rd)))
|
||||
|
||||
(defun deferred:trans-multi-args (args self-func list-func main-func)
|
||||
"[internal] Check the argument values and dispatch to methods."
|
||||
(cond
|
||||
((and (= 1 (length args)) (consp (car args)) (not (functionp (car args))))
|
||||
(let ((lst (car args)))
|
||||
(cond
|
||||
((or (null lst) (null (car lst)))
|
||||
(deferred:next))
|
||||
((deferred:aand lst (car it) (or (functionp it) (deferred-p it)))
|
||||
;; a list of deferred objects
|
||||
(funcall list-func lst))
|
||||
((deferred:aand lst (consp it))
|
||||
;; an alist of deferred objects
|
||||
(funcall main-func lst))
|
||||
(t (error "Wrong argument type. %s" args)))))
|
||||
(t (funcall self-func args))))
|
||||
|
||||
(defun deferred:parallel-array-to-alist (lst)
|
||||
"[internal] Translation array to alist."
|
||||
(cl-loop for d in lst
|
||||
for i from 0 below (length lst)
|
||||
collect (cons i d)))
|
||||
|
||||
(defun deferred:parallel-alist-to-array (alst)
|
||||
"[internal] Translation alist to array."
|
||||
(cl-loop for pair in
|
||||
(sort alst (lambda (x y)
|
||||
(< (car x) (car y))))
|
||||
collect (cdr pair)))
|
||||
|
||||
(defun deferred:parallel-func-to-deferred (alst)
|
||||
"[internal] Normalization for parallel and earlier arguments."
|
||||
(cl-loop for pair in alst
|
||||
for d = (cdr pair)
|
||||
collect
|
||||
(progn
|
||||
(unless (deferred-p d)
|
||||
(setf (cdr pair) (deferred:next d)))
|
||||
pair)))
|
||||
|
||||
(defun deferred:parallel-main (alst)
|
||||
"[internal] Deferred alist implementation for `deferred:parallel'. "
|
||||
(deferred:message "PARALLEL<KEY . VALUE>" )
|
||||
(let ((nd (deferred:new))
|
||||
(len (length alst))
|
||||
values)
|
||||
(cl-loop for pair in
|
||||
(deferred:parallel-func-to-deferred alst)
|
||||
with cd ; current child deferred
|
||||
do
|
||||
(let ((name (car pair)))
|
||||
(setq cd
|
||||
(deferred:nextc (cdr pair)
|
||||
(lambda (x)
|
||||
(push (cons name x) values)
|
||||
(deferred:message "PARALLEL VALUE [%s/%s] %s"
|
||||
(length values) len (cons name x))
|
||||
(when (= len (length values))
|
||||
(deferred:message "PARALLEL COLLECTED")
|
||||
(deferred:post-task nd 'ok (nreverse values)))
|
||||
nil)))
|
||||
(deferred:error cd
|
||||
(lambda (e)
|
||||
(push (cons name e) values)
|
||||
(deferred:message "PARALLEL ERROR [%s/%s] %s"
|
||||
(length values) len (cons name e))
|
||||
(when (= (length values) len)
|
||||
(deferred:message "PARALLEL COLLECTED")
|
||||
(deferred:post-task nd 'ok (nreverse values)))
|
||||
nil))))
|
||||
nd))
|
||||
|
||||
(defun deferred:parallel-list (lst)
|
||||
"[internal] Deferred list implementation for `deferred:parallel'. "
|
||||
(deferred:message "PARALLEL<LIST>" )
|
||||
(let* ((pd (deferred:parallel-main (deferred:parallel-array-to-alist lst)))
|
||||
(rd (deferred:nextc pd 'deferred:parallel-alist-to-array)))
|
||||
(setf (deferred-cancel rd)
|
||||
(lambda (x) (deferred:default-cancel x)
|
||||
(deferred:cancel pd)))
|
||||
rd))
|
||||
|
||||
(defun deferred:parallel (&rest args)
|
||||
"Return a deferred object that calls given deferred objects or
|
||||
functions in parallel and wait for all callbacks. The following
|
||||
deferred task will be called with an array of the return
|
||||
values. ARGS can be a list or an alist of deferred objects or
|
||||
functions."
|
||||
(deferred:message "PARALLEL : %s" args)
|
||||
(deferred:trans-multi-args args
|
||||
'deferred:parallel 'deferred:parallel-list 'deferred:parallel-main))
|
||||
|
||||
(defun deferred:earlier-main (alst)
|
||||
"[internal] Deferred alist implementation for `deferred:earlier'. "
|
||||
(deferred:message "EARLIER<KEY . VALUE>" )
|
||||
(let ((nd (deferred:new))
|
||||
(len (length alst))
|
||||
value results)
|
||||
(cl-loop for pair in
|
||||
(deferred:parallel-func-to-deferred alst)
|
||||
with cd ; current child deferred
|
||||
do
|
||||
(let ((name (car pair)))
|
||||
(setq cd
|
||||
(deferred:nextc (cdr pair)
|
||||
(lambda (x)
|
||||
(push (cons name x) results)
|
||||
(cond
|
||||
((null value)
|
||||
(setq value (cons name x))
|
||||
(deferred:message "EARLIER VALUE %s" (cons name value))
|
||||
(deferred:post-task nd 'ok value))
|
||||
(t
|
||||
(deferred:message "EARLIER MISS [%s/%s] %s" (length results) len (cons name value))
|
||||
(when (eql (length results) len)
|
||||
(deferred:message "EARLIER COLLECTED"))))
|
||||
nil)))
|
||||
(deferred:error cd
|
||||
(lambda (e)
|
||||
(push (cons name e) results)
|
||||
(deferred:message "EARLIER ERROR [%s/%s] %s" (length results) len (cons name e))
|
||||
(when (and (eql (length results) len) (null value))
|
||||
(deferred:message "EARLIER FAILED")
|
||||
(deferred:post-task nd 'ok nil))
|
||||
nil))))
|
||||
nd))
|
||||
|
||||
(defun deferred:earlier-list (lst)
|
||||
"[internal] Deferred list implementation for `deferred:earlier'. "
|
||||
(deferred:message "EARLIER<LIST>" )
|
||||
(let* ((pd (deferred:earlier-main (deferred:parallel-array-to-alist lst)))
|
||||
(rd (deferred:nextc pd (lambda (x) (cdr x)))))
|
||||
(setf (deferred-cancel rd)
|
||||
(lambda (x) (deferred:default-cancel x)
|
||||
(deferred:cancel pd)))
|
||||
rd))
|
||||
|
||||
|
||||
(defun deferred:earlier (&rest args)
|
||||
"Return a deferred object that calls given deferred objects or
|
||||
functions in parallel and wait for the first callback. The
|
||||
following deferred task will be called with the first return
|
||||
value. ARGS can be a list or an alist of deferred objects or
|
||||
functions."
|
||||
(deferred:message "EARLIER : %s" args)
|
||||
(deferred:trans-multi-args args
|
||||
'deferred:earlier 'deferred:earlier-list 'deferred:earlier-main))
|
||||
|
||||
(defmacro deferred:timeout (timeout-msec timeout-form d)
|
||||
"Time out macro on a deferred task D. If the deferred task D
|
||||
does not complete within TIMEOUT-MSEC, this macro cancels the
|
||||
deferred task and return the TIMEOUT-FORM."
|
||||
`(deferred:earlier
|
||||
(deferred:nextc (deferred:wait ,timeout-msec)
|
||||
(lambda (x) ,timeout-form))
|
||||
,d))
|
||||
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Application functions
|
||||
|
||||
(defvar deferred:uid 0 "[internal] Sequence number for some utilities. See the function `deferred:uid'.")
|
||||
|
||||
(defun deferred:uid ()
|
||||
"[internal] Generate a sequence number."
|
||||
(cl-incf deferred:uid))
|
||||
|
||||
(defun deferred:buffer-string (strformat buf)
|
||||
"[internal] Return a string in the buffer with the given format."
|
||||
(format strformat
|
||||
(with-current-buffer buf (buffer-string))))
|
||||
|
||||
(defun deferred:process (command &rest args)
|
||||
"A deferred wrapper of `start-process'. Return a deferred
|
||||
object. The process name and buffer name of the argument of the
|
||||
`start-process' are generated by this function automatically.
|
||||
The next deferred object receives stdout and stderr string from
|
||||
the command process."
|
||||
(deferred:process-gen 'start-process command args))
|
||||
|
||||
(defun deferred:process-shell (command &rest args)
|
||||
"A deferred wrapper of `start-process-shell-command'. Return a deferred
|
||||
object. The process name and buffer name of the argument of the
|
||||
`start-process-shell-command' are generated by this function automatically.
|
||||
The next deferred object receives stdout and stderr string from
|
||||
the command process."
|
||||
(deferred:process-gen 'start-process-shell-command command args))
|
||||
|
||||
(defun deferred:process-buffer (command &rest args)
|
||||
"A deferred wrapper of `start-process'. Return a deferred
|
||||
object. The process name and buffer name of the argument of the
|
||||
`start-process' are generated by this function automatically.
|
||||
The next deferred object receives stdout and stderr buffer from
|
||||
the command process."
|
||||
(deferred:process-buffer-gen 'start-process command args))
|
||||
|
||||
(defun deferred:process-shell-buffer (command &rest args)
|
||||
"A deferred wrapper of `start-process-shell-command'. Return a deferred
|
||||
object. The process name and buffer name of the argument of the
|
||||
`start-process-shell-command' are generated by this function automatically.
|
||||
The next deferred object receives stdout and stderr buffer from
|
||||
the command process."
|
||||
(deferred:process-buffer-gen 'start-process-shell-command command args))
|
||||
|
||||
(defun deferred:process-gen (f command args)
|
||||
"[internal]"
|
||||
(let ((pd (deferred:process-buffer-gen f command args)) d)
|
||||
(setq d (deferred:nextc pd
|
||||
(lambda (buf)
|
||||
(prog1
|
||||
(with-current-buffer buf (buffer-string))
|
||||
(kill-buffer buf)))))
|
||||
(setf (deferred-cancel d)
|
||||
(lambda (_x)
|
||||
(deferred:default-cancel d)
|
||||
(deferred:default-cancel pd)))
|
||||
d))
|
||||
|
||||
(defun deferred:process-buffer-gen (f command args)
|
||||
"[internal]"
|
||||
(let ((d (deferred:next)) (uid (deferred:uid)))
|
||||
(let ((proc-name (format "*deferred:*%s*:%s" command uid))
|
||||
(buf-name (format " *deferred:*%s*:%s" command uid))
|
||||
(pwd default-directory)
|
||||
(env process-environment)
|
||||
(con-type process-connection-type)
|
||||
(nd (deferred:new)) proc-buf proc)
|
||||
(deferred:nextc d
|
||||
(lambda (_x)
|
||||
(setq proc-buf (get-buffer-create buf-name))
|
||||
(condition-case err
|
||||
(let ((default-directory pwd)
|
||||
(process-environment env)
|
||||
(process-connection-type con-type))
|
||||
(setq proc
|
||||
(if (null (car args))
|
||||
(apply f proc-name buf-name command nil)
|
||||
(apply f proc-name buf-name command args)))
|
||||
(set-process-sentinel
|
||||
proc
|
||||
(lambda (proc event)
|
||||
(unless (process-live-p proc)
|
||||
(if (zerop (process-exit-status proc))
|
||||
(deferred:post-task nd 'ok proc-buf)
|
||||
(let ((msg (format "Deferred process exited abnormally:\n command: %s\n exit status: %s %s\n event: %s\n buffer contents: %S"
|
||||
command
|
||||
(process-status proc)
|
||||
(process-exit-status proc)
|
||||
(string-trim-right event)
|
||||
(if (buffer-live-p proc-buf)
|
||||
(with-current-buffer proc-buf
|
||||
(buffer-string))
|
||||
"(unavailable)"))))
|
||||
(kill-buffer proc-buf)
|
||||
(deferred:post-task nd 'ng msg))))))
|
||||
(setf (deferred-cancel nd)
|
||||
(lambda (x) (deferred:default-cancel x)
|
||||
(when proc
|
||||
(kill-process proc)
|
||||
(kill-buffer proc-buf)))))
|
||||
(error (deferred:post-task nd 'ng err)))
|
||||
nil))
|
||||
nd)))
|
||||
|
||||
(defmacro deferred:processc (d command &rest args)
|
||||
"Process chain of `deferred:process'."
|
||||
`(deferred:nextc ,d
|
||||
(lambda (,(cl-gensym)) (deferred:process ,command ,@args))))
|
||||
|
||||
(defmacro deferred:process-bufferc (d command &rest args)
|
||||
"Process chain of `deferred:process-buffer'."
|
||||
`(deferred:nextc ,d
|
||||
(lambda (,(cl-gensym)) (deferred:process-buffer ,command ,@args))))
|
||||
|
||||
(defmacro deferred:process-shellc (d command &rest args)
|
||||
"Process chain of `deferred:process'."
|
||||
`(deferred:nextc ,d
|
||||
(lambda (,(cl-gensym)) (deferred:process-shell ,command ,@args))))
|
||||
|
||||
(defmacro deferred:process-shell-bufferc (d command &rest args)
|
||||
"Process chain of `deferred:process-buffer'."
|
||||
`(deferred:nextc ,d
|
||||
(lambda (,(cl-gensym)) (deferred:process-shell-buffer ,command ,@args))))
|
||||
|
||||
;; Special variables defined in url-vars.el.
|
||||
(defvar url-request-data)
|
||||
(defvar url-request-method)
|
||||
(defvar url-request-extra-headers)
|
||||
|
||||
(declare-function url-http-symbol-value-in-buffer "url-http"
|
||||
(symbol buffer &optional unbound-value))
|
||||
|
||||
(declare-function deferred:url-param-serialize "request" (params))
|
||||
|
||||
(declare-function deferred:url-escape "request" (val))
|
||||
|
||||
(eval-after-load "url"
|
||||
;; for url package
|
||||
;; TODO: proxy, charaset
|
||||
;; List of gloabl variables to preserve and restore before url-retrieve call
|
||||
'(let ((url-global-variables '(url-request-data
|
||||
url-request-method
|
||||
url-request-extra-headers)))
|
||||
|
||||
(defun deferred:url-retrieve (url &optional cbargs silent inhibit-cookies)
|
||||
"A wrapper function for url-retrieve. The next deferred
|
||||
object receives the buffer object that URL will load
|
||||
into. Values of dynamically bound 'url-request-data', 'url-request-method' and
|
||||
'url-request-extra-headers' are passed to url-retrieve call."
|
||||
(let ((nd (deferred:new))
|
||||
buf
|
||||
(local-values (mapcar (lambda (symbol) (symbol-value symbol)) url-global-variables)))
|
||||
(deferred:next
|
||||
(lambda (_x)
|
||||
(cl-progv url-global-variables local-values
|
||||
(condition-case err
|
||||
(setq buf
|
||||
(url-retrieve
|
||||
url (lambda (_xx) (deferred:post-task nd 'ok buf))
|
||||
cbargs silent inhibit-cookies))
|
||||
(error (deferred:post-task nd 'ng err)))
|
||||
nil)))
|
||||
(setf (deferred-cancel nd)
|
||||
(lambda (_x)
|
||||
(when (buffer-live-p buf)
|
||||
(kill-buffer buf))))
|
||||
nd))
|
||||
|
||||
(defun deferred:url-delete-header (buf)
|
||||
(with-current-buffer buf
|
||||
(let ((pos (url-http-symbol-value-in-buffer
|
||||
'url-http-end-of-headers buf)))
|
||||
(when pos
|
||||
(delete-region (point-min) (1+ pos)))))
|
||||
buf)
|
||||
|
||||
(defun deferred:url-delete-buffer (buf)
|
||||
(when (and buf (buffer-live-p buf))
|
||||
(kill-buffer buf))
|
||||
nil)
|
||||
|
||||
(defun deferred:url-get (url &optional params &rest args)
|
||||
"Perform a HTTP GET method with `url-retrieve'. PARAMS is
|
||||
a parameter list of (key . value) or key. ARGS will be appended
|
||||
to deferred:url-retrieve args list. The next deferred
|
||||
object receives the buffer object that URL will load into."
|
||||
(when params
|
||||
(setq url
|
||||
(concat url "?" (deferred:url-param-serialize params))))
|
||||
(let ((d (deferred:$
|
||||
(apply 'deferred:url-retrieve url args)
|
||||
(deferred:nextc it 'deferred:url-delete-header))))
|
||||
(deferred:set-next
|
||||
d (deferred:new 'deferred:url-delete-buffer))
|
||||
d))
|
||||
|
||||
(defun deferred:url-post (url &optional params &rest args)
|
||||
"Perform a HTTP POST method with `url-retrieve'. PARAMS is
|
||||
a parameter list of (key . value) or key. ARGS will be appended
|
||||
to deferred:url-retrieve args list. The next deferred
|
||||
object receives the buffer object that URL will load into."
|
||||
(let ((url-request-method "POST")
|
||||
(url-request-extra-headers
|
||||
(append url-request-extra-headers
|
||||
'(("Content-Type" . "application/x-www-form-urlencoded"))))
|
||||
(url-request-data (deferred:url-param-serialize params)))
|
||||
(let ((d (deferred:$
|
||||
(apply 'deferred:url-retrieve url args)
|
||||
(deferred:nextc it 'deferred:url-delete-header))))
|
||||
(deferred:set-next
|
||||
d (deferred:new 'deferred:url-delete-buffer))
|
||||
d)))
|
||||
|
||||
(defun deferred:url-escape (val)
|
||||
"[internal] Return a new string that is VAL URI-encoded."
|
||||
(unless (stringp val)
|
||||
(setq val (format "%s" val)))
|
||||
(url-hexify-string
|
||||
(encode-coding-string val 'utf-8)))
|
||||
|
||||
(defun deferred:url-param-serialize (params)
|
||||
"[internal] Serialize a list of (key . value) cons cells
|
||||
into a query string."
|
||||
(when params
|
||||
(mapconcat
|
||||
'identity
|
||||
(cl-loop for p in params
|
||||
collect
|
||||
(cond
|
||||
((consp p)
|
||||
(concat
|
||||
(deferred:url-escape (car p)) "="
|
||||
(deferred:url-escape (cdr p))))
|
||||
(t
|
||||
(deferred:url-escape p))))
|
||||
"&")))
|
||||
))
|
||||
|
||||
|
||||
(provide 'deferred)
|
||||
;;; deferred.el ends here
|
||||
1109
lisp/ein/ein-cell.el
Normal file
1109
lisp/ein/ein-cell.el
Normal file
File diff suppressed because it is too large
Load Diff
331
lisp/ein/ein-classes.el
Normal file
331
lisp/ein/ein-classes.el
Normal file
@@ -0,0 +1,331 @@
|
||||
;;; ein-classes.el --- Classes and structures. -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2017 John M. Miller
|
||||
|
||||
;; Author: John M Miller <millejoh at mac dot com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-classes.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-classes.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-worksheet.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;; Content
|
||||
(require 'eieio)
|
||||
|
||||
(cl-defstruct ein:$content
|
||||
"Content returned from the Jupyter notebook server:
|
||||
`ein:$content-url-or-port'
|
||||
URL or port of Jupyter server.
|
||||
|
||||
`ein:$content-name'
|
||||
The name/filename of the content. Always equivalent to the last
|
||||
part of the path field
|
||||
|
||||
`ein:$content-path'
|
||||
The full file path. It will not start with /, and it will be /-delimited.
|
||||
|
||||
`ein:$content-type'
|
||||
One of three values: :directory, :file, :notebook.
|
||||
|
||||
`ein:$content-writable'
|
||||
Indicates if requester has permission to modified the requested content.
|
||||
|
||||
`ein:$content-created'
|
||||
|
||||
`ein:$content-last-modified'
|
||||
|
||||
`ein:$content-mimetype'
|
||||
Specify the mime-type of :file content, null otherwise.
|
||||
|
||||
`ein:$content-raw-content'
|
||||
Contents of resource as returned by Jupyter. Depending on content-type will hold:
|
||||
:directory : JSON list of models for each item in the directory.
|
||||
:file : Text of file as a string or base64 encoded string if mimetype
|
||||
is other than 'text/plain'.
|
||||
:notebook : JSON structure of the file.
|
||||
|
||||
`ein:$content-format'
|
||||
Value will depend on content-type:
|
||||
:directory : :json.
|
||||
:file : Either :text or :base64
|
||||
:notebook : :json.
|
||||
|
||||
"
|
||||
url-or-port
|
||||
notebook-api-version
|
||||
name
|
||||
path
|
||||
type
|
||||
writable
|
||||
created
|
||||
last-modified
|
||||
mimetype
|
||||
raw-content
|
||||
format
|
||||
session-p)
|
||||
;;; Websockets
|
||||
|
||||
(cl-defstruct ein:$websocket
|
||||
"A wrapper object of `websocket'.
|
||||
|
||||
`ein:$websocket-ws' : an instance returned by `websocket-open'
|
||||
`ein:$websocket-kernel' : kernel at the time of instantiation
|
||||
`ein:$websocket-closed-by-client' : t/nil'
|
||||
"
|
||||
ws
|
||||
kernel
|
||||
closed-by-client)
|
||||
|
||||
;;; Notebook
|
||||
(cl-defstruct ein:$notebook
|
||||
"Hold notebook variables.
|
||||
|
||||
`ein:$notebook-url-or-port'
|
||||
URL or port of IPython server.
|
||||
|
||||
`ein:$notebook-notebook-id' : string
|
||||
uuid string (as of ipython 2.0 this is the same is notebook-name).
|
||||
|
||||
`ein:$notebook-notebook-path' : string
|
||||
Path to notebook.
|
||||
|
||||
`ein:$notebook-kernel' : `ein:$kernel'
|
||||
`ein:$kernel' instance.
|
||||
|
||||
`ein:$notebook-kernelspec' : `ein:$kernelspec'
|
||||
Jupyter kernel specification for the notebook.
|
||||
|
||||
`ein:$notebook-kernelinfo' : `ein:kernelinfo'
|
||||
`ein:kernelinfo' instance.
|
||||
|
||||
`ein:$notebook-pager'
|
||||
Variable for `ein:pager-*' functions. See ein-pager.el.
|
||||
|
||||
`ein:$notebook-dirty' : boolean
|
||||
Set to `t' if notebook has unsaved changes. Otherwise `nil'.
|
||||
|
||||
`ein:$notebook-metadata' : plist
|
||||
Notebook meta data (e.g., notebook name).
|
||||
|
||||
`ein:$notebook-name' : string
|
||||
Notebook name.
|
||||
|
||||
`ein:$notebook-nbformat' : integer
|
||||
Notebook file format version.
|
||||
|
||||
`ein:$notebook-nbformat-minor' : integer
|
||||
Notebook file format version.
|
||||
|
||||
`ein:$notebook-events' : `ein:$events'
|
||||
Event handler instance.
|
||||
|
||||
`ein:$notebook-worksheets' : list of `ein:worksheet'
|
||||
List of worksheets.
|
||||
|
||||
`ein:$notebook-scratchsheets' : list of `ein:worksheet'
|
||||
List of scratch worksheets.
|
||||
|
||||
`ein:$notebook-api-version' : integer
|
||||
Major version of the IPython notebook server we are talking to.
|
||||
"
|
||||
url-or-port
|
||||
notebook-id ;; In IPython-2.0 this is "[:path]/[:name].ipynb"
|
||||
notebook-path
|
||||
kernel
|
||||
kernelinfo
|
||||
kernelspec
|
||||
pager
|
||||
dirty
|
||||
metadata
|
||||
notebook-name
|
||||
nbformat
|
||||
nbformat-minor
|
||||
events
|
||||
worksheets
|
||||
scratchsheets
|
||||
api-version)
|
||||
|
||||
|
||||
;;; Worksheet
|
||||
(defclass ein:worksheet ()
|
||||
((nbformat :initarg :nbformat :type integer)
|
||||
(notebook-path :initarg :notebook-path :type function
|
||||
:accessor ein:worksheet--notebook-path)
|
||||
(saved-cells :initarg :saved-cells :initform nil
|
||||
:accessor ein:worksheet--saved-cells
|
||||
:documentation
|
||||
"Slot to cache cells for worksheet without buffer")
|
||||
(dont-save-cells :initarg :dont-save-cells :initform nil :type boolean
|
||||
:accessor ein:worksheet--dont-save-cells-p
|
||||
:documentation "Don't cache cells when this flag is on.")
|
||||
(ewoc :initarg :ewoc :type ewoc :accessor ein:worksheet--ewoc)
|
||||
(kernel :initarg :kernel :type ein:$kernel :accessor ein:worksheet--kernel)
|
||||
(dirty :initarg :dirty :type boolean :initform nil :accessor ein:worksheet--dirty-p)
|
||||
(metadata :initarg :metadata :initform nil :accessor ein:worksheet--metadata)
|
||||
(events :initarg :events :accessor ein:worksheet--events)))
|
||||
|
||||
;;; Kernel
|
||||
(cl-defstruct ein:$kernelspec
|
||||
"Kernel specification as return by the Jupyter notebook server.
|
||||
|
||||
`ein:$kernelspec-name' : string
|
||||
Name used to identify the kernel (like python2, or python3).
|
||||
|
||||
`ein:$kernelspec-display-name' : string
|
||||
Name used to display kernel to user.
|
||||
|
||||
`ein:$kernelspec-language' : string
|
||||
Programming language supported by kernel, like 'python'.
|
||||
|
||||
`ein:$kernelspec-resources' : plist
|
||||
Resources, if any, used by the kernel.
|
||||
|
||||
`ein:$kernelspec-spec' : plist
|
||||
How the outside world defines kernelspec:
|
||||
https://ipython.org/ipython-doc/dev/development/kernels.html#kernelspecs
|
||||
"
|
||||
name
|
||||
display-name
|
||||
resources
|
||||
spec
|
||||
language)
|
||||
|
||||
(cl-defstruct ein:$kernel
|
||||
"Should be named ein:$session. We glom session and kernel as
|
||||
defined by the server as just ein:$kernel in the client."
|
||||
url-or-port
|
||||
path
|
||||
kernelspec
|
||||
events
|
||||
api-version
|
||||
session-id
|
||||
kernel-id
|
||||
shell-channel
|
||||
iopub-channel
|
||||
websocket ; For IPython 3.x+
|
||||
base-url ; /api/kernels/
|
||||
kernel-url ; /api/kernels/<KERNEL-ID>
|
||||
ws-url ; ws://<URL>[:<PORT>]
|
||||
username
|
||||
msg-callbacks
|
||||
oinfo-cache
|
||||
after-start-hook
|
||||
after-execute-hook)
|
||||
|
||||
;;; Cells
|
||||
|
||||
(defclass ein:basecell ()
|
||||
((cell-type :initarg :cell-type :type string :accessor ein:cell-type)
|
||||
(read-only :initarg :read-only :initform nil :type boolean)
|
||||
(ewoc :initarg :ewoc :type ewoc :accessor ein:basecell--ewoc)
|
||||
(element :initarg :element :initform nil :type list
|
||||
:documentation "ewoc nodes")
|
||||
(element-names :initarg :element-names)
|
||||
(input :initarg :input :type string
|
||||
:documentation "Place to hold data until it is rendered via `ewoc'.")
|
||||
(outputs :initarg :outputs :initform nil :type list)
|
||||
(metadata :initarg :metadata :initform nil :type list :accessor ein:cell-metadata)
|
||||
(events :initarg :events :type ein:events)
|
||||
(cell-id :initarg :cell-id :initform (ein:utils-uuid) :type string
|
||||
:accessor ein:cell-id))
|
||||
"Notebook cell base class")
|
||||
|
||||
(defclass ein:codecell (ein:basecell)
|
||||
((traceback :initform nil :initarg :traceback :type list)
|
||||
(cell-type :initarg :cell-type :initform "code")
|
||||
(kernel :initarg :kernel :type ein:$kernel :accessor ein:cell-kernel)
|
||||
(element-names :initform '(:prompt :input :output :footer))
|
||||
(input-prompt-number :initarg :input-prompt-number
|
||||
:documentation "\
|
||||
Integer or \"*\" (running state).
|
||||
Implementation note:
|
||||
Typed `:input-prompt-number' becomes a problem when reading a
|
||||
notebook that saved "*". So don't add `:type'!")
|
||||
(collapsed :initarg :collapsed :initform nil :type boolean)
|
||||
(running :initarg :running :initform nil :type boolean)))
|
||||
|
||||
(defclass ein:textcell (ein:basecell)
|
||||
((cell-type :initarg :cell-type :initform "text")
|
||||
(element-names :initform '(:prompt :input :footer))))
|
||||
|
||||
(defclass ein:htmlcell (ein:textcell)
|
||||
((cell-type :initarg :cell-type :initform "html")))
|
||||
|
||||
(defclass ein:markdowncell (ein:textcell)
|
||||
((cell-type :initarg :cell-type :initform "markdown")))
|
||||
|
||||
(defclass ein:rawcell (ein:textcell)
|
||||
((cell-type :initarg :cell-type :initform "raw")))
|
||||
|
||||
;;; Notifications
|
||||
|
||||
(defclass ein:notification-status ()
|
||||
((status :initarg :status :initform nil)
|
||||
(message :initarg :message :initform nil)
|
||||
(s2m :initarg :s2m))
|
||||
"Hold status and its string representation (message).")
|
||||
|
||||
(defclass ein:notification-tab ()
|
||||
((get-list :initarg :get-list :type function)
|
||||
(get-current :initarg :get-current :type function))
|
||||
;; These "methods" are for not depending on what the TABs for.
|
||||
;; Probably I'd want change this to be a separated Emacs lisp
|
||||
;; library at some point.
|
||||
"See `ein:notification-setup' for explanation.")
|
||||
|
||||
(defclass ein:notification ()
|
||||
((buffer :initarg :buffer :type buffer :document "Notebook buffer")
|
||||
(tab :initarg :tab :type ein:notification-tab)
|
||||
(execution-count
|
||||
:initform "y" :initarg :execution-count
|
||||
:documentation "Last `execution_count' sent by `execute_reply'.")
|
||||
(notebook
|
||||
:initarg :notebook
|
||||
:initform
|
||||
(ein:notification-status
|
||||
"NotebookStatus"
|
||||
:s2m
|
||||
'((notebook_saving.Notebook . "Saving notebook...")
|
||||
(notebook_saved.Notebook . "Notebook saved")
|
||||
(notebook_save_failed.Notebook . "Failed saving notebook!")))
|
||||
:type ein:notification-status)
|
||||
(kernel
|
||||
:initarg :kernel
|
||||
:initform
|
||||
(ein:notification-status
|
||||
"KernelStatus"
|
||||
:s2m
|
||||
'((status_idle.Kernel . nil)
|
||||
(status_busy.Kernel . "Kernel busy...")
|
||||
(status_restarting.Kernel . "Kernel restarting...")
|
||||
(status_restarted.Kernel . "Kernel restarted")
|
||||
(status_dead.Kernel . "Kernel requires restart \\<ein:notebook-mode-map>\\[ein:notebook-restart-session-command-km]")
|
||||
(status_reconnecting.Kernel . "Kernel reconnecting...")
|
||||
(status_reconnected.Kernel . "Kernel reconnected")
|
||||
(status_disconnected.Kernel . "Kernel requires reconnect \\<ein:notebook-mode-map>\\[ein:notebook-reconnect-session-command-km]")))
|
||||
:type ein:notification-status))
|
||||
"Notification widget for Notebook.")
|
||||
|
||||
;;; Events
|
||||
|
||||
(defclass ein:events ()
|
||||
((callbacks :initarg :callbacks :type hash-table
|
||||
:initform (make-hash-table :test 'eq)))
|
||||
"Event handler class.")
|
||||
|
||||
|
||||
(provide 'ein-classes)
|
||||
|
||||
;;; ein-classes.el ends here
|
||||
34
lisp/ein/ein-completer.el
Normal file
34
lisp/ein/ein-completer.el
Normal file
@@ -0,0 +1,34 @@
|
||||
;;; -*- mode: emacs-lisp; lexical-binding: t -*-
|
||||
;;; ein-completer.el --- Completion module
|
||||
|
||||
;; Copyright (C) 2018- Takafumi Arakaki / John Miller
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com> / John Miller <millejoh at mac.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-completer.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-completer.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-completer.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; This needs to get re-written.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(make-obsolete-variable 'ein:complete-on-dot nil "0.15.0")
|
||||
(make-obsolete-variable 'ein:completion-backend nil "0.17.0")
|
||||
|
||||
(provide 'ein-completer)
|
||||
|
||||
;;; ein-completer.el ends here
|
||||
353
lisp/ein/ein-contents-api.el
Normal file
353
lisp/ein/ein-contents-api.el
Normal file
@@ -0,0 +1,353 @@
|
||||
;;; ein-contents-api.el --- Interface to Jupyter's Contents API -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2015 - John Miller
|
||||
|
||||
;; Authors: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
;; John M. Miller <millejoh at mac.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-contents-api.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-contents-api.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-notebooklist.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
;;;
|
||||
;;; An interface to the Jupyter Contents API as described in
|
||||
;;; https://github.com/ipython/ipython/wiki/IPEP-27%3A-Contents-Service.
|
||||
;;;
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein-core)
|
||||
(require 'ein-classes)
|
||||
(require 'ein-utils)
|
||||
(require 'ein-log)
|
||||
(require 'ein-query)
|
||||
|
||||
(declare-function ein:notebook-to-json "ein-notebook")
|
||||
(declare-function ein:notebooklist-url "ein-notebooklist")
|
||||
|
||||
(defcustom ein:content-query-max-depth 2
|
||||
"Don't recurse the directory tree deeper than this."
|
||||
:type 'integer
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:content-query-max-branch 6
|
||||
"Don't descend into more than this number of directories per depth.
|
||||
The total number of parallel queries should therefore be
|
||||
O({max_branch}^{max_depth})."
|
||||
:type 'integer
|
||||
:group 'ein)
|
||||
|
||||
(make-obsolete-variable 'ein:content-query-timeout nil "0.17.0")
|
||||
|
||||
(defcustom ein:force-sync nil
|
||||
"When non-nil, force synchronous http requests."
|
||||
:type 'boolean
|
||||
:group 'ein)
|
||||
|
||||
(defun ein:content-query-contents (url-or-port path &optional callback errback iteration)
|
||||
"Register CALLBACK of arity 1 for the contents at PATH from the URL-OR-PORT.
|
||||
ERRBACK of arity 1 for the contents."
|
||||
(setq callback (or callback #'ignore))
|
||||
(setq errback (or errback #'ignore))
|
||||
(setq iteration (or iteration 0))
|
||||
(ein:query-singleton-ajax
|
||||
(ein:notebooklist-url url-or-port path)
|
||||
:type "GET"
|
||||
:parser #'ein:json-read
|
||||
:complete (apply-partially #'ein:content-query-contents--complete url-or-port path)
|
||||
:success (apply-partially #'ein:content-query-contents--success url-or-port path callback)
|
||||
:error (apply-partially #'ein:content-query-contents--error url-or-port path callback errback iteration)))
|
||||
|
||||
(cl-defun ein:content-query-contents--complete
|
||||
(_url-or-port _path
|
||||
&key data _symbol-status response &allow-other-keys
|
||||
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
|
||||
(ein:log 'debug "ein:query-contents--complete %s" resp-string))
|
||||
|
||||
(cl-defun ein:content-query-contents--error
|
||||
(url-or-port path callback errback iteration
|
||||
&key symbol-status response error-thrown data &allow-other-keys
|
||||
&aux
|
||||
(response-status (request-response-status-code response))
|
||||
(hub-p (request-response-header response "x-jupyterhub-version")))
|
||||
(cl-case response-status
|
||||
(404 (ein:log 'error "ein:content-query-contents--error %s %s"
|
||||
response-status (plist-get data :message))
|
||||
(when errback (funcall errback url-or-port response-status)))
|
||||
(t (if (< iteration 3)
|
||||
(if (and hub-p data (eq response-status 405))
|
||||
(ein:content-query-contents--success url-or-port path callback :data data)
|
||||
(ein:log 'verbose "Retry content-query-contents #%s in response to %s"
|
||||
iteration response-status)
|
||||
(sleep-for 0 (* (1+ iteration) 500))
|
||||
(ein:content-query-contents url-or-port path callback errback (1+ iteration)))
|
||||
(ein:log 'error "ein:content-query-contents--error %s REQUEST-STATUS %s DATA %s"
|
||||
(concat (file-name-as-directory url-or-port) path)
|
||||
symbol-status (cdr error-thrown))
|
||||
(when errback (funcall errback url-or-port response-status))))))
|
||||
|
||||
(cl-defun ein:content-query-contents--success
|
||||
(url-or-port path callback
|
||||
&key data _symbol-status _response &allow-other-keys)
|
||||
(when callback
|
||||
(funcall callback (ein:new-content url-or-port path data))))
|
||||
|
||||
(defun ein:content-to-json (content)
|
||||
(let ((path (if (>= (ein:$content-notebook-api-version content) 3)
|
||||
(ein:$content-path content)
|
||||
(substring (ein:$content-path content)
|
||||
0
|
||||
(or (cl-position ?/ (ein:$content-path content) :from-end t)
|
||||
0)))))
|
||||
(ignore-errors
|
||||
(ein:json-encode `((type . ,(ein:$content-type content))
|
||||
(name . ,(ein:$content-name content))
|
||||
(path . ,path)
|
||||
(format . ,(or (ein:$content-format content) "json"))
|
||||
(content ,@(ein:$content-raw-content content)))))))
|
||||
|
||||
(defun ein:content-from-notebook (nb)
|
||||
(let ((nb-content (ein:notebook-to-json nb)))
|
||||
(make-ein:$content :name (ein:$notebook-notebook-name nb)
|
||||
:path (ein:$notebook-notebook-path nb)
|
||||
:url-or-port (ein:$notebook-url-or-port nb)
|
||||
:type "notebook"
|
||||
:notebook-api-version (ein:$notebook-api-version nb)
|
||||
:raw-content (append nb-content nil))))
|
||||
|
||||
;;; Managing/listing the content hierarchy
|
||||
|
||||
(defvar *ein:content-hierarchy* (make-hash-table :test #'equal)
|
||||
"Content tree keyed by URL-OR-PORT.")
|
||||
|
||||
(defun ein:content-need-hierarchy (url-or-port)
|
||||
"Callers assume ein:content-query-hierarchy succeeded. If not, nil."
|
||||
(aif (gethash url-or-port *ein:content-hierarchy*) it
|
||||
(ein:log 'warn "No recorded content hierarchy for %s" url-or-port)
|
||||
nil))
|
||||
|
||||
(defun ein:new-content (url-or-port path data)
|
||||
;; data is like (:size 72 :content nil :writable t :path Untitled7.ipynb :name Untitled7.ipynb :type notebook)
|
||||
(let ((content (make-ein:$content
|
||||
:url-or-port url-or-port
|
||||
:notebook-api-version (ein:notebook-api-version-numeric url-or-port)
|
||||
:path path))
|
||||
(raw-content (if (vectorp (plist-get data :content))
|
||||
(append (plist-get data :content) nil)
|
||||
(plist-get data :content))))
|
||||
(setf (ein:$content-name content) (plist-get data :name)
|
||||
(ein:$content-path content) (plist-get data :path)
|
||||
(ein:$content-type content) (plist-get data :type)
|
||||
(ein:$content-created content) (plist-get data :created)
|
||||
(ein:$content-last-modified content) (plist-get data :last_modified)
|
||||
(ein:$content-format content) (plist-get data :format)
|
||||
(ein:$content-writable content) (plist-get data :writable)
|
||||
(ein:$content-mimetype content) (plist-get data :mimetype)
|
||||
(ein:$content-raw-content content) raw-content)
|
||||
content))
|
||||
|
||||
(defun ein:content-query-hierarchy* (url-or-port path callback sessions depth content)
|
||||
"Returns list (tree) of content objects. CALLBACK accepts tree."
|
||||
(let* ((url-or-port url-or-port)
|
||||
(path path)
|
||||
(callback callback)
|
||||
(items (ein:$content-raw-content content))
|
||||
(directories (if (< depth ein:content-query-max-depth)
|
||||
(cl-loop for item in items
|
||||
until (>= (length result) ein:content-query-max-branch)
|
||||
if (string= "directory" (plist-get item :type))
|
||||
collect (ein:new-content url-or-port path item)
|
||||
into result
|
||||
end
|
||||
finally return result)))
|
||||
(others (cl-loop for item in items
|
||||
with c0
|
||||
if (not (string= "directory" (plist-get item :type)))
|
||||
do (setf c0 (ein:new-content url-or-port path item))
|
||||
(setf (ein:$content-session-p c0)
|
||||
(gethash (ein:$content-path c0) sessions))
|
||||
and collect c0
|
||||
end)))
|
||||
(deferred:$
|
||||
(apply
|
||||
#'deferred:parallel
|
||||
(cl-loop for c0 in directories
|
||||
collect
|
||||
(let ((c0 c0)
|
||||
(d0 (deferred:new #'identity)))
|
||||
(ein:content-query-contents
|
||||
url-or-port
|
||||
(ein:$content-path c0)
|
||||
(apply-partially #'ein:content-query-hierarchy*
|
||||
url-or-port
|
||||
(ein:$content-path c0)
|
||||
(lambda (tree)
|
||||
(deferred:callback-post d0 (cons c0 tree)))
|
||||
sessions (1+ depth))
|
||||
(lambda (&rest _args) (deferred:callback-post d0 (cons c0 nil))))
|
||||
d0)))
|
||||
(deferred:nextc it
|
||||
(lambda (tree)
|
||||
(let ((result (append others tree)))
|
||||
(when (string= path "")
|
||||
(setf (gethash url-or-port *ein:content-hierarchy*) (-flatten result)))
|
||||
(funcall callback result)))))))
|
||||
|
||||
(defun ein:content-query-hierarchy (url-or-port &optional callback)
|
||||
"Get hierarchy of URL-OR-PORT with CALLBACK arity 1 for which hierarchy."
|
||||
(setq callback (or callback #'ignore))
|
||||
(ein:content-query-sessions
|
||||
url-or-port
|
||||
(apply-partially (lambda (url-or-port* callback* sessions)
|
||||
(ein:content-query-contents url-or-port* ""
|
||||
(apply-partially #'ein:content-query-hierarchy*
|
||||
url-or-port*
|
||||
""
|
||||
callback* sessions 0)
|
||||
(lambda (&rest _ignore)
|
||||
(when callback* (funcall callback* nil)))))
|
||||
url-or-port callback)
|
||||
callback))
|
||||
|
||||
;;; Save Content
|
||||
|
||||
(defsubst ein:content-url (content)
|
||||
(ein:notebooklist-url (ein:$content-url-or-port content)
|
||||
(ein:$content-path content)))
|
||||
|
||||
(defun ein:content-save (content &optional callback cbargs errcb errcbargs)
|
||||
(ein:query-singleton-ajax
|
||||
(ein:content-url content)
|
||||
:type "PUT"
|
||||
:headers '(("Content-Type" . "application/json"))
|
||||
:data (encode-coding-string (ein:content-to-json content) buffer-file-coding-system)
|
||||
:success (apply-partially #'ein:content-save-success callback cbargs)
|
||||
:error (apply-partially #'ein:content-save-error
|
||||
(ein:content-url content) errcb errcbargs)))
|
||||
|
||||
(cl-defun ein:content-save-success (callback cbargs &key _status _response &allow-other-keys)
|
||||
(when callback
|
||||
(apply callback cbargs)))
|
||||
|
||||
(cl-defun ein:content-save-error (url errcb errcbargs &key response &allow-other-keys)
|
||||
(ein:log 'error
|
||||
"ein:content-save-error: %s %s."
|
||||
url (error-message-string (request-response-error-thrown response)))
|
||||
(when errcb
|
||||
(apply errcb errcbargs)))
|
||||
|
||||
(defun ein:content-rename (content new-path &optional callback cbargs)
|
||||
(ein:query-singleton-ajax
|
||||
(ein:content-url content)
|
||||
:type "PATCH"
|
||||
:data (ein:json-encode `((path . ,new-path)))
|
||||
:parser #'ein:json-read
|
||||
:success (apply-partially #'update-content-path content callback cbargs)
|
||||
:error (apply-partially #'ein:content-rename-error (ein:$content-path content))))
|
||||
|
||||
(defun ein:session-rename (url-or-port session-id new-path)
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url url-or-port "api/sessions" session-id)
|
||||
:type "PATCH"
|
||||
:data (ein:json-encode `((path . ,new-path)))
|
||||
:complete #'ein:session-rename--complete))
|
||||
|
||||
(cl-defun ein:session-rename--complete (&key data response _symbol-status &allow-other-keys
|
||||
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
|
||||
(ein:log 'debug "ein:session-rename--complete %s" resp-string))
|
||||
|
||||
(cl-defun update-content-path (content callback cbargs &key data &allow-other-keys)
|
||||
(setf (ein:$content-path content) (plist-get data :path)
|
||||
(ein:$content-name content) (plist-get data :name)
|
||||
(ein:$content-last-modified content) (plist-get data :last_modified))
|
||||
(when callback
|
||||
(apply callback cbargs)))
|
||||
|
||||
(cl-defun ein:content-rename-error (path &key response data &allow-other-keys)
|
||||
(ein:log 'error
|
||||
"Renaming content %s failed %s %s."
|
||||
path (request-response-error-thrown response) (plist-get data :message)))
|
||||
|
||||
;;; Sessions
|
||||
|
||||
(defun ein:content-query-sessions (url-or-port &optional callback errback iteration)
|
||||
"Register CALLBACK of arity 1 to retrieve the sessions.
|
||||
Call ERRBACK of arity 1 (contents) upon failure."
|
||||
(setq callback (or callback #'ignore))
|
||||
(setq errback (or errback #'ignore))
|
||||
(setq iteration (or iteration 0))
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url url-or-port "api/sessions")
|
||||
:type "GET"
|
||||
:parser #'ein:json-read
|
||||
:complete (apply-partially #'ein:content-query-sessions--complete url-or-port callback)
|
||||
:success (apply-partially #'ein:content-query-sessions--success url-or-port callback)
|
||||
:error (apply-partially #'ein:content-query-sessions--error url-or-port callback errback iteration)))
|
||||
|
||||
(cl-defun ein:content-query-sessions--success (url-or-port callback &key data &allow-other-keys)
|
||||
(cl-flet ((read-name (nb-json)
|
||||
(if (< (ein:notebook-api-version-numeric url-or-port) 3)
|
||||
(if (string= (plist-get nb-json :path) "")
|
||||
(plist-get nb-json :name)
|
||||
(format "%s/%s" (plist-get nb-json :path) (plist-get nb-json :name)))
|
||||
(plist-get nb-json :path))))
|
||||
(let ((session-hash (make-hash-table :test 'equal)))
|
||||
(dolist (s (append data nil) (funcall callback session-hash))
|
||||
(setf (gethash (read-name (plist-get s :notebook)) session-hash)
|
||||
(cons (plist-get s :id) (plist-get s :kernel)))))))
|
||||
|
||||
(cl-defun ein:content-query-sessions--error
|
||||
(url-or-port callback errback iteration
|
||||
&key data response error-thrown &allow-other-keys
|
||||
&aux
|
||||
(response-status (request-response-status-code response))
|
||||
(hub-p (request-response-header response "x-jupyterhub-version")))
|
||||
(if (< iteration 3)
|
||||
(if (and hub-p data (eq response-status 405))
|
||||
(ein:content-query-sessions--success url-or-port callback :data data)
|
||||
(ein:log 'verbose "Retry sessions #%s in response to %s %S" iteration response-status response)
|
||||
(sleep-for 0 (* (1+ iteration) 500))
|
||||
(ein:content-query-sessions url-or-port callback errback (1+ iteration)))
|
||||
(ein:log 'error "ein:content-query-sessions--error %s: ERROR %s DATA %s" url-or-port (car error-thrown) (cdr error-thrown))
|
||||
(when errback (funcall errback nil))))
|
||||
|
||||
(cl-defun ein:content-query-sessions--complete
|
||||
(_url-or-port _callback
|
||||
&key data response &allow-other-keys
|
||||
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
|
||||
(ein:log 'debug "ein:query-sessions--complete %s" resp-string))
|
||||
|
||||
;;; Uploads
|
||||
|
||||
|
||||
(defun ein:get-local-file (path)
|
||||
"Get contents of PATH.
|
||||
Guess type of file (one of file, notebook, or directory)
|
||||
and content format (one of json, text, or base64)."
|
||||
(unless (file-readable-p path)
|
||||
(error "File %s is not accessible and cannot be uploaded." path))
|
||||
(let ((name (file-name-nondirectory path))
|
||||
(type (file-name-extension path)))
|
||||
(with-temp-buffer
|
||||
(insert-file-contents path)
|
||||
(cond ((string= type "ipynb")
|
||||
(list name "notebook" "json" (buffer-string)))
|
||||
((eql buffer-file-coding-system 'no-conversion)
|
||||
(list name "file" "base64" (buffer-string)))
|
||||
(t (list name "file" "text" (buffer-string)))))))
|
||||
|
||||
(provide 'ein-contents-api)
|
||||
174
lisp/ein/ein-core.el
Normal file
174
lisp/ein/ein-core.el
Normal file
@@ -0,0 +1,174 @@
|
||||
;;; ein-core.el --- EIN core -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012 Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-core.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-core.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-core.el.
|
||||
;; If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein) ; get autoloaded functions into namespace
|
||||
(require 'ein-utils)
|
||||
(require 'anaphora)
|
||||
(require 'request)
|
||||
|
||||
(defgroup ein nil
|
||||
"IPython notebook client in Emacs"
|
||||
:group 'applications
|
||||
:prefix "ein:")
|
||||
|
||||
(define-obsolete-variable-alias 'ein:url-or-port 'ein:urls "0.17.0")
|
||||
(defcustom ein:urls nil
|
||||
"List of default urls."
|
||||
:type '(repeat (choice (string :tag "Remote url")
|
||||
(integer :tag "Local port" 8888)))
|
||||
:group 'ein)
|
||||
|
||||
(make-obsolete-variable 'ein:default-url-or-port nil "0.17.0")
|
||||
|
||||
(defconst ein:source-dir (file-name-directory load-file-name)
|
||||
"Directory in which `ein*.el` files are located.")
|
||||
|
||||
(defun ein:version (&optional interactively copy-to-kill)
|
||||
"Return a longer version string.
|
||||
With prefix argument, copy the string to kill ring.
|
||||
The result contains `ein:version' and either git revision (if
|
||||
the source is in git repository) or elpa version."
|
||||
(interactive (list t current-prefix-arg))
|
||||
(let* ((version
|
||||
(or (and (ein:git-root-p
|
||||
(concat (file-name-as-directory ein:source-dir) ".."))
|
||||
(let ((default-directory ein:source-dir))
|
||||
(ein:git-revision-dirty)))
|
||||
(and (string-match "/ein-\\([0-9\\.]*\\)/$" ein:source-dir)
|
||||
(match-string 1 ein:source-dir)))))
|
||||
(when interactively
|
||||
(message "EIN version is %s" version))
|
||||
(when copy-to-kill
|
||||
(kill-new version))
|
||||
version))
|
||||
|
||||
;;; Server attribute getters. These should be moved to ein-open.el
|
||||
|
||||
(defvar *ein:notebook-api-version* (make-hash-table :test #'equal)
|
||||
"url-or-port to major notebook version")
|
||||
|
||||
(defvar *ein:kernelspecs* (make-hash-table :test #'equal)
|
||||
"url-or-port to kernelspecs")
|
||||
|
||||
(defun ein:get-kernelspec (url-or-port name &optional lang)
|
||||
(let* ((kernelspecs (ein:need-kernelspecs url-or-port))
|
||||
(name (if (stringp name)
|
||||
(intern (format ":%s" name))
|
||||
name))
|
||||
(ks (or (plist-get kernelspecs name)
|
||||
(cl-loop for (_key spec) on (ein:plist-exclude kernelspecs '(:default)) by 'cddr
|
||||
if (string= (ein:$kernelspec-language spec) lang)
|
||||
return spec
|
||||
end))))
|
||||
(cond ((stringp ks)
|
||||
(ein:get-kernelspec url-or-port ks))
|
||||
(t ks))))
|
||||
|
||||
(defun ein:need-kernelspecs (url-or-port)
|
||||
"Callers assume ein:query-kernelspecs succeeded. If not, nil."
|
||||
(aif (gethash url-or-port *ein:kernelspecs*) it
|
||||
(ein:log 'warn "No recorded kernelspecs for %s" url-or-port)
|
||||
nil))
|
||||
|
||||
(defsubst ein:notebook-api-version-numeric (url-or-port)
|
||||
(truncate (string-to-number (ein:need-notebook-api-version url-or-port))))
|
||||
|
||||
(defun ein:need-notebook-api-version (url-or-port)
|
||||
"Callers assume `ein:query-notebook-api-version' succeeded.
|
||||
If not, we hardcode a guess."
|
||||
(aif (gethash url-or-port *ein:notebook-api-version*) it
|
||||
(ein:log 'warn "No recorded notebook version for %s" url-or-port)
|
||||
"5"))
|
||||
|
||||
(defun ein:generic-getter (func-list)
|
||||
"Internal function for generic getter functions (`ein:get-*').
|
||||
|
||||
FUNC-LIST is a list of function which takes no argument and
|
||||
return what is desired or nil. Each function in FUNC-LIST is
|
||||
called one by one and the first non-nil result will be used. The
|
||||
function is not called when it is not bound. So, it is safe to
|
||||
give functions defined in lazy-loaded sub-modules.
|
||||
|
||||
This is something similar to dispatching in generic function such
|
||||
as `defgeneric' in EIEIO, but it takes no argument. Actual
|
||||
implementation is chosen based on context (buffer, point, etc.).
|
||||
This helps writing generic commands which requires same object
|
||||
but can operate in different contexts."
|
||||
(cl-loop for func in func-list
|
||||
if (and (functionp func) (funcall func))
|
||||
return it))
|
||||
|
||||
(defun ein:get-url-or-port ()
|
||||
(ein:generic-getter '(ein:get-url-or-port--notebooklist
|
||||
ein:get-url-or-port--notebook
|
||||
ein:get-url-or-port--worksheet
|
||||
ein:get-url-or-port--shared-output)))
|
||||
|
||||
(defun ein:get-kernel ()
|
||||
(ein:generic-getter '(ein:get-kernel--notebook
|
||||
ein:get-kernel--worksheet
|
||||
ein:get-kernel--shared-output
|
||||
ein:get-kernel--connect)))
|
||||
|
||||
(defun ein:get-kernel-or-error ()
|
||||
(or (ein:get-kernel)
|
||||
(error "No kernel related to the current buffer.")))
|
||||
|
||||
(defun ein:get-cell-at-point ()
|
||||
(ein:generic-getter '(ein:get-cell-at-point--worksheet
|
||||
ein:get-cell-at-point--shared-output)))
|
||||
|
||||
(defun ein:get-traceback-data ()
|
||||
(append (ein:generic-getter '(ein:get-traceback-data--worksheet
|
||||
ein:get-traceback-data--shared-output
|
||||
ein:get-traceback-data--connect))
|
||||
nil))
|
||||
|
||||
;;; Emacs utilities
|
||||
|
||||
(defun ein:clean-compiled-files ()
|
||||
(let* ((files (directory-files ein:source-dir 'full "^ein-.*\\.elc$")))
|
||||
(mapc #'delete-file files)
|
||||
(message "Removed %s byte-compiled files." (length files))))
|
||||
|
||||
(defun ein:byte-compile-ein ()
|
||||
"Byte compile EIN files."
|
||||
(interactive)
|
||||
(ein:clean-compiled-files)
|
||||
(let* ((files (directory-files ein:source-dir 'full "^ein-.*\\.el$"))
|
||||
(errors (cl-mapcan (lambda (f) (unless (byte-compile-file f) (list f)))
|
||||
files)))
|
||||
(aif errors
|
||||
(error "Got %s errors while compiling these files: %s"
|
||||
(length errors)
|
||||
(ein:join-str " " (mapcar #'file-name-nondirectory it))))
|
||||
(message "Compiled %s files" (length files))))
|
||||
|
||||
(provide 'ein-core)
|
||||
|
||||
;;; ein-core.el ends here
|
||||
230
lisp/ein/ein-dev.el
Normal file
230
lisp/ein/ein-dev.el
Normal file
@@ -0,0 +1,230 @@
|
||||
;;; ein-dev.el --- Development tools -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-dev.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-dev.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-dev.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein-notebook)
|
||||
|
||||
(defvar ein:dev-trace-curl nil "Turn on to really go after it.")
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:dev-start-debug ()
|
||||
"Start logging a bunch of stuff."
|
||||
(interactive)
|
||||
(setq debug-on-error t)
|
||||
(setq request-log-level (quote debug))
|
||||
(let ((curl-trace (concat temporary-file-directory "curl-trace")))
|
||||
(setq request-curl-options
|
||||
(append request-curl-options `("--trace-ascii" ,curl-trace)))
|
||||
(add-function :after
|
||||
(symbol-function 'request--curl-callback)
|
||||
(lambda (&rest _args)
|
||||
(when ein:dev-trace-curl
|
||||
(if (file-readable-p curl-trace)
|
||||
(with-temp-buffer
|
||||
(insert-file-contents curl-trace)
|
||||
(request-log 'debug (buffer-string)))
|
||||
(request-log 'debug "%s unreadable" curl-trace))))))
|
||||
(setq request-message-level (quote verbose))
|
||||
(setq websocket-debug t)
|
||||
(setq websocket-callback-debug-on-error t)
|
||||
(ein:log-set-level 'debug)
|
||||
(ein:log-set-message-level 'verbose))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:dev-stop-debug ()
|
||||
"Inverse of `ein:dev-start-debug'.
|
||||
Impossible to maintain because it needs to match start."
|
||||
(interactive)
|
||||
(setq debug-on-error nil)
|
||||
(setq websocket-debug nil)
|
||||
(setq request-log-level -1)
|
||||
(setq request-message-level 'warn)
|
||||
(setq websocket-callback-debug-on-error nil)
|
||||
(ein:log-set-level 'verbose)
|
||||
(ein:log-set-message-level 'info)
|
||||
(let ((curl-trace (concat temporary-file-directory "curl-trace")))
|
||||
(setq request-curl-options
|
||||
(cl-remove-if (lambda (x) (member x `("--trace-ascii" ,curl-trace)))
|
||||
request-curl-options))))
|
||||
|
||||
(defun ein:dev-stdout-program (command args)
|
||||
"Safely call COMMAND with ARGS and return its stdout."
|
||||
(aand (executable-find command)
|
||||
(with-temp-buffer
|
||||
(erase-buffer)
|
||||
(apply #'call-process it nil t nil args)
|
||||
(buffer-string))))
|
||||
|
||||
(defun ein:dev-packages ()
|
||||
(with-temp-buffer
|
||||
(insert-file-contents (locate-library "ein"))
|
||||
(mapcar (lambda (x) (symbol-name (cl-first x)))
|
||||
(package-desc-reqs (package-buffer-info)))))
|
||||
|
||||
(defun ein:dev-sys-info ()
|
||||
"Returns a list."
|
||||
(cl-flet ((lib-info
|
||||
(name)
|
||||
(let* ((libsym (intern-soft name))
|
||||
(version-var (cl-loop for fmt in '("%s-version" "%s:version")
|
||||
if (intern-soft (format fmt name))
|
||||
return it))
|
||||
(version (symbol-value version-var)))
|
||||
(list :name name
|
||||
:path (aand (locate-library name) (abbreviate-file-name it))
|
||||
:featurep (featurep libsym)
|
||||
:version-var version-var
|
||||
:version version)))
|
||||
(dump-vars
|
||||
(names)
|
||||
(cl-loop for var in names
|
||||
collect (intern (format ":%s" var))
|
||||
collect (symbol-value (intern (format "ein:%s" var))))))
|
||||
(list
|
||||
"EIN system info"
|
||||
:emacs-version (emacs-version)
|
||||
:window-system window-system
|
||||
:emacs-variant
|
||||
(cond ((boundp 'spacemacs-version) (concat "spacemacs" spacemacs-version))
|
||||
((boundp 'doom-version) (concat "doom-" doom-version)))
|
||||
:build system-configuration-options
|
||||
:os (list
|
||||
:uname (ein:dev-stdout-program "uname" '("-a"))
|
||||
:lsb-release (ein:dev-stdout-program "lsb_release" '("-a")))
|
||||
:jupyter (ein:dev-stdout-program "jupyter" '("--version"))
|
||||
:image-types (ein:eval-if-bound 'image-types)
|
||||
:image-types-available (seq-filter #'image-type-available-p
|
||||
(ein:eval-if-bound 'image-types))
|
||||
:request-backend request-backend
|
||||
:ein (append (list :version (ein:version))
|
||||
(dump-vars '("source-dir")))
|
||||
:lib (seq-filter (lambda (info) (plist-get info :path))
|
||||
(mapcar #'lib-info (ein:dev-packages))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:dev-bug-report-template ()
|
||||
"Open a buffer with bug report template."
|
||||
(interactive)
|
||||
(let ((buffer (generate-new-buffer "*ein:bug-report*")))
|
||||
(with-current-buffer buffer
|
||||
(erase-buffer)
|
||||
(insert "## Problem description\n\n"
|
||||
"## Steps to reproduce the problem\n\n"
|
||||
"<!-- Ensure no information sensitive to your institution below!!! -->\n"
|
||||
"## System info:\n\n"
|
||||
"```cl\n")
|
||||
(condition-case err
|
||||
(ein:dev-pp-sys-info buffer)
|
||||
(error (insert (format "ein:dev-sys-info erred: %s" (error-message-string err)))))
|
||||
(insert "```\n"
|
||||
"## Logs:\n")
|
||||
(ein:dev-dump-logs buffer)
|
||||
(goto-char (point-min))
|
||||
(pop-to-buffer buffer))))
|
||||
|
||||
(defvar *ein:jupyter-server-buffer-name*)
|
||||
(defun ein:dev-dump-logs (&optional stream)
|
||||
(interactive)
|
||||
(dolist (notebook (ein:notebook-opened-notebooks))
|
||||
(-when-let* ((kernel (ein:$notebook-kernel notebook))
|
||||
(websocket (ein:$kernel-websocket kernel))
|
||||
(ws (ein:$websocket-ws websocket))
|
||||
(ws-buf (websocket-get-debug-buffer-create ws)))
|
||||
(let (dump)
|
||||
(with-current-buffer ws-buf
|
||||
(setq dump (buffer-substring-no-properties
|
||||
(point-min) (point-max))))
|
||||
(if (zerop (length dump))
|
||||
(kill-buffer ws-buf)
|
||||
(mapc (lambda (s)
|
||||
(princ (format "%s\n" s) (or stream standard-output)))
|
||||
(list
|
||||
(format "#### `%s`:" (ein:url (ein:$kernel-url-or-port kernel)
|
||||
(ein:$kernel-path kernel)))
|
||||
"```"
|
||||
(string-trim dump)
|
||||
"```"))))))
|
||||
(cl-macrolet ((dump
|
||||
(name)
|
||||
`(awhen (get-buffer ,name)
|
||||
(with-current-buffer it
|
||||
(mapc (lambda (s)
|
||||
(princ (format "%s\n" s)
|
||||
(or stream standard-output)))
|
||||
(list
|
||||
(format "#### %s:" ,name)
|
||||
"```"
|
||||
(string-trim (buffer-substring-no-properties
|
||||
(point-min) (point-max)))
|
||||
"```"))))))
|
||||
(dump request-log-buffer-name)
|
||||
(dump ein:log-all-buffer-name)
|
||||
(dump *ein:jupyter-server-buffer-name*)))
|
||||
|
||||
(defun ein:dev-pp-sys-info (&optional stream)
|
||||
(interactive)
|
||||
(princ (ein:dev-obj-to-string (ein:dev-sys-info))
|
||||
(or stream standard-output)))
|
||||
|
||||
(defvar pp-escape-newlines)
|
||||
(defun ein:dev-obj-to-string (object)
|
||||
(with-temp-buffer
|
||||
(erase-buffer)
|
||||
(let ((pp-escape-newlines nil))
|
||||
(pp object (current-buffer)))
|
||||
(goto-char (point-min))
|
||||
(let ((emacs-lisp-mode-hook nil))
|
||||
(emacs-lisp-mode))
|
||||
(ein:dev-pp-sexp)
|
||||
(buffer-string)))
|
||||
|
||||
(defun ein:dev-pp-sexp ()
|
||||
"Prettify s-exp at point recursively.
|
||||
Use this function in addition to `pp' (see `ein:dev-obj-to-string')."
|
||||
(down-list)
|
||||
(condition-case nil
|
||||
(while t
|
||||
(forward-sexp)
|
||||
;; Prettify nested s-exp.
|
||||
(when (looking-back ")" (1- (point)))
|
||||
(save-excursion
|
||||
(backward-sexp)
|
||||
(ein:dev-pp-sexp)))
|
||||
;; Add newline before keyword symbol.
|
||||
(when (looking-at-p " :")
|
||||
(newline-and-indent))
|
||||
;; Add newline before long string literal.
|
||||
(when (and (looking-at-p " \"")
|
||||
(let ((end (save-excursion
|
||||
(forward-sexp)
|
||||
(point))))
|
||||
(> (- end (point)) 80)))
|
||||
(newline-and-indent)))
|
||||
(scan-error)))
|
||||
|
||||
(provide 'ein-dev)
|
||||
|
||||
;;; ein-dev.el ends here
|
||||
63
lisp/ein/ein-events.el
Normal file
63
lisp/ein/ein-events.el
Normal file
@@ -0,0 +1,63 @@
|
||||
;;; ein-events.el --- Event module -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-events.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-events.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-events.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'eieio)
|
||||
|
||||
(require 'ein-core)
|
||||
(require 'ein-classes)
|
||||
(require 'ein-log)
|
||||
|
||||
(defun ein:events-new ()
|
||||
"Return a new event handler instance."
|
||||
(make-instance 'ein:events))
|
||||
|
||||
(defun ein:events-trigger (events event-type &optional data)
|
||||
"Trigger EVENT-TYPE and let event handler EVENTS handle that event."
|
||||
(ein:log 'debug "Event: %S" event-type)
|
||||
(aif (gethash event-type (slot-value events 'callbacks))
|
||||
(mapc (lambda (cb-arg) (ein:funcall-packed cb-arg data)) it)
|
||||
(ein:log 'info "Unknown event: %S" event-type)))
|
||||
|
||||
|
||||
(cl-defmethod ein:events-on ((events ein:events) event-type
|
||||
callback &optional arg)
|
||||
"Set event trigger hook.
|
||||
|
||||
When EVENT-TYPE is triggered on the event handler EVENTS,
|
||||
CALLBACK is called. CALLBACK must take two arguments:
|
||||
ARG as the first argument and DATA, which is passed via
|
||||
`ein:events-trigger', as the second."
|
||||
(cl-assert (symbolp event-type) t "%s not symbol" event-type)
|
||||
(let* ((table (slot-value events 'callbacks))
|
||||
(cbs (gethash event-type table)))
|
||||
(push (cons callback arg) cbs)
|
||||
(puthash event-type cbs table)))
|
||||
|
||||
|
||||
(provide 'ein-events)
|
||||
|
||||
;;; ein-events.el ends here
|
||||
63
lisp/ein/ein-file.el
Normal file
63
lisp/ein/ein-file.el
Normal file
@@ -0,0 +1,63 @@
|
||||
;;; ein-file.el --- Editing files downloaded from jupyter -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2017- John M. Miller
|
||||
|
||||
;; Authors: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
;; John M. Miller <millejoh at mac.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-file.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-file.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-notebooklist.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
(defvar *ein:file-buffername-template* "'/ein:%s:%s")
|
||||
(ein:deflocal ein:content-file-buffer--content nil)
|
||||
|
||||
;; (push '("^ein:.*" . ein:content-file-handler)
|
||||
;; file-name-handler-alist)
|
||||
|
||||
(defun ein:file-buffer-name (urlport path)
|
||||
(format *ein:file-buffername-template*
|
||||
urlport
|
||||
path))
|
||||
|
||||
(defun ein:file-open (url-or-port path)
|
||||
(interactive (ein:notebooklist-parse-nbpath (ein:notebooklist-ask-path "file")))
|
||||
(ein:content-query-contents url-or-port path #'ein:file-open-finish nil))
|
||||
|
||||
(defun ein:file-open-finish (content)
|
||||
(with-current-buffer (get-buffer-create (ein:file-buffer-name (ein:$content-url-or-port content)
|
||||
(ein:$content-path content)))
|
||||
(setq ein:content-file-buffer--content content)
|
||||
(let ((raw-content (ein:$content-raw-content content)))
|
||||
(if (eq system-type 'windows-nt)
|
||||
(insert (decode-coding-string raw-content 'utf-8))
|
||||
(insert raw-content)))
|
||||
(set-visited-file-name (buffer-name))
|
||||
(set-auto-mode)
|
||||
(add-hook 'write-contents-functions 'ein:content-file-save nil t) ;; FIXME Brittle, will not work
|
||||
;; if user changes major mode.
|
||||
(ein:log 'verbose "Opened file %s" (ein:$content-name content))
|
||||
(set-buffer-modified-p nil)
|
||||
(goto-char (point-min))
|
||||
(pop-to-buffer (buffer-name))))
|
||||
|
||||
(defun ein:content-file-save ()
|
||||
(setf (ein:$content-raw-content ein:content-file-buffer--content) (buffer-string))
|
||||
(ein:content-save ein:content-file-buffer--content)
|
||||
(set-buffer-modified-p nil)
|
||||
t)
|
||||
|
||||
(provide 'ein-file)
|
||||
729
lisp/ein/ein-gat.el
Normal file
729
lisp/ein/ein-gat.el
Normal file
@@ -0,0 +1,729 @@
|
||||
;;; ein-gat.el --- hooks to gat -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2019 The Authors
|
||||
|
||||
;; Authors: dickmao <github id: dickmao>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-gat.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-gat.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-gat.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'compile)
|
||||
(require 'seq)
|
||||
(require 'magit-process nil t)
|
||||
(autoload 'ein:jupyter-running-notebook-directory "ein-jupyter")
|
||||
|
||||
;; (declare-function magit--process-coding-system "magit-process")
|
||||
;; (declare-function magit-call-process "magit-process")
|
||||
;; (declare-function magit-start-process "magit-process")
|
||||
;; (declare-function magit-process-sentinel "magit-process")
|
||||
|
||||
(defconst ein:gat-status-cd 7 "gat exits 7 if requiring a change directory.")
|
||||
|
||||
(defcustom ein:gat-python-command (if (equal system-type 'windows-nt)
|
||||
(or (executable-find "py")
|
||||
(executable-find "pythonw")
|
||||
"python")
|
||||
"python")
|
||||
"Python executable name."
|
||||
:type (append '(choice)
|
||||
(let (result)
|
||||
(dolist (py '("python" "python3" "pythonw" "py") result)
|
||||
(setq result (append result `((const :tag ,py ,py))))))
|
||||
'((string :tag "Other")))
|
||||
:group 'ein)
|
||||
|
||||
(defsubst ein:gat-shell-command (command)
|
||||
(string-trim (shell-command-to-string (concat "2>/dev/null " command))))
|
||||
|
||||
(defcustom ein:gat-version
|
||||
(ein:gat-shell-command "gat --project - --region - --zone - version")
|
||||
"Currently, aws or gce."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
(defconst ein:gat-required-version "0.0.4-pre")
|
||||
|
||||
(defvar ein:gat-machine-history nil
|
||||
"History of user entered machine type.")
|
||||
|
||||
(defcustom ein:gat-vendor
|
||||
(ein:gat-shell-command "gat --project - --region - --zone - vendor")
|
||||
"Currently, aws or gce."
|
||||
:type '(choice (const :tag "aws" "aws") (const :tag "gce" "gce"))
|
||||
:group 'ein
|
||||
:set (lambda (symbol value)
|
||||
(setq ein:gat-machine-history nil)
|
||||
(set-default symbol value)))
|
||||
|
||||
(defcustom ein:gat-gce-zone (ein:gat-shell-command "gcloud config get-value compute/zone")
|
||||
"gcloud project zone."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:gat-gce-region (ein:gat-shell-command "gcloud config get-value compute/region")
|
||||
"gcloud project region."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:gat-aws-region (ein:gat-shell-command "aws configure get region")
|
||||
"gcloud project region."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:gat-gce-project (ein:gat-shell-command "gcloud config get-value core/project")
|
||||
"gcloud project id."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:gat-aws-machine-types (split-string (ein:gat-shell-command "aws ec2 describe-instance-type-offerings --location-type=region --page-size=1000 --filter Name=location,Values=us-east-2 --query 'sort_by(InstanceTypeOfferings, &InstanceType)[].InstanceType' --output text"))
|
||||
"gcloud machine types."
|
||||
:type '(repeat string)
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:gat-gce-machine-types (split-string (ein:gat-shell-command (format "gcloud compute machine-types list --filter=\"zone:%s\" --format=\"value[terminator=' '](name)\"" ein:gat-gce-zone)))
|
||||
"gcloud machine types."
|
||||
:type '(repeat string)
|
||||
:group 'ein)
|
||||
|
||||
;; https://accounts.google.com/o/oauth2/auth?client_id=[client-id]&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/compute&response_type=code
|
||||
;; curl -d code=[page-code] -d client_id=[client-id] -d client_secret=[client-secret] -d redirect_uri=urn:ietf:wg:oauth:2.0:oob -d grant_type=authorization_code https://accounts.google.com/o/oauth2/token
|
||||
;; curl -sLk -H "Authorization: Bearer [access-token]" https://compute.googleapis.com/compute/v1/projects/[project-id]/zones/[zone-id]/acceleratorTypes | jq -r -c '.items[].selfLink'
|
||||
(defcustom ein:gat-gpu-types (split-string "nvidia-tesla-t4 nvidia-tesla-v100")
|
||||
"Gat gpu types."
|
||||
:type '(repeat string)
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:gat-base-images '("dickmao/tensorflow-gpu"
|
||||
"dickmao/scipy-gpu"
|
||||
"dickmao/pytorch-gpu")
|
||||
"Known https://hub.docker.com/u/jupyter images."
|
||||
:type '(repeat (string :tag "FROM-appropriate docker image"))
|
||||
:group 'ein)
|
||||
|
||||
(defvar ein:gat-previous-worktree nil)
|
||||
|
||||
(defvar ein:gat-urls nil)
|
||||
|
||||
(defconst ein:gat-master-worktree "master")
|
||||
|
||||
(defvar ein:gat-current-worktree ein:gat-master-worktree)
|
||||
|
||||
(defvar ein:gat-disksizegb-history '("default")
|
||||
"History of user entered disk size.")
|
||||
|
||||
(defvar ein:gat-gpus-history '("0")
|
||||
"History of user entered gpu count.")
|
||||
|
||||
(defvar ein:gat-gpu-type-history nil
|
||||
"History of user entered gpu types.")
|
||||
|
||||
(defvar ein:gat-keyname-history nil
|
||||
"History of user entered aws ssh keyname.")
|
||||
|
||||
(defvar ein:gat-preemptible-history nil
|
||||
"History of preemptible opt-in.")
|
||||
|
||||
(defun ein:gat-where-am-i (&optional print-message)
|
||||
(interactive "p")
|
||||
(let ((from-end (cl-search "/.gat" default-directory :from-end)))
|
||||
(cond ((and (string= major-mode "magit-process-mode")
|
||||
(string-prefix-p "ein-gat:" (buffer-name)))
|
||||
(aprog1 default-directory
|
||||
(when print-message
|
||||
(message it))))
|
||||
((string= major-mode "ein:ipynb-mode")
|
||||
(aprog1 (directory-file-name (file-name-directory (buffer-file-name)))
|
||||
(when print-message
|
||||
(message it))))
|
||||
((file-directory-p
|
||||
(concat (file-name-as-directory default-directory) ".gat"))
|
||||
(aprog1 default-directory
|
||||
(when print-message
|
||||
(message it))))
|
||||
(from-end
|
||||
(aprog1 (file-name-as-directory
|
||||
(cl-subseq default-directory 0 from-end))
|
||||
(when print-message
|
||||
(message it))))
|
||||
(t
|
||||
(if-let ((notebook-dir (ein:jupyter-running-notebook-directory))
|
||||
(notebook (ein:get-notebook))
|
||||
(where (directory-file-name
|
||||
(concat (file-name-as-directory notebook-dir)
|
||||
(file-name-directory (ein:$notebook-notebook-path notebook))))))
|
||||
(aprog1 where
|
||||
(when print-message
|
||||
(message it)))
|
||||
(prog1 nil
|
||||
(when print-message
|
||||
(message "nowhere"))))))))
|
||||
|
||||
(cl-defun ein:gat-jupyter-login (ipynb-name notebook-dir callback &rest args &key public-ip-address)
|
||||
(if public-ip-address
|
||||
(let ((url-or-port (ein:url (format "http://%s:8888" public-ip-address))))
|
||||
(setf (alist-get (intern url-or-port) ein:gat-urls) notebook-dir)
|
||||
(ein:login url-or-port
|
||||
(lambda (buffer url-or-port)
|
||||
(pop-to-buffer buffer)
|
||||
(ein:notebook-open url-or-port ipynb-name nil callback))))
|
||||
(ein:log 'error "ein:gat-jupyter-login: no public ip address")))
|
||||
|
||||
(defun ein:gat-process-filter (proc string)
|
||||
"Copied `magit-process-filter' with added wrinkle of `ansi-color'.
|
||||
Advising `insert' in `magit-process-filter' is a little sus, and
|
||||
moreover, how would I avoid messing `magit-process-filter' of
|
||||
other processes?"
|
||||
(with-current-buffer (process-buffer proc)
|
||||
(let ((inhibit-read-only t))
|
||||
(goto-char (process-mark proc))
|
||||
;; Find last ^M in string. If one was found, ignore
|
||||
;; everything before it and delete the current line.
|
||||
(when-let ((ret-pos (cl-position ?\r string :from-end t)))
|
||||
(cl-callf substring string (1+ ret-pos))
|
||||
(delete-region (line-beginning-position) (point)))
|
||||
(insert (propertize (ansi-color-filter-apply string) 'magit-section
|
||||
(process-get proc 'section)))
|
||||
(set-marker (process-mark proc) (point)))))
|
||||
|
||||
;; (defvar magit-process-popup-time)
|
||||
;; (defvar inhibit-magit-refresh)
|
||||
;; (defvar magit-process-raise-error)
|
||||
;; (defvar magit-process-display-mode-line-error)
|
||||
(cl-defun ein:gat-chain (buffer callback exec &rest args &key public-ip-address notebook-dir &allow-other-keys)
|
||||
(declare (indent 0))
|
||||
(let* ((default-directory (or notebook-dir (ein:gat-where-am-i)))
|
||||
(default-process-coding-system (magit--process-coding-system))
|
||||
(magit-inhibit-refresh t)
|
||||
(_ (awhen (getenv "GAT_APPLICATION_CREDENTIALS")
|
||||
(push (concat "GOOGLE_APPLICATION_CREDENTIALS=" it) process-environment)))
|
||||
(activate-with-editor-mode
|
||||
(when (string= (car exec) with-editor-emacsclient-executable)
|
||||
(lambda () (when (string= (buffer-name) (car (last exec)))
|
||||
(with-editor-mode 1)))))
|
||||
(process (let ((magit-buffer-name-format "%xein-gat%v: %t%x"))
|
||||
(apply #'magit-start-process exec))))
|
||||
(when activate-with-editor-mode
|
||||
(add-hook 'find-file-hook activate-with-editor-mode))
|
||||
;; (with-current-buffer (process-buffer process)
|
||||
;; (special-mode))
|
||||
(with-editor-set-process-filter process #'ein:gat-process-filter)
|
||||
(set-process-sentinel
|
||||
process
|
||||
(lambda (proc event)
|
||||
(let* ((gat-status (process-exit-status proc))
|
||||
(process-buf (process-buffer proc))
|
||||
(section (process-get proc 'section))
|
||||
(gat-status-cd-p (= gat-status ein:gat-status-cd))
|
||||
worktree-dir new-public-ip-address)
|
||||
(when activate-with-editor-mode
|
||||
(remove-hook 'find-file-hook activate-with-editor-mode))
|
||||
(let ((magit-process-display-mode-line-error
|
||||
(if gat-status-cd-p nil magit-process-display-mode-line-error))
|
||||
(magit-process-raise-error
|
||||
(if gat-status-cd-p nil magit-process-raise-error))
|
||||
(short-circuit (lambda (&rest _args) (when gat-status-cd-p 0))))
|
||||
(add-function :before-until (symbol-function 'process-exit-status)
|
||||
short-circuit)
|
||||
(unwind-protect
|
||||
(magit-process-sentinel proc event)
|
||||
(remove-function (symbol-function 'process-exit-status) short-circuit)))
|
||||
(cond
|
||||
((or (zerop gat-status) gat-status-cd-p)
|
||||
(alet (and (bufferp process-buf)
|
||||
(with-current-buffer process-buf
|
||||
(when (integer-or-marker-p (oref section content))
|
||||
(buffer-substring-no-properties (oref section content)
|
||||
(oref section end)))))
|
||||
(when it
|
||||
(when gat-status-cd-p
|
||||
(setq worktree-dir (when (string-match "^cd\\s-+\\(\\S-+\\)" it)
|
||||
(string-trim (match-string 1 it)))))
|
||||
(when-let ((last-line (car (last (split-string (string-trim it) "\n")))))
|
||||
(setq new-public-ip-address
|
||||
(when (string-match "^\\([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\\)" last-line)
|
||||
(string-trim (match-string 1 last-line))))))
|
||||
(when callback
|
||||
(when (buffer-live-p buffer)
|
||||
(set-buffer buffer))
|
||||
(let ((magit-process-popup-time 0))
|
||||
(apply callback
|
||||
(append
|
||||
(when worktree-dir
|
||||
`(:worktree-dir ,worktree-dir))
|
||||
(when-let ((address (or new-public-ip-address
|
||||
public-ip-address)))
|
||||
`(:public-ip-address ,address))))))))
|
||||
(t
|
||||
(ein:log 'error "ein:gat-chain: %s exited %s"
|
||||
(car exec) (process-exit-status proc)))))))
|
||||
process))
|
||||
|
||||
(defun ein:gat--path (archepath worktree-dir)
|
||||
"Form relative path from ARCHEPATH root, WORKTREE-DIR subroot, ARCHEPATH leaf.
|
||||
|
||||
With WORKTREE-DIR of 3/4/1/2/.gat/fantab,
|
||||
1/2/eager.ipynb -> 1/2/.gat/fantab/eager.ipynb
|
||||
1/2/.gat/fubar/subdir/eager.ipynb -> 1/2/.gat/fantab/subdir/eager.ipynb
|
||||
|
||||
With WORKTREE-DIR of /home/dick/gat/test-repo2
|
||||
.gat/getout/eager.ipynb -> eager.ipynb
|
||||
"
|
||||
(when-let ((root (directory-file-name (or (awhen (cl-search ".gat/" archepath :from-end)
|
||||
(cl-subseq archepath 0 it))
|
||||
(file-name-directory archepath)
|
||||
""))))
|
||||
(if (zerop (length root))
|
||||
(concat (replace-regexp-in-string
|
||||
"^\\./" ""
|
||||
(file-name-as-directory
|
||||
(cl-subseq worktree-dir
|
||||
(or (cl-search ".gat/" worktree-dir :from-end)
|
||||
(length worktree-dir)))))
|
||||
(file-name-nondirectory archepath))
|
||||
(concat (file-name-as-directory
|
||||
(cl-subseq worktree-dir
|
||||
(cl-search root worktree-dir :from-end)))
|
||||
(or (awhen (string-match "\\(\\.gat/[^/]+/\\)" archepath)
|
||||
(cl-subseq archepath (+ it (length (match-string 1 archepath)))))
|
||||
(file-name-nondirectory archepath))))))
|
||||
|
||||
(defun ein:gat-zone ()
|
||||
(interactive)
|
||||
(cl-case (intern ein:gat-vendor)
|
||||
(gce ein:gat-gce-zone)
|
||||
(otherwise "-")))
|
||||
|
||||
(defun ein:gat-region ()
|
||||
(interactive)
|
||||
(cl-case (intern ein:gat-vendor)
|
||||
(aws ein:gat-aws-region)
|
||||
(gce ein:gat-gce-region)
|
||||
(otherwise (or ein:gat-aws-region ein:gat-gce-region))))
|
||||
|
||||
(defun ein:gat-project ()
|
||||
(interactive)
|
||||
(cl-case (intern ein:gat-vendor)
|
||||
(gce ein:gat-gce-project)
|
||||
(otherwise "-")))
|
||||
|
||||
(defun ein:gat-machine-types ()
|
||||
(interactive)
|
||||
(cl-case (intern ein:gat-vendor)
|
||||
(aws ein:gat-aws-machine-types)
|
||||
(gce ein:gat-gce-machine-types)
|
||||
(otherwise (or ein:gat-aws-machine-types ein:gat-gce-machine-types))))
|
||||
|
||||
(defsubst ein:gat-need-upgrade ()
|
||||
(version-list-< (version-to-list ein:gat-version)
|
||||
(version-to-list ein:gat-required-version)))
|
||||
|
||||
(defmacro ein:gat-install-gat (&rest body)
|
||||
`(if (and (executable-find "gat")
|
||||
(not (ein:gat-need-upgrade)))
|
||||
(progn ,@body)
|
||||
(if (zerop (length (ein:gat-region)))
|
||||
(ein:log 'error "ein:gat-install-gat: no cloud utilities detected")
|
||||
(ein:log 'info "ein:gat-install-gat: %s gat..."
|
||||
(if (executable-find "gat") "Upgrading" "Installing"))
|
||||
(let* ((orig-buf (current-buffer))
|
||||
(bufname "*gat-install*")
|
||||
(dir (make-temp-file "gat-install" t))
|
||||
(commands `(,(format "cd %s" dir)
|
||||
,(format "git clone --depth=1 --single-branch --branch=%s https://github.com/dickmao/gat.git" (if noninteractive "dev" ein:gat-required-version))
|
||||
"make -C gat install"))
|
||||
(compile (format "bash -ex -c '%s'" (mapconcat #'identity commands "; ")))
|
||||
(callback (lambda (_buf msg)
|
||||
(when (cl-search "finished" msg)
|
||||
(with-current-buffer orig-buf
|
||||
(custom-set-default
|
||||
'ein:gat-version
|
||||
(ein:gat-shell-command
|
||||
"gat --project - --region - --zone - version"))
|
||||
,@body)))))
|
||||
(let ((compilation-scroll-output t))
|
||||
(compilation-start compile nil (lambda (&rest _args) bufname)))
|
||||
(with-current-buffer bufname
|
||||
(add-hook 'compilation-finish-functions callback nil t))))))
|
||||
|
||||
(defun ein:gat-edit (&optional _refresh)
|
||||
(interactive "P")
|
||||
(ein:gat-install-gat
|
||||
(if-let ((default-directory (ein:gat-where-am-i))
|
||||
(gat-chain-args `("gat" nil "--project" ,(ein:gat-project)
|
||||
"--region" ,(ein:gat-region) "--zone" ,(ein:gat-zone))))
|
||||
(if (special-variable-p 'magit-process-popup-time)
|
||||
(let ((magit-process-popup-time -1)
|
||||
(notebook (ein:get-notebook)))
|
||||
(ein:gat-chain
|
||||
(current-buffer)
|
||||
(cl-function
|
||||
(lambda (&rest args &key worktree-dir &allow-other-keys)
|
||||
(if notebook
|
||||
(ein:notebook-open
|
||||
(ein:$notebook-url-or-port notebook)
|
||||
(ein:gat--path (ein:$notebook-notebook-path notebook)
|
||||
worktree-dir)
|
||||
(ein:$notebook-kernelspec notebook))
|
||||
(cd worktree-dir))))
|
||||
(append gat-chain-args
|
||||
(list "edit"
|
||||
(alet (ein:gat-elicit-worktree t)
|
||||
(setq ein:gat-previous-worktree ein:gat-current-worktree)
|
||||
(setq ein:gat-current-worktree it))))))
|
||||
(error "ein:gat-create: magit not installed"))
|
||||
(message "ein:gat-edit: not a notebook buffer"))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:gat-create (&optional _refresh)
|
||||
(interactive "P")
|
||||
(ein:gat-install-gat
|
||||
(if-let ((default-directory (ein:gat-where-am-i))
|
||||
(notebook (ein:get-notebook))
|
||||
(gat-chain-args `("gat" nil "--project" ,(ein:gat-project)
|
||||
"--region" ,(ein:gat-region) "--zone" " -")))
|
||||
(if (special-variable-p 'magit-process-popup-time)
|
||||
(let ((magit-process-popup-time 0))
|
||||
(ein:gat-chain
|
||||
(current-buffer)
|
||||
(cl-function
|
||||
(lambda (&rest args &key worktree-dir &allow-other-keys)
|
||||
(ein:notebook-open
|
||||
(ein:$notebook-url-or-port notebook)
|
||||
(ein:gat--path (ein:$notebook-notebook-path notebook)
|
||||
worktree-dir)
|
||||
(ein:$notebook-kernelspec notebook))))
|
||||
(append gat-chain-args
|
||||
(list "create"
|
||||
(alet (ein:gat-elicit-worktree nil)
|
||||
(setq ein:gat-previous-worktree ein:gat-current-worktree)
|
||||
(setq ein:gat-current-worktree it))))))
|
||||
(error "ein:gat-create: magit not installed"))
|
||||
(message "ein:gat-create: not a notebook buffer"))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:gat-run-local-batch (&optional refresh)
|
||||
(interactive "P")
|
||||
(ein:gat--run nil t refresh))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:gat-run-local (&optional refresh)
|
||||
(interactive "P")
|
||||
(ein:gat--run nil nil refresh))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:gat-run-remote-batch (&optional refresh)
|
||||
(interactive "P")
|
||||
(ein:gat--run t t refresh))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:gat-run-remote (&optional refresh)
|
||||
(interactive "P")
|
||||
(ein:gat--run t nil refresh))
|
||||
|
||||
(defun ein:gat-hash-password (raw-password)
|
||||
(let ((gat-hash-password-python
|
||||
(format "%s - <<EOF
|
||||
from notebook.auth import passwd
|
||||
print(passwd('%s', 'sha1'))
|
||||
EOF
|
||||
" ein:gat-python-command raw-password)))
|
||||
(ein:gat-shell-command gat-hash-password-python)))
|
||||
|
||||
(defun ein:gat-crib-password ()
|
||||
(let* ((gat-crib-password-python
|
||||
(format "%s - <<EOF
|
||||
from traitlets.config.application import Application
|
||||
from traitlets import Unicode
|
||||
class NotebookApp(Application):
|
||||
password = Unicode(u'', config=True,)
|
||||
|
||||
app = NotebookApp()
|
||||
app.load_config_file('jupyter_notebook_config.py', '~/.jupyter')
|
||||
print(app.password)
|
||||
EOF
|
||||
" ein:gat-python-command))
|
||||
(config-dir
|
||||
(elt (assoc-default
|
||||
'config
|
||||
(ein:json-read-from-string (ein:gat-shell-command "jupyter --paths --json")))
|
||||
0))
|
||||
(config-json (expand-file-name "jupyter_notebook_config.json" config-dir))
|
||||
(config-py (expand-file-name "jupyter_notebook_config.py" config-dir))
|
||||
result)
|
||||
(when (file-exists-p config-py)
|
||||
(setq result
|
||||
(awhen (ein:gat-shell-command gat-crib-password-python)
|
||||
(unless (zerop (length it)) it))))
|
||||
(unless (stringp result)
|
||||
(when (file-exists-p config-json)
|
||||
(-let* (((&alist 'NotebookApp (&alist 'password))
|
||||
(json-read-file config-json)))
|
||||
(setq result password))))
|
||||
result))
|
||||
|
||||
(defun ein:gat-kaggle-env (var json-key)
|
||||
(when-let ((val (or (getenv var)
|
||||
(let ((json (expand-file-name "kaggle.json" "~/.kaggle")))
|
||||
(when (file-exists-p json)
|
||||
(assoc-default json-key (json-read-file json)))))))
|
||||
(format "--env %s=%s" var val)))
|
||||
|
||||
(defun ein:gat--run (remote-p batch-p refresh)
|
||||
(unless with-editor-emacsclient-executable
|
||||
(error "Could not determine emacsclient"))
|
||||
(ein:gat-install-gat
|
||||
(-if-let* ((ipynb-name
|
||||
(if (string= major-mode "ein:ipynb-mode")
|
||||
(file-name-nondirectory (buffer-file-name))
|
||||
(awhen (aand (ein:get-notebook) (ein:$notebook-notebook-name it)) it)))
|
||||
(callback
|
||||
(if (string= major-mode "ein:ipynb-mode")
|
||||
(apply-partially (lambda (buffer*
|
||||
_notebook _created
|
||||
&rest _args)
|
||||
(when (buffer-live-p buffer*)
|
||||
(kill-buffer-if-not-modified buffer*)))
|
||||
(current-buffer))
|
||||
#'ignore))
|
||||
(default-directory (ein:gat-where-am-i))
|
||||
(password (if (or batch-p (not remote-p))
|
||||
""
|
||||
(or (ein:gat-crib-password)
|
||||
(let ((new-password
|
||||
(read-passwd "Enter new password for remote server [none]: " t)))
|
||||
(if (zerop (length new-password))
|
||||
new-password
|
||||
(let ((hashed (ein:gat-hash-password new-password)))
|
||||
(if (cl-search ":" hashed)
|
||||
hashed
|
||||
(prog1 nil
|
||||
(ein:log 'error "ein:gat--run: %s %s"
|
||||
"Could not hash" new-password)))))))))
|
||||
(gat-chain-args `("gat" nil
|
||||
"--project" ,(ein:gat-project)
|
||||
"--region" ,(ein:gat-region)
|
||||
"--zone" ,(ein:gat-zone)))
|
||||
(common-options (append '("--bespoke")
|
||||
'("--user" "root")
|
||||
'("--env" "GRANT_SUDO=1")
|
||||
(awhen (ein:gat-kaggle-env "KAGGLE_USERNAME" 'username)
|
||||
(split-string it))
|
||||
(awhen (ein:gat-kaggle-env "KAGGLE_KEY" 'key)
|
||||
(split-string it))
|
||||
(awhen (ein:gat-kaggle-env "KAGGLE_NULL" 'null)
|
||||
(split-string it))))
|
||||
(gat-chain-run (if remote-p
|
||||
(append '("run-remote")
|
||||
common-options
|
||||
`("--vendor" ,ein:gat-vendor)
|
||||
`("--machine" ,(ein:gat-elicit-machine))
|
||||
`(,@(when (string= (ein:gat-elicit-preemptible) "y")
|
||||
(list "--spot")))
|
||||
`(,@(awhen (ein:gat-elicit-disksizegb)
|
||||
(list "--disksizegb"
|
||||
(number-to-string it))))
|
||||
`(,@(when (string= ein:gat-vendor "aws")
|
||||
(list "--keyname"
|
||||
(ein:gat-elicit-keyname))))
|
||||
`(,@(-when-let* ((gce-p (string= ein:gat-vendor "gce"))
|
||||
(gpus (ein:gat-elicit-gpus))
|
||||
(nonzero (not (zerop gpus))))
|
||||
(list "--gpus"
|
||||
(number-to-string gpus)
|
||||
"--gpu"
|
||||
(ein:gat-elicit-gpu-type)))))
|
||||
(append '("run-local") common-options)))
|
||||
(now (truncate (float-time)))
|
||||
(gat-log-exec (append gat-chain-args
|
||||
(list "log" "--after" (format "%s" now)
|
||||
"--vendor" ein:gat-vendor
|
||||
"--until" "is running at:"
|
||||
"--nextunit" "shutdown.service")))
|
||||
(command (cond (batch-p
|
||||
(format "start.sh jupyter nbconvert --ExecutePreprocessor.timeout=21600 --to notebook --execute %s" ipynb-name))
|
||||
((zerop (length password))
|
||||
(format "start-notebook.sh --NotebookApp.token=''"))
|
||||
(t
|
||||
(format "start-notebook.sh --NotebookApp.password='%s'" password))))
|
||||
(last-known-buffer (current-buffer)))
|
||||
(progn
|
||||
(unless (or (file-directory-p
|
||||
(concat (file-name-as-directory default-directory) ".gat"))
|
||||
(member ".gat" (split-string default-directory "/")))
|
||||
(let* ((command (format "gat --project %s --region %s --zone %s create"
|
||||
(ein:gat-project) (ein:gat-region) (ein:gat-zone)))
|
||||
(retcode (shell-command command)))
|
||||
(unless (zerop retcode)
|
||||
(error "ein:gat-where-am-i: \"%s\" exited with %d" command retcode))))
|
||||
(cl-destructuring-bind (pre-docker . post-docker) (ein:gat-dockerfiles-state)
|
||||
(if (or refresh (null pre-docker))
|
||||
(if (fboundp 'magit-with-editor)
|
||||
(magit-with-editor
|
||||
(let* ((dockerfile (format "Dockerfile.%s" (file-name-sans-extension ipynb-name)))
|
||||
(base-image (ein:gat-elicit-base-image))
|
||||
(_ (with-temp-file dockerfile
|
||||
(insert (format "FROM %s\nCOPY --chown=jovyan:users ./%s .\n"
|
||||
base-image ipynb-name))))
|
||||
(my-editor (when (and (boundp 'server-name)
|
||||
(server-running-p server-name))
|
||||
`("-s" ,server-name))))
|
||||
(ein:gat-chain
|
||||
last-known-buffer
|
||||
(apply-partially
|
||||
#'ein:gat-chain
|
||||
last-known-buffer
|
||||
(when remote-p
|
||||
(apply-partially
|
||||
#'ein:gat-chain
|
||||
last-known-buffer
|
||||
(unless batch-p
|
||||
(apply-partially #'ein:gat-jupyter-login ipynb-name default-directory callback))
|
||||
gat-log-exec))
|
||||
(append gat-chain-args gat-chain-run (list "--dockerfile" dockerfile "--command" command)))
|
||||
`(,with-editor-emacsclient-executable nil ,@my-editor ,dockerfile))))
|
||||
(error "ein:gat--run: magit not installed"))
|
||||
(if (special-variable-p 'magit-process-popup-time)
|
||||
(let ((magit-process-popup-time 0))
|
||||
(ein:gat-chain
|
||||
last-known-buffer
|
||||
(when remote-p
|
||||
(apply-partially
|
||||
#'ein:gat-chain
|
||||
last-known-buffer
|
||||
(unless batch-p
|
||||
(apply-partially #'ein:gat-jupyter-login ipynb-name default-directory callback))
|
||||
gat-log-exec))
|
||||
(append gat-chain-args gat-chain-run (list "--dockerfile" pre-docker "--command" command))))
|
||||
(error "ein:gat--run: magit not installed")))))
|
||||
(message "ein:gat--run: aborting"))))
|
||||
|
||||
(defun ein:gat-elicit-base-image ()
|
||||
"Using a defcustom as HIST is suspect but pithy."
|
||||
(ein:completing-read
|
||||
"FROM image: " ein:gat-base-images nil 'confirm
|
||||
nil 'ein:gat-base-images (car ein:gat-base-images)))
|
||||
|
||||
(defun ein:gat-elicit-preemptible ()
|
||||
(interactive)
|
||||
(let ((kind (cl-case (intern ein:gat-vendor)
|
||||
(gce "Preemptible")
|
||||
(otherwise "Spot")))
|
||||
(default (or (car ein:gat-preemptible-history) "n")))
|
||||
(ein:completing-read
|
||||
(format "%s [%s]: " kind default)
|
||||
(split-string "y n")
|
||||
nil t nil
|
||||
'ein:gat-preemptible-history default)))
|
||||
|
||||
(defun ein:gat-elicit-keyname ()
|
||||
(interactive)
|
||||
(ein:completing-read
|
||||
(format "Keyname%s: " (aif (car ein:gat-keyname-history)
|
||||
(format " [%s]" it) ""))
|
||||
nil nil nil nil
|
||||
'ein:gat-keyname-history (car ein:gat-keyname-history)))
|
||||
|
||||
(defun ein:gat-elicit-machine ()
|
||||
(interactive)
|
||||
(let ((machine ""))
|
||||
(while (zerop (length machine))
|
||||
(setq machine (ein:completing-read
|
||||
(format "Machine Type%s: " (aif (car ein:gat-machine-history)
|
||||
(format " [%s]" it) ""))
|
||||
(append (seq-uniq ein:gat-machine-history)
|
||||
(seq-remove (lambda (x) (member x ein:gat-machine-history))
|
||||
(cl-copy-list (ein:gat-machine-types))))
|
||||
nil t nil 'ein:gat-machine-history
|
||||
(car ein:gat-machine-history))))
|
||||
machine))
|
||||
|
||||
(defun ein:gat-elicit-gpu-type ()
|
||||
(interactive)
|
||||
(let ((types ein:gat-gpu-types))
|
||||
(ein:completing-read
|
||||
(format "GPU%s: " (aif (car ein:gat-gpu-type-history)
|
||||
(format " [%s]" it) ""))
|
||||
(append (seq-uniq ein:gat-gpu-type-history)
|
||||
(seq-remove (lambda (x) (member x ein:gat-gpu-type-history))
|
||||
(cl-copy-list types)))
|
||||
nil t nil 'ein:gat-gpu-type-history
|
||||
(car (or ein:gat-gpu-type-history types)))))
|
||||
|
||||
(defun ein:gat-elicit-gpus ()
|
||||
(interactive)
|
||||
(cl-loop for answer =
|
||||
(string-to-number
|
||||
(ein:completing-read
|
||||
(format "Number GPUs%s: "
|
||||
(format " [%s]" (or (car ein:gat-gpus-history) "0")))
|
||||
'("0") nil nil nil
|
||||
'ein:gat-gpus-history (car ein:gat-gpus-history)))
|
||||
until (>= answer 0)
|
||||
finally return answer))
|
||||
|
||||
(defun ein:gat-elicit-worktree (extant)
|
||||
(let ((already (split-string
|
||||
(ein:gat-shell-command
|
||||
(format "gat --project %s --region %s --zone %s list"
|
||||
(ein:gat-project) (ein:gat-region) (ein:gat-zone))))))
|
||||
(if extant
|
||||
(ein:completing-read
|
||||
"Experiment: " already nil t nil nil
|
||||
ein:gat-previous-worktree)
|
||||
(read-string "New experiment: "))))
|
||||
|
||||
(defun ein:gat-elicit-disksizegb ()
|
||||
"Return nil for default [currently max(8, 6 + image size)]."
|
||||
(interactive)
|
||||
(cl-loop with answer
|
||||
do (setq answer (ein:completing-read
|
||||
(format "Disk GiB%s: "
|
||||
(format " [%s]"
|
||||
(or (car ein:gat-disksizegb-history)
|
||||
"default")))
|
||||
'("default") nil nil nil
|
||||
'ein:gat-disksizegb-history
|
||||
(car ein:gat-disksizegb-history)))
|
||||
if (string= answer "default")
|
||||
do (setq answer nil)
|
||||
else
|
||||
do (setq answer (string-to-number answer))
|
||||
end
|
||||
until (or (null answer) (> answer 0))
|
||||
finally return answer))
|
||||
|
||||
(defun ein:gat-dockerfiles-state ()
|
||||
"Return cons of (pre-Dockerfile . post-Dockerfile).
|
||||
Pre-Dockerfile is Dockerfile.<notebook> if extant, else Dockerfile."
|
||||
(-if-let* ((default-directory (ein:gat-where-am-i))
|
||||
(notebook-name (cond ((string= major-mode "ein:ipynb-mode")
|
||||
(file-name-nondirectory (buffer-file-name)))
|
||||
(t
|
||||
(aand (ein:get-notebook) (ein:$notebook-notebook-name it)))))
|
||||
(dockers (directory-files (file-name-as-directory default-directory)
|
||||
nil "^Dockerfile")))
|
||||
(let* ((pre-docker-p (lambda (f) (or (string= f (format "Dockerfile.%s" (file-name-sans-extension notebook-name)))
|
||||
(string= f "Dockerfile"))))
|
||||
(pre-docker (seq-find pre-docker-p (sort (cl-copy-list dockers) #'string>)))
|
||||
(post-docker-p (lambda (f) (string= f (format "%s.gat" pre-docker))))
|
||||
(post-docker (and (stringp pre-docker) (seq-find post-docker-p (sort (cl-copy-list dockers) #'string>)))))
|
||||
`(,pre-docker . ,post-docker))
|
||||
'(nil)))
|
||||
|
||||
(provide 'ein-gat)
|
||||
126
lisp/ein/ein-ipdb.el
Normal file
126
lisp/ein/ein-ipdb.el
Normal file
@@ -0,0 +1,126 @@
|
||||
;;; ein-ipdb.el --- Support ipython debugger (ipdb) -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2015 - John Miller
|
||||
|
||||
;; Author: John Miller <millejoh at mac.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-ipdb.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-ipdb.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-kernel.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
|
||||
(defvar *ein:ipdb-sessions* (make-hash-table)
|
||||
"Kernel Id to ein:$ipdb-session.")
|
||||
|
||||
(declare-function ein:kernel--get-msg "ein-kernel")
|
||||
|
||||
(cl-defstruct ein:$ipdb-session buffer kernel prompt notebook)
|
||||
|
||||
(defun ein:ipdb-get-session (kernel)
|
||||
(gethash (ein:$kernel-kernel-id kernel) *ein:ipdb-sessions*))
|
||||
|
||||
(defun ein:ipdb-start-session (kernel prompt notebook)
|
||||
(let* ((buffer (get-buffer-create
|
||||
(format "*ipdb: %s*"
|
||||
(ein:$kernel-kernel-id kernel))))
|
||||
(session (make-ein:$ipdb-session :buffer buffer
|
||||
:kernel kernel
|
||||
:prompt prompt
|
||||
:notebook notebook)))
|
||||
(puthash (ein:$kernel-kernel-id kernel) session *ein:ipdb-sessions*)
|
||||
(with-current-buffer buffer
|
||||
(kill-all-local-variables)
|
||||
(add-hook 'kill-buffer-hook
|
||||
(apply-partially #'ein:ipdb-quit-session session) nil t)
|
||||
(ein:ipdb-mode)
|
||||
(setq comint-use-prompt-regexp t)
|
||||
(setq comint-prompt-regexp (concat "^" (regexp-quote prompt)))
|
||||
(setq comint-input-sender (apply-partially #'ein:ipdb-input-sender session))
|
||||
(setq comint-prompt-read-only t)
|
||||
(set (make-local-variable 'comint-output-filter-functions)
|
||||
'(ansi-color-process-output))
|
||||
(let ((proc (start-process "ein:ipdb" buffer "cat"))
|
||||
(sentinel (lambda (process _event)
|
||||
(when (memq (process-status process) '(exit signal))
|
||||
(ein:ipdb-cleanup-session session)))))
|
||||
(set-process-query-on-exit-flag proc nil)
|
||||
(set-process-sentinel proc sentinel)
|
||||
(set-marker (process-mark proc) (point))
|
||||
(comint-output-filter proc (concat "\n" (ein:$ipdb-session-prompt session)))))
|
||||
(pop-to-buffer buffer)))
|
||||
|
||||
(defun ein:ipdb-quit-session (session)
|
||||
(let* ((kernel (ein:$ipdb-session-kernel session))
|
||||
(msg (ein:kernel--get-msg kernel "input_reply" (list :value "exit"))))
|
||||
(ein:websocket-send-stdin-channel kernel msg)))
|
||||
|
||||
(defun ein:ipdb-stop-session (session)
|
||||
(awhen (get-buffer-process (ein:$ipdb-session-buffer session))
|
||||
(when (process-live-p it)
|
||||
(kill-process it))))
|
||||
|
||||
(defun ein:ipdb-cleanup-session (session)
|
||||
(let ((kernel (ein:$ipdb-session-kernel session))
|
||||
(notebook (ein:$ipdb-session-notebook session))
|
||||
(buffer (ein:$ipdb-session-buffer session)))
|
||||
(remhash (ein:$kernel-kernel-id kernel) *ein:ipdb-sessions*)
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(insert "\nFinished\n")))
|
||||
(awhen (ein:notebook-buffer notebook)
|
||||
(when (buffer-live-p it)
|
||||
(pop-to-buffer it)))))
|
||||
|
||||
(defun ein:ipdb--handle-iopub-reply (kernel packet)
|
||||
(cl-destructuring-bind
|
||||
(&key content &allow-other-keys)
|
||||
(ein:json-read-from-string packet)
|
||||
(-when-let* ((session (ein:ipdb-get-session kernel))
|
||||
(buffer (ein:$ipdb-session-buffer session))
|
||||
(prompt (ein:$ipdb-session-prompt session))
|
||||
(proc (get-buffer-process buffer))
|
||||
(proc-live-p (process-live-p proc)))
|
||||
(let ((text (plist-get content :text))
|
||||
(ename (plist-get content :ename)))
|
||||
(when (stringp text)
|
||||
(comint-output-filter proc text))
|
||||
(if (and (stringp ename) (string-match-p "bdbquit" ename))
|
||||
(ein:ipdb-stop-session session)
|
||||
(comint-output-filter proc prompt))))))
|
||||
|
||||
(defun ein:ipdb-input-sender (session proc input)
|
||||
;; in case of eof, comint-input-sender-no-newline is t
|
||||
(if comint-input-sender-no-newline
|
||||
(ein:ipdb-quit-session session)
|
||||
(when (process-live-p proc)
|
||||
(with-current-buffer (process-buffer proc)
|
||||
(let* ((buffer-read-only nil)
|
||||
(kernel (ein:$ipdb-session-kernel session))
|
||||
(content (list :value input))
|
||||
(msg (ein:kernel--get-msg kernel "input_reply" content)))
|
||||
(ein:websocket-send-stdin-channel kernel msg))))))
|
||||
|
||||
(define-derived-mode ein:ipdb-mode comint-mode "ein:debugger"
|
||||
"Run an EIN debug session.
|
||||
|
||||
\\<ein:ipdb-mode-map>")
|
||||
|
||||
(provide 'ein-ipdb)
|
||||
81
lisp/ein/ein-ipynb-mode.el
Normal file
81
lisp/ein/ein-ipynb-mode.el
Normal file
@@ -0,0 +1,81 @@
|
||||
;;; ein-ipynb-mode.el --- A simple mode for ipynb file -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012 Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-ipynb-mode.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-ipynb-mode.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-ipynb-mode.el.
|
||||
;; If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein-process)
|
||||
(require 'js)
|
||||
|
||||
;;;###autoload
|
||||
(define-derived-mode ein:ipynb-mode js-mode "ein:ipynb"
|
||||
"A simple mode for ipynb file.
|
||||
|
||||
\\{ein:ipynb-mode-map}
|
||||
"
|
||||
:group 'ein
|
||||
:after-hook
|
||||
(let* ((filename (file-name-nondirectory buffer-file-truename))
|
||||
(remote-filename (concat (file-name-as-directory "run-remote") filename)))
|
||||
;; fragile hack to refresh s3fuse
|
||||
(call-process "cat" nil nil nil remote-filename)
|
||||
(when (and (file-readable-p remote-filename)
|
||||
(file-newer-than-file-p remote-filename filename)
|
||||
(prog1
|
||||
(let ((inhibit-quit t))
|
||||
(prog1
|
||||
(with-local-quit
|
||||
(y-or-n-p "Corresponding run-remote is newer. Replace? (will first backup) "))
|
||||
(setq quit-flag nil)))
|
||||
(message "")))
|
||||
(if-let ((make-backup-files t)
|
||||
(where-to (funcall make-backup-file-name-function buffer-file-name)))
|
||||
(let* (backup-inhibited
|
||||
(orig-hooks find-file-hook)
|
||||
(reassure (lambda ()
|
||||
(message "Backed up to %s" where-to)
|
||||
(setq find-file-hook orig-hooks))))
|
||||
(backup-buffer)
|
||||
(copy-file remote-filename filename t t)
|
||||
(add-hook 'find-file-hook reassure nil)
|
||||
(find-file-noselect filename t))
|
||||
(message "Backup failed. Not replaced")))))
|
||||
|
||||
(let ((map ein:ipynb-mode-map))
|
||||
(set-keymap-parent map js-mode-map)
|
||||
(define-key map "\C-c\C-z" 'ein:process-find-file-callback)
|
||||
(define-key map "\C-c\C-o" 'ein:process-find-file-callback)
|
||||
(define-key map "\C-c\C-r" 'ein:gat-run-remote)
|
||||
(easy-menu-define ein:ipynb-menu map "EIN IPyNB Mode Menu"
|
||||
`("EIN IPyNB File"
|
||||
,@(ein:generate-menu
|
||||
'(("Open notebook" ein:process-find-file-callback))))))
|
||||
|
||||
;;;###autoload
|
||||
(add-to-list 'auto-mode-alist '("\\.ipynb\\'" . ein:ipynb-mode))
|
||||
|
||||
(provide 'ein-ipynb-mode)
|
||||
|
||||
;;; ein-ipynb-mode.el ends here
|
||||
435
lisp/ein/ein-jupyter.el
Normal file
435
lisp/ein/ein-jupyter.el
Normal file
@@ -0,0 +1,435 @@
|
||||
;;; ein-jupyter.el --- Manage the jupyter notebook server -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2017 John M. Miller
|
||||
|
||||
;; Authors: John M. Miller <millejoh at mac.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-jupyter.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-jupyter.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-jupyter.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein-core)
|
||||
(require 'ein-notebooklist)
|
||||
(require 'ein-dev)
|
||||
(require 'exec-path-from-shell nil t)
|
||||
(autoload 'ein:gat-chain "ein-gat")
|
||||
(autoload 'ein:gat-project "ein-gat")
|
||||
(autoload 'ein:gat-region "ein-gat")
|
||||
(autoload 'ein:gat-zone "ein-gat")
|
||||
|
||||
(defcustom ein:jupyter-use-containers nil
|
||||
"Take EIN in a different direcsh."
|
||||
:group 'ein
|
||||
:type 'boolean)
|
||||
|
||||
(defcustom ein:jupyter-docker-image "jupyter/datascience-notebook"
|
||||
"Docker pull whichever jupyter image you prefer. This defaults to
|
||||
the `jupyter docker stacks` on hub.docker.com.
|
||||
|
||||
Optionally append ':tag', e.g., ':latest' in the customary way."
|
||||
:group 'ein
|
||||
:type 'string)
|
||||
|
||||
(defcustom ein:jupyter-docker-mount-point "/home/jovyan/ipynb"
|
||||
"Where in docker image to mount `ein:jupyter-default-notebook-directory'."
|
||||
:group 'ein
|
||||
:type 'string)
|
||||
|
||||
(defcustom ein:jupyter-docker-additional-switches "-e JUPYTER_ENABLE_LAB=no --rm"
|
||||
"Additional options to the `docker run` call.
|
||||
|
||||
Note some options like '-v' and '-network' are imposed by EIN."
|
||||
:group 'ein
|
||||
:type 'string)
|
||||
|
||||
(defcustom ein:jupyter-cannot-find-jupyter nil
|
||||
"Use purcell's `exec-path-from-shell'"
|
||||
:group 'ein
|
||||
:type 'boolean)
|
||||
|
||||
(defcustom ein:jupyter-server-command "jupyter"
|
||||
"The default command to start a jupyter notebook server.
|
||||
Changing this to `jupyter-notebook' requires customizing
|
||||
`ein:jupyter-server-use-subcommand' to nil."
|
||||
:group 'ein
|
||||
:type 'string
|
||||
:set-after '(ein:jupyter-cannot-find-jupyter)
|
||||
:set (lambda (symbol value)
|
||||
(set-default symbol value)
|
||||
(when (and (featurep 'exec-path-from-shell)
|
||||
ein:jupyter-cannot-find-jupyter
|
||||
(memq window-system '(mac ns x)))
|
||||
(eval `(let (,@(when (boundp 'exec-path-from-shell-check-startup-files)
|
||||
(list 'exec-path-from-shell-check-startup-files)))
|
||||
(exec-path-from-shell-initialize))))))
|
||||
|
||||
(defcustom ein:jupyter-default-server-command ein:jupyter-server-command
|
||||
"Obsolete alias for `ein:jupyter-server-command'"
|
||||
:group 'ein
|
||||
:type 'string
|
||||
:set-after '(ein:jupyter-server-command)
|
||||
:set (lambda (symbol value)
|
||||
(set-default symbol value)
|
||||
(set-default 'ein:jupyter-server-command value)))
|
||||
|
||||
;;;###autoload
|
||||
(defcustom ein:jupyter-server-use-subcommand "notebook"
|
||||
"For JupyterLab 3.0+ change the subcommand to \"server\".
|
||||
Users of \"jupyter-notebook\" (as opposed to \"jupyter notebook\") select Omit."
|
||||
:group 'ein
|
||||
:type '(choice (string :tag "Subcommand" "notebook")
|
||||
(const :tag "Omit" nil)))
|
||||
|
||||
(defcustom ein:jupyter-server-args '("--no-browser")
|
||||
"Add any additional command line options you wish to include
|
||||
with the call to the jupyter notebook."
|
||||
:group 'ein
|
||||
:type '(repeat string))
|
||||
|
||||
(defcustom ein:jupyter-default-notebook-directory nil
|
||||
"Default location of ipynb files."
|
||||
:group 'ein
|
||||
:type 'directory)
|
||||
|
||||
(defcustom ein:jupyter-default-kernel 'first-alphabetically
|
||||
"With which of ${XDG_DATA_HOME}/jupyter/kernels to create new notebooks."
|
||||
:group 'ein
|
||||
:type (append
|
||||
'(choice (other :tag "First alphabetically" first-alphabetically))
|
||||
(condition-case err
|
||||
(mapcar
|
||||
(lambda (x) `(const :tag ,(cdr x) ,(car x)))
|
||||
(cl-loop
|
||||
for (k . spec) in
|
||||
(alist-get
|
||||
'kernelspecs
|
||||
(let ((json-object-type 'alist))
|
||||
(json-read-from-string ;; intentionally not ein:json-read-from-string
|
||||
(shell-command-to-string
|
||||
(format "2>/dev/null %s kernelspec list --json"
|
||||
ein:jupyter-server-command)))))
|
||||
collect `(,k . ,(alist-get 'display_name (alist-get 'spec spec)))))
|
||||
(error (ein:log 'warn "ein:jupyter-default-kernel: %s" err)
|
||||
'((string :tag "Ask"))))))
|
||||
|
||||
(defconst *ein:jupyter-server-process-name* "ein server")
|
||||
(defconst *ein:jupyter-server-buffer-name*
|
||||
(format "*%s*" *ein:jupyter-server-process-name*))
|
||||
(defvar-local ein:jupyter-server-notebook-directory nil
|
||||
"Keep track of prevailing --notebook-dir argument.")
|
||||
|
||||
(defun ein:jupyter-running-notebook-directory ()
|
||||
(when (ein:jupyter-server-process)
|
||||
(buffer-local-value 'ein:jupyter-server-notebook-directory
|
||||
(get-buffer *ein:jupyter-server-buffer-name*))))
|
||||
|
||||
(defun ein:jupyter-get-default-kernel (kernels)
|
||||
(cond (ein:%notebooklist-new-kernel%
|
||||
(ein:$kernelspec-name ein:%notebooklist-new-kernel%))
|
||||
((eq ein:jupyter-default-kernel 'first-alphabetically)
|
||||
(car (car kernels)))
|
||||
((stringp ein:jupyter-default-kernel)
|
||||
ein:jupyter-default-kernel)
|
||||
(t
|
||||
(symbol-name ein:jupyter-default-kernel))))
|
||||
|
||||
(defun ein:jupyter-process-lines (_url-or-port command &rest args)
|
||||
"If URL-OR-PORT registered as a k8s url, preface COMMAND ARGS
|
||||
with `kubectl exec'."
|
||||
(if-let ((found (executable-find command)))
|
||||
(with-temp-buffer
|
||||
(let ((status (apply #'call-process found nil t nil args)))
|
||||
(if (zerop status)
|
||||
(progn
|
||||
(goto-char (point-min))
|
||||
(let (lines)
|
||||
(while (not (eobp))
|
||||
(setq lines (cons (buffer-substring-no-properties
|
||||
(line-beginning-position)
|
||||
(line-end-position))
|
||||
lines))
|
||||
(forward-line 1))
|
||||
(nreverse lines)))
|
||||
(prog1 nil
|
||||
(ein:log 'warn "ein:jupyter-process-lines: '%s %s' returned %s"
|
||||
found (ein:join-str " " args) status)))))
|
||||
(prog1 nil
|
||||
(ein:log 'warn "ein:jupyter-process-lines: cannot find %s" command))))
|
||||
|
||||
(defsubst ein:jupyter-server-process ()
|
||||
"Return the emacs process object of our session."
|
||||
(get-buffer-process (get-buffer *ein:jupyter-server-buffer-name*)))
|
||||
|
||||
(defun ein:jupyter-server--run (buf user-cmd dir &optional args)
|
||||
(get-buffer-create buf)
|
||||
(let* ((cmd (if ein:jupyter-use-containers "docker" user-cmd))
|
||||
(vargs (cond (ein:jupyter-use-containers
|
||||
(split-string
|
||||
(format "run --network host -v %s:%s %s %s"
|
||||
dir
|
||||
ein:jupyter-docker-mount-point
|
||||
ein:jupyter-docker-additional-switches
|
||||
ein:jupyter-docker-image)))
|
||||
(t
|
||||
(append (split-string (or ein:jupyter-server-use-subcommand ""))
|
||||
(when dir
|
||||
(list (format "--notebook-dir=%s"
|
||||
(convert-standard-filename dir))))
|
||||
args
|
||||
(let ((copy (cl-copy-list ein:jupyter-server-args)))
|
||||
(when (ein:debug-p)
|
||||
(cl-pushnew "--debug" copy :test #'equal))
|
||||
copy)))))
|
||||
(proc (apply #'start-process
|
||||
*ein:jupyter-server-process-name* buf cmd vargs)))
|
||||
(ein:log 'info "ein:jupyter-server--run: %s %s" cmd (ein:join-str " " vargs))
|
||||
(set-process-query-on-exit-flag proc nil)
|
||||
proc))
|
||||
|
||||
(defun ein:jupyter-my-url-or-port ()
|
||||
(when-let ((my-pid (aand (ein:jupyter-server-process) (process-id it))))
|
||||
(catch 'done
|
||||
(dolist (json (ein:jupyter-crib-running-servers))
|
||||
(cl-destructuring-bind (&key pid url &allow-other-keys)
|
||||
json
|
||||
(when (equal my-pid pid)
|
||||
(throw 'done (ein:url url))))))))
|
||||
|
||||
(defun ein:jupyter-server-ready-p ()
|
||||
(when (ein:jupyter-server-process)
|
||||
(with-current-buffer *ein:jupyter-server-buffer-name*
|
||||
(save-excursion
|
||||
(goto-char (point-max))
|
||||
(re-search-backward (format "Process %s" *ein:jupyter-server-process-name*)
|
||||
nil "") ;; important if we start-stop-start
|
||||
(re-search-forward
|
||||
"\\([[:alnum:]]+\\) is\\( now\\)? running"
|
||||
nil t)))))
|
||||
|
||||
(defun ein:jupyter-server-login-and-open (url-or-port &optional callback)
|
||||
"Log in and open a notebooklist buffer for a running jupyter notebook server.
|
||||
|
||||
Determine if there is a running jupyter server (started via a
|
||||
call to `ein:jupyter-server-start') and then try to guess if
|
||||
token authentication is enabled. If a token is found use it to
|
||||
generate a call to `ein:notebooklist-login' and once
|
||||
authenticated open the notebooklist buffer via a call to
|
||||
`ein:notebooklist-open'."
|
||||
(if-let ((token (ein:notebooklist-token-or-password url-or-port)))
|
||||
(ein:notebooklist-login url-or-port callback nil nil token)
|
||||
(ein:log 'error "`(ein:notebooklist-token-or-password %s)` must return non-nil"
|
||||
url-or-port)))
|
||||
|
||||
(defsubst ein:set-process-sentinel (proc url-or-port)
|
||||
"URL-OR-PORT might get redirected.
|
||||
This is currently only the case for jupyterhub. Once login
|
||||
handshake provides the new URL-OR-PORT, we set various state as
|
||||
pertains our singleton jupyter server process here."
|
||||
|
||||
;; Would have used `add-function' if it didn't produce gv-ref warnings.
|
||||
(set-process-sentinel
|
||||
proc
|
||||
(apply-partially (lambda (url-or-port* sentinel proc* event)
|
||||
(aif sentinel (funcall it proc* event))
|
||||
(funcall #'ein:notebooklist-sentinel url-or-port* proc* event))
|
||||
url-or-port (process-sentinel proc))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:jupyter-crib-token (url-or-port)
|
||||
"Shell out to jupyter for its credentials knowledge. Return list
|
||||
of (PASSWORD TOKEN)."
|
||||
(aif (cl-loop for line in
|
||||
(apply #'ein:jupyter-process-lines url-or-port
|
||||
ein:jupyter-server-command
|
||||
(append
|
||||
(split-string (or ein:jupyter-server-use-subcommand ""))
|
||||
'("list" "--json")))
|
||||
with token0
|
||||
with password0
|
||||
when (cl-destructuring-bind
|
||||
(&key password url token &allow-other-keys)
|
||||
(ein:json-read-from-string line)
|
||||
(prog1 (equal (ein:url url) url-or-port)
|
||||
(setq password0 password) ;; t or :json-false
|
||||
(setq token0 token)))
|
||||
return (list password0 token0))
|
||||
it (list nil nil)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:jupyter-crib-running-servers ()
|
||||
"Shell out to jupyter for running servers."
|
||||
(cl-loop for line in
|
||||
(apply #'ein:jupyter-process-lines nil
|
||||
ein:jupyter-server-command
|
||||
(append
|
||||
(split-string (or ein:jupyter-server-use-subcommand ""))
|
||||
'("list" "--json")))
|
||||
collecting (ein:json-read-from-string line)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:jupyter-server-start (server-command
|
||||
notebook-directory
|
||||
&optional no-login-p login-callback port)
|
||||
"Start SERVER-COMMAND with `--notebook-dir' NOTEBOOK-DIRECTORY.
|
||||
|
||||
Login after connection established unless NO-LOGIN-P is set.
|
||||
LOGIN-CALLBACK takes two arguments, the buffer created by
|
||||
`ein:notebooklist-open--finish', and the url-or-port argument
|
||||
of `ein:notebooklist-open*'.
|
||||
|
||||
With \\[universal-argument] prefix arg, prompt the user for the
|
||||
server command."
|
||||
(interactive
|
||||
(list (let ((default-command (executable-find ein:jupyter-server-command)))
|
||||
(if (and (not ein:jupyter-use-containers)
|
||||
(or current-prefix-arg (not default-command)))
|
||||
(let (command result)
|
||||
(while (not (setq
|
||||
result
|
||||
(executable-find
|
||||
(setq
|
||||
command
|
||||
(read-string
|
||||
(format
|
||||
"%sServer command: "
|
||||
(if command
|
||||
(format "[%s not executable] " command)
|
||||
""))
|
||||
nil nil ein:jupyter-server-command))))))
|
||||
result)
|
||||
default-command))
|
||||
(let ((default-dir ein:jupyter-default-notebook-directory)
|
||||
result)
|
||||
(while (or (not result) (not (file-directory-p result)))
|
||||
(setq result (read-directory-name
|
||||
(format "%sNotebook directory: "
|
||||
(if result
|
||||
(format "[%s not a directory]" result)
|
||||
""))
|
||||
default-dir default-dir t)))
|
||||
result)
|
||||
nil
|
||||
(lambda (buffer _url-or-port)
|
||||
(pop-to-buffer buffer))
|
||||
nil))
|
||||
(when (ein:jupyter-server-process)
|
||||
(error "ein:jupyter-server-start: First `M-x ein:stop'"))
|
||||
(let ((proc (ein:jupyter-server--run *ein:jupyter-server-buffer-name*
|
||||
server-command
|
||||
notebook-directory
|
||||
(when (numberp port)
|
||||
`("--port" ,(format "%s" port)
|
||||
"--port-retries" "0")))))
|
||||
(cl-loop repeat 30
|
||||
until (ein:jupyter-server-ready-p)
|
||||
do (sleep-for 0 500)
|
||||
finally do
|
||||
(if-let ((buffer (get-buffer *ein:jupyter-server-buffer-name*))
|
||||
(url-or-port (ein:jupyter-my-url-or-port)))
|
||||
(with-current-buffer buffer
|
||||
(setq ein:jupyter-server-notebook-directory
|
||||
(convert-standard-filename notebook-directory))
|
||||
(add-hook 'kill-buffer-query-functions
|
||||
(lambda () (or (not (ein:jupyter-server-process))
|
||||
(ein:jupyter-server-stop t url-or-port)))
|
||||
nil t))
|
||||
(ein:log 'warn "Jupyter server failed to start, cancelling operation")))
|
||||
(when-let ((login-p (not no-login-p))
|
||||
(url-or-port (ein:jupyter-my-url-or-port)))
|
||||
(unless login-callback
|
||||
(setq login-callback #'ignore))
|
||||
(add-function :after (var login-callback)
|
||||
(apply-partially (lambda (proc* _buffer url-or-port)
|
||||
(ein:set-process-sentinel proc* url-or-port))
|
||||
proc))
|
||||
(ein:jupyter-server-login-and-open
|
||||
url-or-port
|
||||
login-callback))))
|
||||
|
||||
;;;###autoload
|
||||
(defalias 'ein:run 'ein:jupyter-server-start)
|
||||
|
||||
;;;###autoload
|
||||
(defalias 'ein:stop 'ein:jupyter-server-stop)
|
||||
|
||||
(defvar ein:gat-urls)
|
||||
(defvar ein:gat-aws-region)
|
||||
;;;###autoload
|
||||
(defun ein:jupyter-server-stop (&optional ask-p url-or-port)
|
||||
(interactive
|
||||
(list t (awhen (ein:get-notebook)
|
||||
(ein:$notebook-url-or-port it))))
|
||||
(let ((my-url-or-port (ein:jupyter-my-url-or-port))
|
||||
(all-p t))
|
||||
(dolist (url-or-port
|
||||
(if url-or-port (list url-or-port) (ein:notebooklist-keys))
|
||||
(prog1 all-p
|
||||
(when (and (null (ein:notebooklist-keys))
|
||||
(ein:shared-output-healthy-p))
|
||||
(kill-buffer (ein:shared-output-buffer)))))
|
||||
(let* ((gat-dir (alist-get (intern url-or-port)
|
||||
(awhen (bound-and-true-p ein:gat-urls) it)))
|
||||
(my-p (string= url-or-port my-url-or-port))
|
||||
(close-p (or (not ask-p)
|
||||
(prog1 (y-or-n-p (format "Close %s?" url-or-port))
|
||||
(message "")))))
|
||||
(if (not close-p)
|
||||
(setq all-p nil)
|
||||
(ein:notebook-close-notebooks
|
||||
(lambda (notebook)
|
||||
(string= url-or-port (ein:$notebook-url-or-port notebook)))
|
||||
t)
|
||||
(cl-loop repeat 10
|
||||
until (null (seq-some (lambda (proc)
|
||||
(cl-search "request curl"
|
||||
(process-name proc)))
|
||||
(process-list)))
|
||||
do (sleep-for 0 500))
|
||||
(cond (my-p
|
||||
(-when-let* ((proc (ein:jupyter-server-process))
|
||||
(pid (process-id proc)))
|
||||
(run-at-time 2 nil
|
||||
(lambda ()
|
||||
(signal-process pid (if (eq system-type 'windows-nt) 9 15))))
|
||||
;; NotebookPasswordApp::shutdown_server() also ignores req response.
|
||||
(ein:query-singleton-ajax (ein:url url-or-port "api/shutdown")
|
||||
:type "POST")))
|
||||
(gat-dir
|
||||
(with-current-buffer (ein:notebooklist-get-buffer url-or-port)
|
||||
(-when-let* ((gat-chain-args `("gat" nil
|
||||
"--project" ,(ein:gat-project)
|
||||
"--region" ,(ein:gat-region)
|
||||
"--zone" ,(ein:gat-zone)))
|
||||
(now (truncate (float-time)))
|
||||
(gat-log-exec (append gat-chain-args
|
||||
(list "log" "--after" (format "%s" now)
|
||||
"--vendor" (aif (bound-and-true-p ein:gat-vendor) it "aws")
|
||||
"--nextunit" "shutdown.service")))
|
||||
(magit-process-popup-time 0))
|
||||
(ein:gat-chain (current-buffer) nil gat-log-exec :notebook-dir gat-dir)
|
||||
;; NotebookPasswordApp::shutdown_server() also ignores req response.
|
||||
(ein:query-singleton-ajax (ein:url url-or-port "api/shutdown")
|
||||
:type "POST")))))
|
||||
;; `ein:notebooklist-sentinel' frequently does not trigger
|
||||
(ein:notebooklist-list-remove url-or-port)
|
||||
(maphash (lambda (k _v) (when (equal (car k) url-or-port)
|
||||
(remhash k *ein:notebook--pending-query*)))
|
||||
*ein:notebook--pending-query*)
|
||||
(kill-buffer (ein:notebooklist-get-buffer url-or-port)))))))
|
||||
|
||||
(provide 'ein-jupyter)
|
||||
612
lisp/ein/ein-kernel.el
Normal file
612
lisp/ein/ein-kernel.el
Normal file
@@ -0,0 +1,612 @@
|
||||
;;; ein-kernel.el --- Communicate with IPython notebook server -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-kernel.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-kernel.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-kernel.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
;; `ein:kernel' is the proxy class of notebook server state.
|
||||
;; It agglomerates both the "kernel" and "session" objects of server described here
|
||||
;; https://github.com/jupyter/jupyter/wiki/Jupyter-Notebook-Server-API
|
||||
;; It may have been better to keep them separate to allow parallel reasoning with
|
||||
;; the notebook server, but that time is past.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ansi-color)
|
||||
|
||||
(require 'ein-core)
|
||||
(require 'ein-classes)
|
||||
(require 'ein-log)
|
||||
(require 'ein-websocket)
|
||||
(require 'ein-events)
|
||||
(require 'ein-query)
|
||||
(require 'ein-ipdb)
|
||||
|
||||
(declare-function ein:notebook-get-opened-notebook "ein-notebook")
|
||||
(declare-function ein:notebooklist-get-buffer "ein-notebooklist")
|
||||
(declare-function ein:notebooklist-reload "ein-notebooklist")
|
||||
|
||||
(defun ein:$kernel-session-url (kernel)
|
||||
(concat "/api/sessions/" (ein:$kernel-session-id kernel)))
|
||||
|
||||
;;;###autoload
|
||||
(defalias 'ein:kernel-url-or-port 'ein:$kernel-url-or-port)
|
||||
|
||||
;;;###autoload
|
||||
(defalias 'ein:kernel-id 'ein:$kernel-kernel-id)
|
||||
|
||||
(make-obsolete-variable 'ein:pre-kernel-execute-functions nil "0.17.0")
|
||||
(make-obsolete-variable 'ein:on-shell-reply-functions nil "0.17.0")
|
||||
(make-obsolete-variable 'ein:on-kernel-connect-functions nil "0.17.0")
|
||||
|
||||
(defun ein:kernel-new (url-or-port path kernelspec base-url events &optional api-version)
|
||||
(make-ein:$kernel
|
||||
:url-or-port url-or-port
|
||||
:path path
|
||||
:kernelspec kernelspec
|
||||
:events events
|
||||
:api-version (or api-version 5)
|
||||
:session-id (ein:utils-uuid)
|
||||
:kernel-id nil
|
||||
:websocket nil
|
||||
:base-url base-url
|
||||
:oinfo-cache (make-hash-table :test #'equal)
|
||||
:username "username"
|
||||
:msg-callbacks (make-hash-table :test 'equal)))
|
||||
|
||||
(defun ein:kernel-del (kernel)
|
||||
"Destructor for `ein:$kernel'."
|
||||
(ein:kernel-disconnect kernel))
|
||||
|
||||
(defun ein:kernel--get-msg (kernel msg-type content)
|
||||
(list
|
||||
:header (list
|
||||
:msg_id (ein:utils-uuid)
|
||||
:username (ein:$kernel-username kernel)
|
||||
:session (ein:$kernel-session-id kernel)
|
||||
:version "5.0"
|
||||
:date (format-time-string "%Y-%m-%dT%T" (current-time)) ; ISO 8601 timestamp
|
||||
:msg_type msg-type)
|
||||
:metadata (make-hash-table)
|
||||
:content content
|
||||
:parent_header (make-hash-table)))
|
||||
|
||||
(cl-defun ein:kernel-session-p (kernel callback &optional iteration)
|
||||
"Don't make any changes on the server side. CALLBACK with arity
|
||||
2, kernel and a boolean whether session exists on server."
|
||||
(unless iteration
|
||||
(setq iteration 0))
|
||||
(let ((session-id (ein:$kernel-session-id kernel)))
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url (ein:$kernel-url-or-port kernel) "api/sessions" session-id)
|
||||
:type "GET"
|
||||
:parser #'ein:json-read
|
||||
:complete (apply-partially #'ein:kernel-session-p--complete session-id)
|
||||
:success (apply-partially #'ein:kernel-session-p--success kernel session-id callback)
|
||||
:error (apply-partially #'ein:kernel-session-p--error kernel callback iteration))))
|
||||
|
||||
(cl-defun ein:kernel-session-p--complete (_session-id
|
||||
&key data response
|
||||
&allow-other-keys
|
||||
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
|
||||
(ein:log 'debug "ein:kernel-session-p--complete %s" resp-string))
|
||||
|
||||
(cl-defun ein:kernel-session-p--error (kernel callback iteration
|
||||
&key error-thrown _symbol-status data
|
||||
&allow-other-keys)
|
||||
(if (ein:aand (plist-get data :message) (cl-search "not found" it))
|
||||
(when callback (funcall callback kernel nil))
|
||||
(let* ((max-tries 3)
|
||||
(tries-left (1- (- max-tries iteration))))
|
||||
(ein:log 'verbose "ein:kernel-session-p--error [%s], %s tries left"
|
||||
(car error-thrown) tries-left)
|
||||
(if (> tries-left 0)
|
||||
(ein:kernel-session-p kernel callback (1+ iteration))))))
|
||||
|
||||
(cl-defun ein:kernel-session-p--success (kernel session-id callback
|
||||
&key data &allow-other-keys)
|
||||
(let ((session-p (equal (plist-get data :id) session-id)))
|
||||
(ein:log 'verbose "ein:kernel-session-p--success: session-id=%s session-p=%s"
|
||||
session-id session-p)
|
||||
(when callback (funcall callback kernel session-p))))
|
||||
|
||||
(cl-defun ein:kernel-restart-session (kernel)
|
||||
"Server side delete of KERNEL session and subsequent restart with all new state"
|
||||
(ein:kernel-delete-session
|
||||
(lambda (kernel)
|
||||
(ein:events-trigger (ein:$kernel-events kernel) 'status_restarting.Kernel)
|
||||
(ein:kernel-retrieve-session kernel 0
|
||||
(lambda (kernel)
|
||||
(ein:events-trigger (ein:$kernel-events kernel)
|
||||
'status_restarted.Kernel))))
|
||||
:kernel kernel))
|
||||
|
||||
(cl-defun ein:kernel-retrieve-session (kernel &optional iteration callback)
|
||||
"Formerly ein:kernel-start, but that was a misnomer.
|
||||
|
||||
The server 1. really starts a session (and an accompanying
|
||||
kernel), and 2. may not even start a session if one exists for
|
||||
the same path.
|
||||
|
||||
If picking up from where we last left off, that is, we restart
|
||||
emacs and reconnect to same server, jupyter will hand us back the
|
||||
original, still running session.
|
||||
|
||||
CALLBACK of arity 1, the kernel."
|
||||
;; The server logic is here (could not find other documentation)
|
||||
;; https://github.com/jupyter/notebook/blob/04a686dbaf9dfe553324a03cb9e6f778cf1e3da1/notebook/services/sessions/handlers.py#L56-L81
|
||||
(unless iteration
|
||||
(setq iteration 0))
|
||||
(if (<= (ein:$kernel-api-version kernel) 2)
|
||||
(error "Api %s unsupported" (ein:$kernel-api-version kernel))
|
||||
(let ((kernel-id (ein:$kernel-kernel-id kernel))
|
||||
(kernelspec (ein:$kernel-kernelspec kernel))
|
||||
(path (ein:$kernel-path kernel)))
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url (ein:$kernel-url-or-port kernel) "api/sessions")
|
||||
:type "POST"
|
||||
:data (ein:json-encode
|
||||
`((path . ,path)
|
||||
(type . "notebook")
|
||||
,@(if kernelspec
|
||||
`((kernel .
|
||||
((name . ,(ein:$kernelspec-name kernelspec))
|
||||
,@(if kernel-id
|
||||
`((id . ,kernel-id)))))))))
|
||||
:parser #'ein:json-read
|
||||
:complete (apply-partially #'ein:kernel-retrieve-session--complete kernel callback)
|
||||
:success (apply-partially #'ein:kernel-retrieve-session--success kernel callback)
|
||||
:error (apply-partially #'ein:kernel-retrieve-session--error kernel iteration callback)))))
|
||||
|
||||
(cl-defun ein:kernel-retrieve-session--complete
|
||||
(_kernel _callback
|
||||
&key data response
|
||||
&allow-other-keys
|
||||
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
|
||||
(ein:log 'debug "ein:kernel-retrieve-session--complete %s" resp-string))
|
||||
|
||||
(cl-defun ein:kernel-retrieve-session--error
|
||||
(kernel iteration callback
|
||||
&key error-thrown _symbol-status &allow-other-keys)
|
||||
(let* ((max-tries 3)
|
||||
(tries-left (1- (- max-tries iteration))))
|
||||
(ein:log 'verbose "ein:kernel-retrieve-session--error [%s], %s tries left"
|
||||
(car error-thrown) tries-left)
|
||||
(sleep-for 0 (* (1+ iteration) 500))
|
||||
(if (> tries-left 0)
|
||||
(ein:kernel-retrieve-session kernel (1+ iteration) callback))))
|
||||
|
||||
(cl-defun ein:kernel-retrieve-session--success (kernel callback &key data &allow-other-keys)
|
||||
(let ((session-id (plist-get data :id)))
|
||||
(if (plist-get data :kernel)
|
||||
(setq data (plist-get data :kernel)))
|
||||
(cl-destructuring-bind (&key id &allow-other-keys) data
|
||||
(ein:log 'verbose "ein:kernel-retrieve-session--success: kernel-id=%s session-id=%s"
|
||||
id session-id)
|
||||
(setf (ein:$kernel-kernel-id kernel) id)
|
||||
(setf (ein:$kernel-session-id kernel) session-id)
|
||||
(setf (ein:$kernel-ws-url kernel) (ein:kernel--ws-url (ein:$kernel-url-or-port kernel)))
|
||||
(setf (ein:$kernel-kernel-url kernel)
|
||||
(concat (file-name-as-directory (ein:$kernel-base-url kernel)) id)))
|
||||
(ein:kernel-start-websocket kernel callback)))
|
||||
|
||||
(defun ein:kernel-reconnect-session (kernel &optional callback)
|
||||
"If session does not already exist, prompt user to create a new session.
|
||||
Otherwise, return extant session.
|
||||
`ein:kernel-retrieve-session; both retrieves and creates.
|
||||
CALLBACK takes one argument kernel (e.g., execute cell now that
|
||||
we're reconnected)."
|
||||
(ein:kernel-disconnect kernel)
|
||||
(ein:kernel-session-p
|
||||
kernel
|
||||
(apply-partially
|
||||
(lambda (callback* kernel session-p)
|
||||
(when (or session-p
|
||||
(and (not noninteractive) (y-or-n-p "Session not found. Restart?")))
|
||||
(ein:events-trigger (ein:$kernel-events kernel) 'status_reconnecting.Kernel)
|
||||
(ein:kernel-retrieve-session
|
||||
kernel 0
|
||||
(apply-partially
|
||||
(lambda (callback** kernel)
|
||||
(ein:events-trigger (ein:$kernel-events kernel)
|
||||
'status_reconnected.Kernel)
|
||||
(when callback** (funcall callback** kernel)))
|
||||
callback*))))
|
||||
callback)))
|
||||
|
||||
(defun ein:kernel--ws-url (url-or-port)
|
||||
"Assuming URL-OR-PORT already normalized by `ein:url'.
|
||||
See https://github.com/ipython/ipython/pull/3307"
|
||||
(let* ((parsed-url (url-generic-parse-url url-or-port))
|
||||
(protocol (if (string= (url-type parsed-url) "https") "wss" "ws")))
|
||||
(format "%s://%s:%s%s"
|
||||
protocol
|
||||
(url-host parsed-url)
|
||||
(url-port parsed-url)
|
||||
(url-filename parsed-url))))
|
||||
|
||||
(defun ein:kernel--handle-websocket-reply (kernel _ws frame)
|
||||
(-when-let* ((packet (websocket-frame-payload frame))
|
||||
(channel (plist-get (ein:json-read-from-string packet) :channel)))
|
||||
(cond ((string-equal channel "iopub")
|
||||
(ein:kernel--handle-iopub-reply kernel packet))
|
||||
((string-equal channel "shell")
|
||||
(ein:kernel--handle-shell-reply kernel packet))
|
||||
((string-equal channel "stdin")
|
||||
(ein:kernel--handle-stdin-reply kernel packet))
|
||||
(t (ein:log 'warn "Received reply from unforeseen channel %s" channel)))))
|
||||
|
||||
(defun ein:start-single-websocket (kernel open-callback)
|
||||
"OPEN-CALLBACK (kernel) (e.g., execute cell)"
|
||||
(let ((ws-url (concat (ein:$kernel-ws-url kernel)
|
||||
(ein:$kernel-kernel-url kernel)
|
||||
"/channels?session_id="
|
||||
(ein:$kernel-session-id kernel))))
|
||||
(ein:log 'verbose "WS start: %s" ws-url)
|
||||
(setf (ein:$kernel-websocket kernel)
|
||||
(ein:websocket ws-url kernel
|
||||
(apply-partially #'ein:kernel--handle-websocket-reply kernel)
|
||||
(lambda (ws)
|
||||
(-if-let* ((websocket (websocket-client-data ws))
|
||||
(kernel (ein:$websocket-kernel websocket)))
|
||||
(unless (ein:$websocket-closed-by-client websocket)
|
||||
(ein:log 'verbose "WS closed unexpectedly: %s" (websocket-url ws))
|
||||
(ein:kernel-disconnect kernel))
|
||||
(ein:log 'error "ein:start-single-websocket: on-close no client data for %s." (websocket-url ws))))
|
||||
(apply-partially
|
||||
(lambda (cb ws)
|
||||
(-if-let* ((websocket (websocket-client-data ws))
|
||||
(kernel (ein:$websocket-kernel websocket)))
|
||||
(progn
|
||||
(awhen (and (ein:kernel-live-p kernel) cb)
|
||||
(funcall it kernel))
|
||||
(ein:log 'verbose "WS opened: %s" (websocket-url ws)))
|
||||
(ein:log 'error "ein:start-single-websocket: on-open no client data for %s." (websocket-url ws))))
|
||||
open-callback)))))
|
||||
|
||||
(defun ein:kernel-start-websocket (kernel callback)
|
||||
(cond ((<= (ein:$kernel-api-version kernel) 2)
|
||||
(error "Api version %s unsupported" (ein:$kernel-api-version kernel)))
|
||||
(t (ein:start-single-websocket kernel callback))))
|
||||
|
||||
(defun ein:kernel-on-connect (_kernel _content _metadata)
|
||||
(ein:log 'info "Kernel connect_request_reply received."))
|
||||
|
||||
(defun ein:kernel-disconnect (kernel)
|
||||
"Close websocket connection to running kernel, but do not
|
||||
delete the kernel on the server side"
|
||||
(ein:events-trigger (ein:$kernel-events kernel) 'status_disconnected.Kernel)
|
||||
(aif (ein:$kernel-websocket kernel)
|
||||
(progn (ein:websocket-close it)
|
||||
(setf (ein:$kernel-websocket kernel) nil))))
|
||||
|
||||
(defun ein:kernel-live-p (kernel)
|
||||
(and (ein:$kernel-p kernel)
|
||||
(ein:aand (ein:$kernel-websocket kernel) (ein:websocket-open-p it))))
|
||||
|
||||
(defun ein:kernel-when-ready (kernel callback)
|
||||
"Execute CALLBACK of arity 1 (the kernel) when KERNEL is ready.
|
||||
Warn user otherwise."
|
||||
(if (ein:kernel-live-p kernel)
|
||||
(funcall callback kernel)
|
||||
(ein:log 'verbose "Kernel %s unavailable" (ein:$kernel-kernel-id kernel))
|
||||
(ein:kernel-reconnect-session kernel callback)))
|
||||
|
||||
(defun ein:kernel-object-info-request (kernel objname callbacks &optional cursor-pos detail-level)
|
||||
"Send object info request of OBJNAME to KERNEL.
|
||||
|
||||
When calling this method pass a CALLBACKS structure of the form:
|
||||
|
||||
(:object_info_reply (FUNCTION . ARGUMENT))
|
||||
|
||||
Call signature::
|
||||
|
||||
(`funcall' FUNCTION ARGUMENT CONTENT METADATA)
|
||||
|
||||
CONTENT and METADATA are given by `object_info_reply' message.
|
||||
|
||||
`object_info_reply' message is documented here:
|
||||
http://ipython.org/ipython-doc/dev/development/messaging.html#object-information
|
||||
"
|
||||
(cl-assert (ein:kernel-live-p kernel) nil "object_info_reply: Kernel is not active.")
|
||||
(when objname
|
||||
(if (<= (ein:$kernel-api-version kernel) 2)
|
||||
(error "Api version %s unsupported" (ein:$kernel-api-version kernel)))
|
||||
(let* ((content (if (< (ein:$kernel-api-version kernel) 5)
|
||||
(list
|
||||
;; :text ""
|
||||
:oname (format "%s" objname)
|
||||
:cursor_pos (or cursor-pos 0)
|
||||
:detail_level (or detail-level 0))
|
||||
(list
|
||||
:code (format "%s" objname)
|
||||
:cursor_pos (or cursor-pos 0)
|
||||
:detail_level (or detail-level 0))))
|
||||
(msg (ein:kernel--get-msg kernel "inspect_request"
|
||||
(append content (list :detail_level 1))))
|
||||
(msg-id (plist-get (plist-get msg :header) :msg_id)))
|
||||
(ein:websocket-send-shell-channel kernel msg)
|
||||
(ein:kernel-set-callbacks-for-msg kernel msg-id callbacks))))
|
||||
|
||||
(cl-defun ein:kernel-execute (kernel code &optional callbacks
|
||||
&key
|
||||
(silent t)
|
||||
(store-history t)
|
||||
(user-expressions (make-hash-table))
|
||||
(allow-stdin t)
|
||||
(stop-on-error nil))
|
||||
"Execute CODE on KERNEL.
|
||||
|
||||
The CALLBACKS plist looks like:
|
||||
|
||||
(:execute_reply EXECUTE-REPLY-CALLBACK
|
||||
:output OUTPUT-CALLBACK
|
||||
:clear_output CLEAR-OUTPUT-CALLBACK
|
||||
:set_next_input SET-NEXT-INPUT)
|
||||
|
||||
Right hand sides ending -CALLBACK above are of the form (FUNCTION
|
||||
ARG1 ... ARGN).
|
||||
|
||||
(Hindsight: this was all much better implemented using `apply-partially')
|
||||
|
||||
Return randomly generated MSG-ID tag uniquely identifying
|
||||
expectation of a kernel response."
|
||||
(cl-assert (ein:kernel-live-p kernel) nil "execute_reply: Kernel is not active.")
|
||||
(let* ((content (list
|
||||
:code code
|
||||
:silent (or silent json-false)
|
||||
:store_history (or store-history json-false)
|
||||
:user_expressions user-expressions
|
||||
:allow_stdin allow-stdin
|
||||
:stop_on_error (or stop-on-error json-false)))
|
||||
(msg (ein:kernel--get-msg kernel "execute_request" content))
|
||||
(msg-id (plist-get (plist-get msg :header) :msg_id)))
|
||||
(ein:log 'debug "ein:kernel-execute: code=%s msg_id=%s" code msg-id)
|
||||
(ein:websocket-send-shell-channel kernel msg)
|
||||
(ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
|
||||
(unless silent
|
||||
(mapc #'ein:funcall-packed
|
||||
(ein:$kernel-after-execute-hook kernel)))
|
||||
msg-id))
|
||||
|
||||
(defun ein:kernel-connect-request (kernel callbacks)
|
||||
"Request basic information for a KERNEL.
|
||||
|
||||
When calling this method pass a CALLBACKS structure of the form::
|
||||
|
||||
(:connect_reply (FUNCTION . ARGUMENT))
|
||||
|
||||
Call signature::
|
||||
|
||||
(`funcall' FUNCTION ARGUMENT CONTENT METADATA)
|
||||
|
||||
CONTENT and METADATA are given by `kernel_info_reply' message.
|
||||
|
||||
`connect_request' message is documented here:
|
||||
http://ipython.org/ipython-doc/dev/development/messaging.html#connect
|
||||
|
||||
Example::
|
||||
|
||||
(ein:kernel-connect-request
|
||||
(ein:get-kernel)
|
||||
\\='(:kernel_connect_reply (message . \"CONTENT: %S\\nMETADATA: %S\")))
|
||||
"
|
||||
;(cl-assert (ein:kernel-live-p kernel) nil "connect_reply: Kernel is not active.")
|
||||
(let* ((msg (ein:kernel--get-msg kernel "connect_request" (make-hash-table)))
|
||||
(msg-id (plist-get (plist-get msg :header) :msg_id)))
|
||||
(ein:websocket-send-shell-channel kernel msg)
|
||||
(ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
|
||||
msg-id))
|
||||
|
||||
(defun ein:kernel-interrupt (kernel)
|
||||
(when (ein:kernel-live-p kernel)
|
||||
(ein:log 'info "Interrupting kernel")
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url (ein:$kernel-url-or-port kernel)
|
||||
(ein:$kernel-kernel-url kernel)
|
||||
"interrupt")
|
||||
:type "POST"
|
||||
:success (lambda (&rest _ignore)
|
||||
(ein:log 'info "Sent interruption command.")))))
|
||||
|
||||
(defvar ein:force-sync)
|
||||
(declare-function ein:content-query-sessions "ein-contents-api")
|
||||
(cl-defun ein:kernel-delete-session (&optional callback
|
||||
&key url-or-port path kernel
|
||||
&aux (session-id))
|
||||
"Regardless of success or error, we clear all state variables of
|
||||
kernel and funcall CALLBACK (kernel)"
|
||||
(cond (kernel
|
||||
(setq url-or-port (ein:$kernel-url-or-port kernel))
|
||||
(setq path (ein:$kernel-path kernel))
|
||||
(setq session-id (ein:$kernel-session-id kernel)))
|
||||
((and url-or-port path)
|
||||
(aif (ein:notebook-get-opened-notebook url-or-port path)
|
||||
(progn
|
||||
(setq kernel (ein:$notebook-kernel it))
|
||||
(setq session-id (ein:$kernel-session-id kernel)))
|
||||
(let ((ein:force-sync t))
|
||||
(ein:content-query-sessions
|
||||
url-or-port
|
||||
(lambda (session-hash)
|
||||
(setq session-id (car (gethash path session-hash))))
|
||||
nil))))
|
||||
(t (error "ein:kernel-delete-session: need kernel, or url-or-port and path")))
|
||||
(if session-id
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url url-or-port "api/sessions" session-id)
|
||||
:type "DELETE"
|
||||
:complete (apply-partially #'ein:kernel-delete-session--complete kernel session-id callback)
|
||||
:error (apply-partially #'ein:kernel-delete-session--error session-id nil)
|
||||
:success (apply-partially #'ein:kernel-delete-session--success session-id
|
||||
(aif (ein:notebooklist-get-buffer url-or-port)
|
||||
(buffer-local-value 'ein:%notebooklist% it))
|
||||
nil))
|
||||
(ein:log 'verbose "ein:kernel-delete-session: no sessions found for %s" path)
|
||||
(when callback
|
||||
(funcall callback kernel))))
|
||||
|
||||
(cl-defun ein:kernel-delete-session--error (session-id _callback
|
||||
&key _response error-thrown
|
||||
&allow-other-keys)
|
||||
(ein:log 'error "ein:kernel-delete-session--error %s: ERROR %s DATA %s"
|
||||
session-id (car error-thrown) (cdr error-thrown)))
|
||||
|
||||
(cl-defun ein:kernel-delete-session--success (session-id nblist _callback
|
||||
&key _data _symbol-status _response
|
||||
&allow-other-keys)
|
||||
(ein:log 'verbose "ein:kernel-delete-session--success: %s deleted" session-id)
|
||||
(when nblist
|
||||
(ein:notebooklist-reload nblist)))
|
||||
|
||||
(cl-defun ein:kernel-delete-session--complete (kernel _session-id callback
|
||||
&key data response
|
||||
&allow-other-keys
|
||||
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
|
||||
(ein:log 'verbose "ein:kernel-delete-session--complete %s" resp-string)
|
||||
(when kernel
|
||||
(ein:kernel-disconnect kernel))
|
||||
(when callback (funcall callback kernel)))
|
||||
|
||||
;; Reply handlers.
|
||||
(defun ein:kernel-get-callbacks-for-msg (kernel msg-id)
|
||||
(gethash msg-id (ein:$kernel-msg-callbacks kernel)))
|
||||
|
||||
(defun ein:kernel-set-callbacks-for-msg (kernel msg-id callbacks)
|
||||
"Set up promise for MSG-ID."
|
||||
(puthash msg-id callbacks (ein:$kernel-msg-callbacks kernel)))
|
||||
|
||||
(defun ein:kernel--handle-stdin-reply (kernel packet)
|
||||
(cl-destructuring-bind
|
||||
(&key header _parent_header _metadata content &allow-other-keys)
|
||||
(ein:json-read-from-string packet)
|
||||
(let ((msg-type (plist-get header :msg_type))
|
||||
(msg-id (plist-get header :msg_id))
|
||||
(password (plist-get content :password)))
|
||||
(ein:log 'debug "ein:kernel--handle-stdin-reply: msg_type=%s msg_id=%s"
|
||||
msg-type msg-id)
|
||||
(cond ((string-equal msg-type "input_request")
|
||||
(if (not (eql password :json-false))
|
||||
(let* ((passwd (read-passwd (plist-get content :prompt)))
|
||||
(content (list :value passwd))
|
||||
(msg (ein:kernel--get-msg kernel "input_reply" content)))
|
||||
(ein:websocket-send-stdin-channel kernel msg))
|
||||
(cond ((string-match "^\\(ipdb> \\|(Pdb) \\)"
|
||||
(plist-get content :prompt))
|
||||
(aif (ein:ipdb-get-session kernel)
|
||||
(pop-to-buffer (ein:$ipdb-session-buffer it))
|
||||
(let* ((url-or-port (ein:$kernel-url-or-port kernel))
|
||||
(path (ein:$kernel-path kernel))
|
||||
(notebook (ein:notebook-get-opened-notebook
|
||||
url-or-port path)))
|
||||
(ein:ipdb-start-session
|
||||
kernel
|
||||
(match-string 1 (plist-get content :prompt))
|
||||
notebook))))
|
||||
(t (let* ((in (read-string (plist-get content :prompt)))
|
||||
(content (list :value in))
|
||||
(msg (ein:kernel--get-msg kernel "input_reply" content)))
|
||||
(ein:websocket-send-stdin-channel kernel msg))))))))))
|
||||
|
||||
(defun ein:kernel--handle-payload (kernel callbacks payload)
|
||||
(cl-loop with events = (ein:$kernel-events kernel)
|
||||
for p in (append payload nil)
|
||||
for text = (or (plist-get p :text) (plist-get (plist-get p :data) :text/plain))
|
||||
for source = (plist-get p :source)
|
||||
if (member source '("IPython.kernel.zmq.page.page"
|
||||
"IPython.zmq.page.page"
|
||||
"page"))
|
||||
do (unless (equal (ein:trim text) "")
|
||||
(ein:events-trigger
|
||||
events 'open_with_text.Pager (list :text text)))
|
||||
else if
|
||||
(member
|
||||
source
|
||||
'("IPython.kernel.zmq.zmqshell.ZMQInteractiveShell.set_next_input"
|
||||
"IPython.zmq.zmqshell.ZMQInteractiveShell.set_next_input"
|
||||
"set_next_input"))
|
||||
do (let ((cb (plist-get callbacks :set_next_input)))
|
||||
(when cb (ein:funcall-packed cb text)))))
|
||||
|
||||
(defun ein:kernel--handle-shell-reply (kernel packet)
|
||||
(cl-destructuring-bind
|
||||
(&key header content metadata parent_header &allow-other-keys)
|
||||
(ein:json-read-from-string packet)
|
||||
(let* ((msg-type (plist-get header :msg_type))
|
||||
(msg-id (plist-get parent_header :msg_id))
|
||||
(callbacks (ein:kernel-get-callbacks-for-msg kernel msg-id)))
|
||||
(ein:log 'debug "ein:kernel--handle-shell-reply: msg_type=%s msg_id=%s"
|
||||
msg-type msg-id)
|
||||
(aif (plist-get callbacks (intern-soft (format ":%s" msg-type)))
|
||||
(ein:funcall-packed it content metadata)
|
||||
(ein:log 'info "ein:kernel--handle-shell-reply: No :%s callback for msg_id=%s"
|
||||
msg-type msg-id))
|
||||
(aif (plist-get content :payload)
|
||||
(ein:kernel--handle-payload kernel callbacks it))
|
||||
(let ((events (ein:$kernel-events kernel)))
|
||||
(ein:case-equal msg-type
|
||||
(("execute_reply")
|
||||
(aif (plist-get content :execution_count)
|
||||
(ein:events-trigger events 'execution_count.Kernel it))))))))
|
||||
|
||||
(defun ein:kernel--handle-iopub-reply (kernel packet)
|
||||
(cl-destructuring-bind
|
||||
(&key content metadata parent_header header &allow-other-keys)
|
||||
(ein:json-read-from-string packet)
|
||||
(let* ((msg-type (plist-get header :msg_type))
|
||||
(msg-id (plist-get header :msg_id))
|
||||
(parent-id (plist-get parent_header :msg_id))
|
||||
(callbacks (ein:kernel-get-callbacks-for-msg kernel parent-id))
|
||||
(events (ein:$kernel-events kernel)))
|
||||
(ein:log 'debug
|
||||
"ein:kernel--handle-iopub-reply: msg_type=%s msg_id=%s parent_id=%s"
|
||||
msg-type msg-id parent-id)
|
||||
(ein:case-equal msg-type
|
||||
(("stream" "display_data" "pyout" "pyerr" "error" "execute_result")
|
||||
(aif (plist-get callbacks :output) ;; ein:cell--handle-output
|
||||
(ein:funcall-packed it msg-type content metadata)
|
||||
(ein:log 'warn (concat "ein:kernel--handle-iopub-reply: "
|
||||
"No :output callback for parent_id=%s")
|
||||
parent-id))
|
||||
(when (ein:ipdb-get-session kernel)
|
||||
(ein:ipdb--handle-iopub-reply kernel packet)))
|
||||
(("status")
|
||||
(ein:case-equal (plist-get content :execution_state)
|
||||
(("busy")
|
||||
(ein:events-trigger events 'status_busy.Kernel))
|
||||
(("idle")
|
||||
(ein:events-trigger events 'status_idle.Kernel)
|
||||
(awhen (ein:ipdb-get-session kernel)
|
||||
(ein:ipdb-stop-session it)))
|
||||
(("dead")
|
||||
(ein:kernel-disconnect kernel)
|
||||
(awhen (ein:ipdb-get-session kernel)
|
||||
(ein:ipdb-stop-session it)))))
|
||||
(("data_pub")
|
||||
(ein:log 'verbose "ein:kernel--handle-iopub-reply: data_pub %S" packet))
|
||||
(("clear_output")
|
||||
(aif (plist-get callbacks :clear_output)
|
||||
(ein:funcall-packed it content metadata)
|
||||
(ein:log 'info (concat "ein:kernel--handle-iopub-reply: "
|
||||
"No :clear_output callback for parent_id=%s")
|
||||
parent-id)))))))
|
||||
|
||||
(provide 'ein-kernel)
|
||||
|
||||
;;; ein-kernel.el ends here
|
||||
56
lisp/ein/ein-kernelinfo.el
Normal file
56
lisp/ein/ein-kernelinfo.el
Normal file
@@ -0,0 +1,56 @@
|
||||
;;; ein-kernelinfo.el --- Kernel info module -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012 Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-kernelinfo.el is free software: you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation, either version 3 of the
|
||||
;; License, or (at your option) any later version.
|
||||
|
||||
;; ein-kernelinfo.el is distributed in the hope that it will be
|
||||
;; useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
;; of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-kernelinfo.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'eieio)
|
||||
(require 'ein-kernel)
|
||||
|
||||
(defclass ein:kernelinfo ()
|
||||
((kernel
|
||||
:initarg :kernel :type ein:$kernel
|
||||
:documentation "Kernel instance.")
|
||||
(get-buffers
|
||||
:initarg :get-buffers
|
||||
:documentation "A packed function to get buffers associated
|
||||
with the kernel. The buffer local `default-directory' variable
|
||||
in these buffer will be synced with the kernel's cwd.")
|
||||
(hostname
|
||||
:initarg :hostname :type string
|
||||
:documentation "Host name of the machine where the kernel is running on.")
|
||||
(ccwd
|
||||
:initarg :ccwd :type string
|
||||
:documentation "cached CWD (last time checked CWD)."))
|
||||
:documentation "Info related (but unimportant) to kernel")
|
||||
|
||||
(defun ein:kernelinfo-new (kernel get-buffers)
|
||||
"Make a new `ein:kernelinfo' instance based on KERNEL and GET-BUFFERS."
|
||||
(let ((kerinfo (make-instance 'ein:kernelinfo)))
|
||||
(setf (slot-value kerinfo 'kernel) kernel)
|
||||
(setf (slot-value kerinfo 'get-buffers) get-buffers)
|
||||
kerinfo))
|
||||
(provide 'ein-kernelinfo)
|
||||
|
||||
;;; ein-kernelinfo.el ends here
|
||||
55
lisp/ein/ein-kill-ring.el
Normal file
55
lisp/ein/ein-kill-ring.el
Normal file
@@ -0,0 +1,55 @@
|
||||
;;; ein-kill-ring.el --- Kill-ring for cells -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-kill-ring.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-kill-ring.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-kill-ring.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Stolen from simple.el.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(defvar ein:kill-ring nil)
|
||||
(defvar ein:kill-ring-yank-pointer nil)
|
||||
(defvar ein:kill-ring-max kill-ring-max)
|
||||
|
||||
(defun ein:kill-new (obj)
|
||||
"Make OBJ the latest kill in the kill ring `ein:kill-ring'.
|
||||
Set `ein:kill-ring-yank-pointer' to point to it."
|
||||
(push obj ein:kill-ring)
|
||||
(if (> (length ein:kill-ring) ein:kill-ring-max)
|
||||
(setcdr (nthcdr (1- ein:kill-ring-max) ein:kill-ring) nil))
|
||||
(setq ein:kill-ring-yank-pointer ein:kill-ring))
|
||||
|
||||
(defun ein:current-kill (n &optional do-not-move)
|
||||
"Rotate the yanking point by N places, and then return that kill.
|
||||
If optional arg DO-NOT-MOVE is non-nil, then don't actually
|
||||
move the yanking point; just return the Nth kill forward."
|
||||
(unless ein:kill-ring (error "Kill ring is empty"))
|
||||
(let ((ARGth-kill-element
|
||||
(nthcdr (mod (- n (length ein:kill-ring-yank-pointer))
|
||||
(length ein:kill-ring))
|
||||
ein:kill-ring)))
|
||||
(unless do-not-move
|
||||
(setq ein:kill-ring-yank-pointer ARGth-kill-element))
|
||||
(car ARGth-kill-element)))
|
||||
|
||||
(provide 'ein-kill-ring)
|
||||
|
||||
;;; ein-kill-ring.el ends here
|
||||
116
lisp/ein/ein-log.el
Normal file
116
lisp/ein/ein-log.el
Normal file
@@ -0,0 +1,116 @@
|
||||
;;; ein-log.el --- Logging module for ein.el -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-log.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-log.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-log.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein-core)
|
||||
|
||||
(defvar ein:log-all-buffer-name "*ein:log-all*")
|
||||
|
||||
(defvar ein:log-level-def
|
||||
'(;; debugging
|
||||
(blather . 60) (trace . 50) (debug . 40)
|
||||
;; information
|
||||
(verbose . 30) (info . 20)
|
||||
;; errors
|
||||
(warn . 10) (error . 0))
|
||||
"Named logging levels.")
|
||||
;; Some names are stolen from supervisord (http://supervisord.org/logging.html)
|
||||
|
||||
(defvar ein:log-level 30)
|
||||
(defvar ein:log-message-level 20)
|
||||
|
||||
(defvar ein:log-print-length 10 "`print-length' for `ein:log'")
|
||||
(defvar ein:log-print-level 1 "`print-level' for `ein:log'")
|
||||
(defvar ein:log-max-string 1000)
|
||||
|
||||
(defun ein:log-set-level (level)
|
||||
(setq ein:log-level (ein:log-level-name-to-int level)))
|
||||
|
||||
(defun ein:log-set-message-level (level)
|
||||
(setq ein:log-message-level (ein:log-level-name-to-int level)))
|
||||
|
||||
(defun ein:log-level-int-to-name (int)
|
||||
(cl-loop for (n . i) in ein:log-level-def
|
||||
when (>= int i)
|
||||
return n
|
||||
finally 'error))
|
||||
|
||||
(defun ein:log-level-name-to-int (name)
|
||||
(cdr (assq name ein:log-level-def)))
|
||||
|
||||
(defsubst ein:log-strip-timestamp (msg)
|
||||
(replace-regexp-in-string "^[0-9: ]+" "" msg))
|
||||
|
||||
(defun ein:log-wrapper (level func)
|
||||
(setq level (ein:log-level-name-to-int level))
|
||||
(when (<= level ein:log-level)
|
||||
(let* ((levname (ein:log-level-int-to-name level))
|
||||
(print-level ein:log-print-level)
|
||||
(print-length ein:log-print-length)
|
||||
(msg (format "%s: [%s] %s" (format-time-string "%H:%M:%S:%3N") levname (funcall func)))
|
||||
(orig-buffer (current-buffer)))
|
||||
(if (and ein:log-max-string
|
||||
(> (length msg) ein:log-max-string))
|
||||
(setq msg (substring msg 0 ein:log-max-string)))
|
||||
(ein:with-read-only-buffer (get-buffer-create ein:log-all-buffer-name)
|
||||
(goto-char (point-max))
|
||||
(insert msg (format " @%S" orig-buffer) "\n"))
|
||||
(when (<= level ein:log-message-level)
|
||||
(message "ein: %s" (ein:log-strip-timestamp msg))))))
|
||||
|
||||
(make-obsolete-variable 'ein:debug nil "0.17.0")
|
||||
|
||||
(defmacro ein:log (level string &rest args)
|
||||
(declare (indent 1))
|
||||
`(ein:log-wrapper ,level (lambda () (format ,string ,@args))))
|
||||
|
||||
(defsubst ein:debug-p ()
|
||||
"Set to non-`nil' to raise errors instead of suppressing it.
|
||||
Change the behavior of `ein:log-ignore-errors'."
|
||||
(>= ein:log-level (alist-get 'debug ein:log-level-def)))
|
||||
|
||||
(defun ein:log-pop-to-ws-buffer ()
|
||||
(interactive)
|
||||
(-if-let* ((kernel (ein:get-kernel--notebook))
|
||||
(websocket (ein:$kernel-websocket kernel)))
|
||||
(pop-to-buffer
|
||||
(websocket-get-debug-buffer-create
|
||||
(ein:$websocket-ws websocket)))
|
||||
(message "Must be run from notebook buffer")))
|
||||
|
||||
(defun ein:log-pop-to-request-buffer ()
|
||||
(interactive)
|
||||
(aif (get-buffer request-log-buffer-name)
|
||||
(pop-to-buffer it)
|
||||
(message "No buffer %s" request-log-buffer-name)))
|
||||
|
||||
(defun ein:log-pop-to-all-buffer ()
|
||||
(interactive)
|
||||
(pop-to-buffer (get-buffer-create ein:log-all-buffer-name)))
|
||||
|
||||
(provide 'ein-log)
|
||||
|
||||
;;; ein-log.el ends here
|
||||
8082
lisp/ein/ein-markdown-mode.el
Normal file
8082
lisp/ein/ein-markdown-mode.el
Normal file
File diff suppressed because it is too large
Load Diff
65
lisp/ein/ein-node.el
Normal file
65
lisp/ein/ein-node.el
Normal file
@@ -0,0 +1,65 @@
|
||||
;;; ein-node.el --- Structure to hold data in ewoc node -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-node.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-node.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-node.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ewoc)
|
||||
(require 'ein-core)
|
||||
|
||||
(cl-defstruct ein:$node
|
||||
path ; list of path
|
||||
data ; actual data
|
||||
class ; list
|
||||
)
|
||||
|
||||
(defun ein:node-new (path data &optional class &rest args)
|
||||
(apply #'make-ein:$node :path path :data data :class class args))
|
||||
|
||||
(defun ein:node-add-class (node &rest classes)
|
||||
(mapc (lambda (c) (cl-pushnew c (ein:$node-class node))) classes))
|
||||
|
||||
(defun ein:node-remove-class (node &rest classes)
|
||||
(let ((node-class (ein:$node-class node)))
|
||||
(mapc (lambda (c) (setq node-class (delq c node-class))) classes)
|
||||
(setf (ein:$node-class node) node-class)))
|
||||
|
||||
(defun ein:node-has-class (node class)
|
||||
(memq class (ein:$node-class node)))
|
||||
|
||||
(defun ein:node-filter (ewoc-node-list &rest args)
|
||||
(cl-loop for (key . class) in (ein:plist-iter args)
|
||||
do (setq ewoc-node-list
|
||||
(cl-loop for ewoc-node in ewoc-node-list
|
||||
for node = (ewoc-data ewoc-node)
|
||||
when (cl-case key
|
||||
(:is (ein:node-has-class node class))
|
||||
(:not (not (ein:node-has-class node class)))
|
||||
(t (error "%s is not supported" key)))
|
||||
collect ewoc-node)))
|
||||
ewoc-node-list)
|
||||
|
||||
(provide 'ein-node)
|
||||
|
||||
;;; ein-node.el ends here
|
||||
1011
lisp/ein/ein-notebook.el
Normal file
1011
lisp/ein/ein-notebook.el
Normal file
File diff suppressed because it is too large
Load Diff
826
lisp/ein/ein-notebooklist.el
Normal file
826
lisp/ein/ein-notebooklist.el
Normal file
@@ -0,0 +1,826 @@
|
||||
;;; ein-notebooklist.el --- Notebook list buffer -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2018- John M. Miller
|
||||
|
||||
;; Authors: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
;; John M. Miller <millejoh at mac.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-notebooklist.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-notebooklist.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-notebooklist.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'widget)
|
||||
(require 'cus-edit)
|
||||
|
||||
(require 'ein-core)
|
||||
(require 'ein-contents-api)
|
||||
(require 'deferred)
|
||||
(require 'dash)
|
||||
(require 'ido)
|
||||
|
||||
(declare-function ein:jupyter-crib-token "ein-jupyter")
|
||||
(declare-function ein:jupyter-get-default-kernel "ein-jupyter")
|
||||
(declare-function ein:jupyter-crib-running-servers "ein-jupyter")
|
||||
(declare-function ein:file-open "ein-file")
|
||||
(autoload 'ein:get-notebook "ein-notebook")
|
||||
|
||||
(defcustom ein:notebooklist-login-timeout (truncate (* 6.3 1000))
|
||||
"Timeout in milliseconds for logging into server"
|
||||
:group 'ein
|
||||
:type 'integer)
|
||||
|
||||
(make-obsolete-variable 'ein:notebooklist-first-open-hook nil "0.17.0")
|
||||
|
||||
(cl-defstruct ein:$notebooklist
|
||||
"Hold notebooklist variables.
|
||||
|
||||
`ein:$notebooklist-url-or-port'
|
||||
URL or port of IPython server.
|
||||
|
||||
`ein:$notebooklist-path'
|
||||
The path for the notebooklist.
|
||||
|
||||
`ein:$notebooklist-data'
|
||||
JSON data sent from the server.
|
||||
`ein:$notebooklist-api-version'
|
||||
Major version of the IPython notebook server we are talking to."
|
||||
url-or-port
|
||||
path
|
||||
data
|
||||
api-version)
|
||||
|
||||
(define-obsolete-variable-alias 'ein:notebooklist 'ein:%notebooklist% "0.1.2")
|
||||
(ein:deflocal ein:%notebooklist% nil
|
||||
"Buffer local variable to store an instance of `ein:$notebooklist'.")
|
||||
|
||||
(ein:deflocal ein:%notebooklist-new-kernel% nil
|
||||
"Buffer local variable to store kernel type for newly created notebooks.")
|
||||
|
||||
(defcustom ein:notebooklist-sort-field :name
|
||||
"The notebook list sort field."
|
||||
:type '(choice (const :tag "Name" :name)
|
||||
(const :tag "Last modified" :last_modified))
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:notebooklist-sort-order :ascending
|
||||
"The notebook list sort order."
|
||||
:type '(choice (const :tag "Ascending" :ascending)
|
||||
(const :tag "Descending" :descending))
|
||||
:group 'ein)
|
||||
|
||||
(defvar ein:notebooklist-buffer-name-template "*ein:notebooklist %s*")
|
||||
|
||||
(defvar ein:notebooklist-map (make-hash-table :test 'equal)
|
||||
"Data store for `ein:notebooklist-list'.
|
||||
Mapping from URL-OR-PORT to an instance of `ein:$notebooklist'.")
|
||||
|
||||
(defun ein:notebooklist-keys ()
|
||||
"Get a list of registered server urls."
|
||||
(hash-table-keys ein:notebooklist-map))
|
||||
|
||||
(defun ein:notebooklist-list ()
|
||||
"Get a list of opened `ein:$notebooklist'."
|
||||
(hash-table-values ein:notebooklist-map))
|
||||
|
||||
(defun ein:notebooklist-list-remove (url-or-port)
|
||||
(remhash url-or-port ein:notebooklist-map))
|
||||
|
||||
(defun ein:notebooklist-list-add (nblist)
|
||||
"Register notebook list instance NBLIST for global lookup.
|
||||
This function adds NBLIST to `ein:notebooklist-map'."
|
||||
(puthash (ein:$notebooklist-url-or-port nblist)
|
||||
nblist
|
||||
ein:notebooklist-map))
|
||||
|
||||
(defun ein:notebooklist-list-get (url-or-port)
|
||||
"Get an instance of `ein:$notebooklist' by URL-OR-PORT as a key."
|
||||
(gethash url-or-port ein:notebooklist-map))
|
||||
|
||||
(defsubst ein:notebooklist-url (url-or-port &rest paths)
|
||||
(apply #'ein:url url-or-port "api/contents" paths))
|
||||
|
||||
(defun ein:notebooklist-sentinel (url-or-port process event)
|
||||
"Remove URL-OR-PORT from ein:notebooklist-map when PROCESS dies"
|
||||
(when (not (string= "open" (substring event 0 4)))
|
||||
(ein:log 'info "Process %s %s %s"
|
||||
(car (process-command process))
|
||||
(replace-regexp-in-string "\n$" "" event)
|
||||
url-or-port)
|
||||
(ein:notebooklist-list-remove url-or-port)))
|
||||
|
||||
(defun ein:notebooklist-get-buffer (url-or-port)
|
||||
(get-buffer-create
|
||||
(format ein:notebooklist-buffer-name-template url-or-port)))
|
||||
|
||||
(defun ein:notebooklist-token-or-password (url-or-port)
|
||||
"Return token or password for URL-OR-PORT.
|
||||
|
||||
Jupyter requires one or the other but not both.
|
||||
Return empty string token if all authentication disabled.
|
||||
Return nil if unclear what, if any, authentication applies."
|
||||
(cl-multiple-value-bind (password-p token) (ein:jupyter-crib-token url-or-port)
|
||||
(cond ((eq password-p t) (read-passwd (format "Password for %s: " url-or-port)))
|
||||
((and (stringp token) (eq password-p :json-false)) token)
|
||||
(t nil))))
|
||||
|
||||
(defun ein:notebooklist-ask-url-or-port ()
|
||||
(let* ((default (ein:url (aif (ein:get-notebook)
|
||||
(ein:$notebook-url-or-port it)
|
||||
(aif ein:%notebooklist%
|
||||
(ein:$notebooklist-url-or-port it)))))
|
||||
(url-or-port-list
|
||||
(-distinct (mapcar #'ein:url
|
||||
(append (when default (list default))
|
||||
(if (stringp ein:urls)
|
||||
(list ein:urls)
|
||||
ein:urls)
|
||||
(mapcar
|
||||
(lambda (json)
|
||||
(cl-destructuring-bind (&key url &allow-other-keys)
|
||||
json
|
||||
(ein:url url)))
|
||||
(ein:jupyter-crib-running-servers))))))
|
||||
(url-or-port (let (ido-report-no-match ido-use-faces)
|
||||
(ein:completing-read "URL or port: "
|
||||
url-or-port-list
|
||||
nil nil nil nil
|
||||
(car-safe url-or-port-list)))))
|
||||
(ein:url url-or-port)))
|
||||
|
||||
(defsubst ein:notebooklist-canonical-url-or-port (url-host username)
|
||||
"Canonicalize.
|
||||
For the record,
|
||||
https://hub.data8x.berkeley.edu
|
||||
needs to look like
|
||||
https://hub.data8x.berkeley.edu/user/1dcdab3c2f59736806b85af865a1a28d"
|
||||
(ein:url url-host "user" username))
|
||||
|
||||
(cl-defun ein:notebooklist-open* (url-or-port &optional path resync callback errback hub-p
|
||||
&aux (canonical-p (not hub-p)) tokens-key)
|
||||
"Workhorse of `ein:login'.
|
||||
|
||||
A notebooklist can be opened from any PATH within the server root hierarchy.
|
||||
PATH is empty at the root. RESYNC, when non-nil, requeries the contents-api
|
||||
version and kernelspecs.
|
||||
|
||||
Full jupyterhub url is https://hub.data8x.berkeley.edu/user/1dcdab3c2f59736806b85af865a1a28d/?token=c421c6863ddb4e7ea5a311c31c948cd0
|
||||
|
||||
URL-HOST is hub.data8x.berkeley.edu
|
||||
USERNAME is 1dcdab3c2f59736806b85af865a1a28d
|
||||
TOKEN is c421c6863ddb4e7ea5a311c31c948cd0
|
||||
|
||||
CALLBACK takes two arguments, the resulting buffer and URL-OR-PORT.
|
||||
ERRBACK takes one argument, the resulting buffer."
|
||||
(setq path (or path ""))
|
||||
(if (and (not resync) (ein:notebooklist-list-get url-or-port))
|
||||
(ein:content-query-contents
|
||||
url-or-port path
|
||||
(apply-partially #'ein:notebooklist-open--finish url-or-port callback)
|
||||
errback)
|
||||
(when hub-p
|
||||
(let* ((parsed-url (url-generic-parse-url url-or-port))
|
||||
(url-host (url-host parsed-url))
|
||||
(cookies (ein:query-get-cookies url-host "/user/"))
|
||||
(previous-users
|
||||
(mapcar
|
||||
(lambda (entry)
|
||||
(file-name-nondirectory (directory-file-name (plist-get entry :path))))
|
||||
cookies))
|
||||
(pq (url-path-and-query parsed-url))
|
||||
(path0 (car pq))
|
||||
(query (cdr pq))
|
||||
(_ (setf canonical-p
|
||||
(and (stringp path0)
|
||||
(string-match "user/\\([a-z0-9]+\\)" path0))))
|
||||
(username (if canonical-p
|
||||
(match-string-no-properties 1 path0)
|
||||
(read-no-blanks-input "User: " (car previous-users))))
|
||||
(_ (setf url-or-port
|
||||
(ein:notebooklist-canonical-url-or-port url-host username)))
|
||||
(_ (setf tokens-key
|
||||
(ein:query-divine-authorization-tokens-key url-or-port)))
|
||||
(token
|
||||
(if (and (stringp query)
|
||||
(string-match "token=\\([a-z0-9]+\\)" query))
|
||||
(prog1
|
||||
(match-string-no-properties 1 query)
|
||||
(cl-assert canonical-p))
|
||||
(when canonical-p
|
||||
(read-no-blanks-input "Token: ")))))
|
||||
(when token
|
||||
(setf (gethash tokens-key ein:query-authorization-tokens) token))))
|
||||
(if (not canonical-p)
|
||||
;; Retread to get _xsrf for canonical url
|
||||
(progn
|
||||
(ein:notebooklist-list-remove url-or-port)
|
||||
(ein:notebooklist-login--iteration url-or-port callback errback nil -1 nil))
|
||||
(when tokens-key
|
||||
(let ((belay-tokens
|
||||
(lambda (&rest _args)
|
||||
(remhash tokens-key ein:query-authorization-tokens))))
|
||||
(add-function :before (var errback) belay-tokens)
|
||||
(add-function :before (var callback) belay-tokens)))
|
||||
(ein:query-notebook-api-version
|
||||
url-or-port
|
||||
(lambda ()
|
||||
(ein:query-kernelspecs
|
||||
url-or-port
|
||||
(lambda ()
|
||||
(deferred:$
|
||||
(deferred:next
|
||||
(lambda ()
|
||||
(ein:content-query-hierarchy url-or-port))))
|
||||
(ein:content-query-contents
|
||||
url-or-port path
|
||||
(apply-partially #'ein:notebooklist-open--finish url-or-port callback)
|
||||
errback))))))))
|
||||
|
||||
(make-obsolete-variable 'ein:notebooklist-keepalive-refresh-time nil "0.17.0")
|
||||
(make-obsolete-variable 'ein:enable-keepalive nil "0.17.0")
|
||||
|
||||
(defcustom ein:notebooklist-date-format "%F"
|
||||
"The format spec for date in notebooklist mode.
|
||||
See `ein:format-time-string'."
|
||||
:type '(or string function)
|
||||
:group 'ein)
|
||||
|
||||
(defun ein:notebooklist-open--finish (url-or-port callback content)
|
||||
"Called via `ein:notebooklist-open*'."
|
||||
(ein:log 'verbose "Opening notebooklist at %s"
|
||||
(ein:url url-or-port (ein:$content-path content)))
|
||||
(with-current-buffer (ein:notebooklist-get-buffer url-or-port)
|
||||
(ein:notebooklist-mode)
|
||||
(let ((restore-point (aand (widget-at)
|
||||
(awhen (widget-value it)
|
||||
(and (stringp it) it))
|
||||
(string-match-p "Open\\|Stop\\|Delete" it)
|
||||
(point))))
|
||||
(awhen ein:%notebooklist%
|
||||
(ein:notebooklist-list-remove (ein:$notebooklist-url-or-port it)))
|
||||
(setq ein:%notebooklist%
|
||||
(make-ein:$notebooklist :url-or-port url-or-port
|
||||
:path (ein:$content-path content)
|
||||
:data (ein:$content-raw-content content)
|
||||
:api-version (ein:$content-notebook-api-version content)))
|
||||
(ein:notebooklist-list-add ein:%notebooklist%)
|
||||
(let ((inhibit-read-only t))
|
||||
(erase-buffer))
|
||||
(when callback
|
||||
(funcall callback (current-buffer) url-or-port))
|
||||
(ein:content-query-sessions url-or-port (apply-partially #'ein:notebooklist-render
|
||||
url-or-port
|
||||
restore-point))
|
||||
(current-buffer))))
|
||||
|
||||
(cl-defun ein:notebooklist-open-error (url-or-port path
|
||||
&key error-thrown &allow-other-keys)
|
||||
(ein:log 'error
|
||||
"ein:notebooklist-open-error %s: ERROR %s DATA %s" (concat (file-name-as-directory url-or-port) path) (car error-thrown) (cdr error-thrown)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:notebooklist-reload (&optional nblist resync callback)
|
||||
"Reload current Notebook list."
|
||||
(interactive)
|
||||
(setq nblist (or nblist ein:%notebooklist%))
|
||||
(ein:notebooklist-open* (ein:$notebooklist-url-or-port nblist)
|
||||
(ein:$notebooklist-path nblist) resync callback))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:notebooklist-new-notebook (url-or-port kernelspec &optional callback no-pop retry explicit-path)
|
||||
(interactive (list (ein:notebooklist-ask-url-or-port)
|
||||
(ein:completing-read
|
||||
"Select kernel: "
|
||||
(ein:list-available-kernels
|
||||
(ein:$notebooklist-url-or-port ein:%notebooklist%))
|
||||
nil t nil nil "default" nil)))
|
||||
(let* ((notebooklist (ein:notebooklist-list-get url-or-port))
|
||||
(path (or explicit-path (ein:$notebooklist-path notebooklist)))
|
||||
(url (ein:notebooklist-url url-or-port path)))
|
||||
(ein:query-singleton-ajax
|
||||
url
|
||||
:type "POST"
|
||||
:data (ein:json-encode '((type . "notebook")))
|
||||
:headers (list (cons "Content-Type" "application/json"))
|
||||
:parser #'ein:json-read
|
||||
:error (apply-partially #'ein:notebooklist-new-notebook-error
|
||||
url-or-port kernelspec callback no-pop retry explicit-path)
|
||||
:success (apply-partially #'ein:notebooklist-new-notebook-success
|
||||
url-or-port kernelspec
|
||||
path
|
||||
callback no-pop))))
|
||||
|
||||
(cl-defun ein:notebooklist-new-notebook-success (url-or-port
|
||||
kernelspec
|
||||
path
|
||||
callback
|
||||
no-pop
|
||||
&key data
|
||||
&allow-other-keys)
|
||||
(let ((nbpath (plist-get data :path)))
|
||||
(ein:notebook-open url-or-port nbpath kernelspec callback nil no-pop)
|
||||
(ein:notebooklist-open* url-or-port path)))
|
||||
|
||||
(cl-defun ein:notebooklist-new-notebook-error
|
||||
(url-or-port kernelspec callback no-pop retry explicit-path
|
||||
&key symbol-status error-thrown &allow-other-keys)
|
||||
(let ((notice (format "ein:notebooklist-new-notebook-error: %s %s"
|
||||
symbol-status error-thrown)))
|
||||
(if retry
|
||||
(ein:log 'error notice)
|
||||
(ein:log 'info notice)
|
||||
(sleep-for 0 1500)
|
||||
(ein:notebooklist-new-notebook url-or-port kernelspec callback no-pop t explicit-path))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:notebooklist-new-notebook-with-name
|
||||
(url-or-port kernelspec name &optional callback no-pop)
|
||||
"Upon notebook-open, rename the notebook, then funcall CALLBACK."
|
||||
(interactive
|
||||
(let ((url-or-port (ein:get-url-or-port)))
|
||||
(unless url-or-port
|
||||
(error "ein:notebooklist-new-notebook-with-name: no server context"))
|
||||
(let ((kernelspec (ein:completing-read
|
||||
"Select kernel: "
|
||||
(ein:list-available-kernels url-or-port)
|
||||
nil t nil nil "default" nil))
|
||||
(name (read-from-minibuffer
|
||||
(format "Notebook name (at %s): " url-or-port))))
|
||||
(list url-or-port kernelspec name))))
|
||||
(unless callback
|
||||
(setq callback #'ignore))
|
||||
(add-function :before (var callback)
|
||||
(apply-partially
|
||||
(lambda (name* notebook _created)
|
||||
(with-current-buffer (ein:notebook-buffer notebook)
|
||||
(ein:notebook-rename-command name*)))
|
||||
name))
|
||||
(ein:notebooklist-new-notebook url-or-port kernelspec callback no-pop))
|
||||
|
||||
(defun ein:notebooklist-delete-notebook (_notebooklist url-or-port path &optional callback)
|
||||
"CALLBACK with no arguments, e.g., semaphore"
|
||||
(setq callback (or callback #'ignore))
|
||||
(dolist (buf (seq-filter (lambda (b)
|
||||
(with-current-buffer b
|
||||
(aif (ein:get-notebook)
|
||||
(string= path (ein:$notebook-notebook-path it)))))
|
||||
(buffer-list)))
|
||||
(cl-letf (((symbol-function 'y-or-n-p) (lambda (&rest _args) nil)))
|
||||
(kill-buffer buf)))
|
||||
(if (ein:notebook-opened-notebooks (lambda (nb)
|
||||
(string= path
|
||||
(ein:$notebook-notebook-path nb))))
|
||||
(ein:log 'error "ein:notebooklist-delete-notebook: cannot close %s" path)
|
||||
(let ((delete-nb
|
||||
(apply-partially
|
||||
(lambda (url* settings* _kernel)
|
||||
(apply #'ein:query-singleton-ajax url* settings*))
|
||||
(ein:notebooklist-url url-or-port path)
|
||||
(list :type "DELETE"
|
||||
:complete (apply-partially
|
||||
#'ein:notebooklist-delete-notebook--complete
|
||||
(ein:url url-or-port path) callback)))))
|
||||
(ein:message-whir
|
||||
"Ending session" (var delete-nb)
|
||||
(ein:kernel-delete-session delete-nb
|
||||
:url-or-port url-or-port
|
||||
:path path)))))
|
||||
|
||||
(cl-defun ein:notebooklist-delete-notebook--complete
|
||||
(_url callback
|
||||
&key data response _symbol-status
|
||||
&allow-other-keys
|
||||
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
|
||||
(ein:log 'debug "ein:notebooklist-delete-notebook--complete %s" resp-string)
|
||||
(when callback (funcall callback)))
|
||||
|
||||
(defun generate-breadcrumbs (path)
|
||||
"Given notebooklist path, generate alist of breadcrumps of form (name . path)."
|
||||
(let* ((paths (split-string path "/" t))
|
||||
(current-path "/")
|
||||
(pairs (list (cons "Home" ""))))
|
||||
(dolist (p paths pairs)
|
||||
(setf current-path (concat current-path "/" p)
|
||||
pairs (append pairs (list (cons p current-path)))))))
|
||||
|
||||
(cl-defun ein:nblist--sort-group (group by-param order)
|
||||
(sort group #'(lambda (x y)
|
||||
(cond ((eq order :ascending)
|
||||
(string-lessp (plist-get x by-param)
|
||||
(plist-get y by-param)))
|
||||
((eq order :descending)
|
||||
(string-greaterp (plist-get x by-param)
|
||||
(plist-get y by-param)))))))
|
||||
|
||||
(defun ein:notebooklist--order-data (nblist-data sort-param sort-order)
|
||||
"Try to sanely sort the notebooklist data for the current path."
|
||||
(let* ((groups (-group-by (lambda (x) (plist-get x :type)) nblist-data))
|
||||
(dirs (ein:nblist--sort-group (cdr (assoc "directory" groups))
|
||||
sort-param
|
||||
sort-order))
|
||||
(nbs (ein:nblist--sort-group (cdr (assoc "notebook" groups))
|
||||
sort-param
|
||||
sort-order))
|
||||
(files (ein:nblist--sort-group
|
||||
(-flatten-n 1 (-map #'cdr (-group-by
|
||||
#'(lambda (x) (car (last (split-string (plist-get x :name) "\\."))))
|
||||
(cdr (assoc "file" groups)))))
|
||||
sort-param
|
||||
sort-order)))
|
||||
(-concat dirs nbs files)))
|
||||
|
||||
(defun render-header (url-or-port &rest _args)
|
||||
(with-current-buffer (ein:notebooklist-get-buffer url-or-port)
|
||||
(widget-insert
|
||||
(format "Contents API %s (%s)\n\n"
|
||||
(ein:need-notebook-api-version url-or-port)
|
||||
url-or-port))
|
||||
(let ((breadcrumbs (generate-breadcrumbs
|
||||
(ein:$notebooklist-path ein:%notebooklist%))))
|
||||
(dolist (p breadcrumbs)
|
||||
(let ((url-or-port url-or-port)
|
||||
(name (car p))
|
||||
(path (cdr p)))
|
||||
(widget-insert " | ")
|
||||
(widget-create
|
||||
'link
|
||||
:notify (lambda (&rest _ignore)
|
||||
(ein:notebooklist-open* url-or-port path nil
|
||||
(lambda (buffer _url-or-port)
|
||||
(pop-to-buffer buffer))))
|
||||
name)))
|
||||
(widget-insert " |\n\n"))
|
||||
(let* ((url-or-port url-or-port)
|
||||
(kernels (ein:list-available-kernels url-or-port)))
|
||||
(widget-create
|
||||
'link
|
||||
:notify (lambda (&rest _ignore) (ein:notebooklist-new-notebook
|
||||
url-or-port
|
||||
ein:%notebooklist-new-kernel%))
|
||||
"New Notebook")
|
||||
(widget-insert " ")
|
||||
(widget-create
|
||||
'link
|
||||
:notify (lambda (&rest _ignore) (ein:notebooklist-reload nil t))
|
||||
"Resync")
|
||||
(widget-insert " ")
|
||||
(widget-create
|
||||
'link
|
||||
:notify (lambda (&rest _ignore)
|
||||
(browse-url (ein:url url-or-port)))
|
||||
"Open In Browser")
|
||||
|
||||
(widget-insert "\n\nCreate New Notebooks Using Kernel:\n")
|
||||
(let ((radio-widget
|
||||
(widget-create
|
||||
'radio-button-choice
|
||||
:notify (lambda (widget &rest _args)
|
||||
(let ((update (ein:get-kernelspec url-or-port
|
||||
(widget-value widget))))
|
||||
(unless (equal ein:%notebooklist-new-kernel% update)
|
||||
(when ein:%notebooklist-new-kernel%
|
||||
(message "New notebooks started with %s kernel"
|
||||
(ein:$kernelspec-display-name update)))
|
||||
(setq ein:%notebooklist-new-kernel% update)))))))
|
||||
(if kernels
|
||||
(let ((initial (ein:jupyter-get-default-kernel kernels)))
|
||||
(dolist (k kernels)
|
||||
(let ((child (widget-radio-add-item
|
||||
radio-widget
|
||||
(list 'item
|
||||
:value (car k)
|
||||
:format (format "%s\n" (cdr k))))))
|
||||
(when (string= initial (car k))
|
||||
(widget-apply-action (widget-get child :button)))))
|
||||
(widget-insert "\n"))
|
||||
(widget-insert "\n No kernels found\n"))))))
|
||||
|
||||
(defun ein:format-nbitem-data (name last-modified)
|
||||
(let ((dt (date-to-time last-modified)))
|
||||
(format "%-40s%+20s" name
|
||||
(ein:format-time-string ein:notebooklist-date-format dt))))
|
||||
|
||||
(defun render-directory (url-or-port sessions)
|
||||
;; SESSIONS is a hashtable of path to (session-id . kernel-id) pairs
|
||||
(with-current-buffer (ein:notebooklist-get-buffer url-or-port)
|
||||
(cl-loop with reloader = (apply-partially (lambda (nblist _kernel)
|
||||
(ein:notebooklist-reload nblist))
|
||||
ein:%notebooklist%)
|
||||
for note in (ein:notebooklist--order-data
|
||||
(ein:$notebooklist-data ein:%notebooklist%)
|
||||
ein:notebooklist-sort-field
|
||||
ein:notebooklist-sort-order)
|
||||
for name = (plist-get note :name)
|
||||
for path = (plist-get note :path)
|
||||
for last-modified = (plist-get note :last_modified)
|
||||
for type = (plist-get note :type)
|
||||
do (ein:notebook-get-opened-notebook url-or-port path)
|
||||
if (string= type "directory")
|
||||
do (progn (widget-create
|
||||
'link
|
||||
:notify (let ((url-or-port url-or-port)
|
||||
(name name))
|
||||
(lambda (&rest _ignore)
|
||||
;; each directory creates a whole new notebooklist
|
||||
(ein:notebooklist-open* url-or-port
|
||||
(concat (file-name-as-directory
|
||||
(ein:$notebooklist-path ein:%notebooklist%))
|
||||
name)
|
||||
nil
|
||||
(lambda (buffer _url-or-port) (pop-to-buffer buffer)))))
|
||||
"Dir")
|
||||
(widget-insert " : " name)
|
||||
(widget-insert "\n"))
|
||||
end
|
||||
if (string= type "file")
|
||||
do (progn (widget-create
|
||||
'link
|
||||
:notify (apply-partially
|
||||
(lambda (url-or-port* path* &rest _args)
|
||||
(ein:file-open url-or-port* path*))
|
||||
url-or-port path)
|
||||
"Open")
|
||||
(widget-insert " ")
|
||||
(widget-insert " : " (ein:format-nbitem-data name last-modified))
|
||||
(widget-insert "\n"))
|
||||
end
|
||||
if (string= type "notebook")
|
||||
do (progn (widget-create
|
||||
'link
|
||||
:notify (apply-partially
|
||||
(lambda (url-or-port* path* &rest _args)
|
||||
(ein:notebook-open url-or-port* path*))
|
||||
url-or-port path)
|
||||
"Open")
|
||||
(widget-insert " ")
|
||||
(if (gethash path sessions)
|
||||
(widget-create
|
||||
'link
|
||||
:notify
|
||||
(apply-partially
|
||||
(cl-function
|
||||
(lambda (url-or-port*
|
||||
path*
|
||||
&rest _ignore
|
||||
&aux (callback (lambda (_kernel) t)))
|
||||
(ein:message-whir
|
||||
"Ending session" (var callback)
|
||||
(ein:kernel-delete-session callback
|
||||
:url-or-port url-or-port*
|
||||
:path path*))))
|
||||
url-or-port path)
|
||||
"Stop")
|
||||
(widget-insert "[----]"))
|
||||
(widget-insert " ")
|
||||
(widget-create
|
||||
'link
|
||||
:notify (apply-partially
|
||||
(lambda (notebooklist* url-or-port* path* callback*
|
||||
&rest _args)
|
||||
(when (or noninteractive
|
||||
(y-or-n-p (format "Delete notebook %s?" path*)))
|
||||
(ein:notebooklist-delete-notebook
|
||||
notebooklist* url-or-port* path*
|
||||
(apply-partially callback* nil))))
|
||||
ein:%notebooklist% url-or-port path reloader)
|
||||
"Delete")
|
||||
(widget-insert " : " (ein:format-nbitem-data name last-modified))
|
||||
(widget-insert "\n"))
|
||||
end)))
|
||||
|
||||
(defun ein:notebooklist-render (url-or-port restore-point sessions)
|
||||
(with-current-buffer (ein:notebooklist-get-buffer url-or-port)
|
||||
(if (not (ein:$notebooklist-path ein:%notebooklist%))
|
||||
(ein:log 'error "ein:notebooklist-render: cannot render null")
|
||||
(render-header url-or-port sessions)
|
||||
(render-directory url-or-port sessions)
|
||||
(widget-setup)
|
||||
(awhen (get-buffer-window (current-buffer))
|
||||
(set-window-point it (or restore-point (point-min)))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:notebooklist-list-paths (&optional content-type)
|
||||
"Return all files of CONTENT-TYPE for all sessions"
|
||||
(apply #'append
|
||||
(cl-loop for nblist in (ein:notebooklist-list)
|
||||
for url-or-port = (ein:$notebooklist-url-or-port nblist)
|
||||
collect
|
||||
(cl-loop for content in (ein:content-need-hierarchy url-or-port)
|
||||
when (or (null content-type)
|
||||
(string= (ein:$content-type content) content-type))
|
||||
collect (ein:url url-or-port (ein:$content-path content))))))
|
||||
|
||||
|
||||
(defun ein:notebooklist-parse-nbpath (nbpath)
|
||||
"Return `(,url-or-port ,path) from URL-OR-PORT/PATH"
|
||||
(cl-loop for url-or-port in (ein:notebooklist-keys)
|
||||
if (cl-search url-or-port nbpath :end2 (length url-or-port))
|
||||
return (list (substring nbpath 0 (length url-or-port))
|
||||
(substring nbpath (1+ (length url-or-port))))
|
||||
end
|
||||
finally (ein:display-warning
|
||||
(format "%s not among: %s" nbpath (ein:notebooklist-keys))
|
||||
:error)))
|
||||
|
||||
(defsubst ein:notebooklist-ask-path (&optional content-type)
|
||||
(ein:completing-read (format "Open %s: " content-type)
|
||||
(ein:notebooklist-list-paths content-type)
|
||||
nil t))
|
||||
|
||||
;;;###autoload
|
||||
|
||||
(defun ein:notebooklist-load (&optional url-or-port)
|
||||
"Load notebook list but do not pop-up the notebook list buffer.
|
||||
|
||||
For example, if you want to load notebook list when Emacs starts,
|
||||
add this in the Emacs initialization file:
|
||||
|
||||
(add-to-hook \\='after-init-hook \\='ein:notebooklist-load)
|
||||
|
||||
or even this (if you want fast Emacs start-up):
|
||||
|
||||
;; load notebook list if Emacs is idle for 3 sec after start-up
|
||||
(run-with-idle-timer 3 nil #\\='ein:notebooklist-load)"
|
||||
(ein:notebooklist-open* url-or-port))
|
||||
|
||||
;;; Login
|
||||
|
||||
(defun ein:notebooklist-login--iteration (url-or-port callback errback token iteration response-status)
|
||||
(ein:log 'debug "Login attempt #%d in response to %s from %s."
|
||||
iteration response-status url-or-port)
|
||||
(setq callback (or callback #'ignore))
|
||||
(setq errback (or errback #'ignore))
|
||||
(let* ((reset-p (not response-status))
|
||||
(request-curl-options (if reset-p
|
||||
(cons "--junk-session-cookies" request-curl-options)
|
||||
request-curl-options))
|
||||
(parsed-url (url-generic-parse-url (file-name-as-directory url-or-port)))
|
||||
(host (url-host parsed-url))
|
||||
(query (cdr (url-path-and-query parsed-url))))
|
||||
(when reset-p
|
||||
(remhash host ein:query-xsrf-cache))
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url url-or-port (if query "" "login"))
|
||||
;; do not use :type "POST" here (see git history)
|
||||
:timeout ein:notebooklist-login-timeout
|
||||
:data (when (and token (not query)) (concat "password=" (url-hexify-string token)))
|
||||
:parser #'ein:notebooklist-login--parser
|
||||
:complete (apply-partially #'ein:notebooklist-login--complete url-or-port)
|
||||
:error (apply-partially #'ein:notebooklist-login--error url-or-port token
|
||||
callback errback iteration)
|
||||
:success (apply-partially #'ein:notebooklist-login--success url-or-port callback
|
||||
errback token iteration))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:notebooklist-open (url-or-port callback)
|
||||
"This is now an alias for `ein:notebooklist-login'."
|
||||
(interactive `(,(ein:notebooklist-ask-url-or-port)
|
||||
,(lambda (buffer _url-or-port) (pop-to-buffer buffer))))
|
||||
(ein:notebooklist-login url-or-port callback))
|
||||
|
||||
(make-obsolete 'ein:notebooklist-open 'ein:notebooklist-login "0.14.2")
|
||||
|
||||
;;;###autoload
|
||||
(defalias 'ein:login 'ein:notebooklist-login)
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:notebooklist-login (url-or-port callback &optional cookie-name cookie-content token)
|
||||
"Deal with security before main entry of ein:notebooklist-open*.
|
||||
CALLBACK takes two arguments, the buffer created by
|
||||
ein:notebooklist-open--success and the url-or-port argument of
|
||||
ein:notebooklist-open*."
|
||||
(interactive `(,(ein:notebooklist-ask-url-or-port)
|
||||
,(lambda (buffer _url-or-port) (pop-to-buffer buffer))
|
||||
,(when current-prefix-arg
|
||||
(read-no-blanks-input "Cookie name: "))
|
||||
,(when current-prefix-arg
|
||||
(read-no-blanks-input "Cookie content: "))
|
||||
nil))
|
||||
(when cookie-name
|
||||
(let* ((parsed-url (url-generic-parse-url (file-name-as-directory url-or-port)))
|
||||
(domain (url-host parsed-url))
|
||||
(securep (string-match "^wss://" url-or-port))
|
||||
(line (mapconcat #'identity (list domain "FALSE" (car (url-path-and-query parsed-url)) (if securep "TRUE" "FALSE") "0" cookie-name (concat cookie-content "\n")) "\t")))
|
||||
(write-region line nil (request--curl-cookie-jar) 'append)))
|
||||
(let ((token (or token (ein:notebooklist-token-or-password url-or-port))))
|
||||
(cond ((null token) ;; don't know
|
||||
(ein:notebooklist-login--iteration url-or-port callback nil nil -1 nil))
|
||||
((string= token "") ;; all authentication disabled
|
||||
(ein:log 'verbose "Skipping login %s" url-or-port)
|
||||
(ein:notebooklist-open* url-or-port nil nil callback nil))
|
||||
(t
|
||||
(ein:notebooklist-login--iteration url-or-port callback nil token 0 nil)))))
|
||||
|
||||
(defun ein:notebooklist-login--parser ()
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(when (re-search-forward "<input type=.?password" nil t)
|
||||
(list :reprompt t))))
|
||||
|
||||
(defun ein:notebooklist-login--success-1 (url-or-port callback errback &optional hub-p)
|
||||
(ein:log 'info "Login to %s complete." url-or-port)
|
||||
(ein:notebooklist-open* url-or-port nil nil callback errback hub-p))
|
||||
|
||||
(defun ein:notebooklist-login--error-1 (url-or-port error-thrown response errback)
|
||||
(ein:log 'error "Login to %s failed, error-thrown %s, raw-header %s"
|
||||
url-or-port
|
||||
(subst-char-in-string ?\n ?\ (format "%s" error-thrown))
|
||||
(request-response--raw-header response))
|
||||
(funcall errback))
|
||||
|
||||
(cl-defun ein:notebooklist-login--complete
|
||||
(_url-or-port
|
||||
&key data response
|
||||
&allow-other-keys &aux
|
||||
(resp-string (format "STATUS: %s DATA: %s"
|
||||
(request-response-status-code response) data)))
|
||||
(ein:log 'debug "ein:notebooklist-login--complete %s" resp-string))
|
||||
|
||||
(cl-defun ein:notebooklist-login--success
|
||||
(url-or-port callback errback token iteration
|
||||
&key data response error-thrown
|
||||
&allow-other-keys &aux
|
||||
(response-status (request-response-status-code response))
|
||||
(hub-p (request-response-header response "x-jupyterhub-version")))
|
||||
(if (plist-get data :reprompt)
|
||||
(cond ((>= iteration 0)
|
||||
(ein:notebooklist-login--error-1 url-or-port error-thrown response errback))
|
||||
(hub-p (ein:notebooklist-open* url-or-port nil nil callback errback t))
|
||||
(t (setq token (read-passwd (format "Password for %s: " url-or-port)))
|
||||
(ein:notebooklist-login--iteration url-or-port callback errback token
|
||||
(1+ iteration) response-status)))
|
||||
(ein:notebooklist-login--success-1 url-or-port callback errback hub-p)))
|
||||
|
||||
(cl-defun ein:notebooklist-login--error
|
||||
(url-or-port token callback errback iteration
|
||||
&key _data response error-thrown
|
||||
&allow-other-keys &aux
|
||||
(response-status (request-response-status-code response))
|
||||
(hub-p (request-response-header response "x-jupyterhub-version")))
|
||||
(cond (hub-p
|
||||
(if (< iteration 0)
|
||||
(ein:notebooklist-login--iteration url-or-port callback errback
|
||||
token (1+ iteration) response-status)
|
||||
(if (and (eq response-status 405)) ;; no javascript is okay
|
||||
(ein:notebooklist-login--success-1 url-or-port callback errback hub-p)
|
||||
(ein:notebooklist-login--error-1 url-or-port error-thrown response errback))))
|
||||
((and response-status (< iteration 0))
|
||||
(setq token (read-passwd (format "Password for %s: " url-or-port)))
|
||||
(ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status))
|
||||
((and (eq response-status 403) (< iteration 1))
|
||||
(ein:notebooklist-login--iteration url-or-port callback errback token (1+ iteration) response-status))
|
||||
(t (ein:notebooklist-login--error-1 url-or-port error-thrown response errback))))
|
||||
|
||||
(defun ein:get-url-or-port--notebooklist ()
|
||||
(when (ein:$notebooklist-p ein:%notebooklist%)
|
||||
(ein:$notebooklist-url-or-port ein:%notebooklist%)))
|
||||
|
||||
(defun ein:notebooklist-prev-item () (interactive) (move-beginning-of-line 0))
|
||||
(defun ein:notebooklist-next-item () (interactive) (move-beginning-of-line 2))
|
||||
|
||||
(defvar ein:notebooklist-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(set-keymap-parent map (make-composed-keymap widget-keymap
|
||||
special-mode-map))
|
||||
(define-key map "\C-c\C-r" 'ein:notebooklist-reload)
|
||||
(define-key map "\C-c\C-f" 'ein:file-open)
|
||||
(define-key map "\C-c\C-o" 'ein:notebook-open)
|
||||
(define-key map "p" 'ein:notebooklist-prev-item)
|
||||
(define-key map "n" 'ein:notebooklist-next-item)
|
||||
map)
|
||||
"Keymap for ein:notebooklist-mode.")
|
||||
|
||||
(easy-menu-define ein:notebooklist-menu ein:notebooklist-mode-map
|
||||
"EIN Notebook List Mode Menu"
|
||||
`("EIN Notebook List"
|
||||
,@(ein:generate-menu
|
||||
'(("Reload" ein:notebooklist-reload)
|
||||
("New Notebook" ein:notebooklist-new-notebook)
|
||||
("New Notebook (with name)"
|
||||
ein:notebooklist-new-notebook-with-name)))))
|
||||
|
||||
(define-derived-mode ein:notebooklist-mode special-mode "ein:notebooklist"
|
||||
"IPython notebook list mode.
|
||||
Commands:
|
||||
\\{ein:notebooklist-mode-map}"
|
||||
(set (make-local-variable 'revert-buffer-function)
|
||||
(lambda (&rest _args) (ein:notebooklist-reload))))
|
||||
|
||||
|
||||
(provide 'ein-notebooklist)
|
||||
|
||||
;;; ein-notebooklist.el ends here
|
||||
180
lisp/ein/ein-notification.el
Normal file
180
lisp/ein/ein-notification.el
Normal file
@@ -0,0 +1,180 @@
|
||||
;;; ein-notification.el --- Notification widget for Notebook -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-notification.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-notification.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-notification.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
|
||||
(require 'eieio)
|
||||
|
||||
(require 'ein-core)
|
||||
(require 'ein-classes)
|
||||
(require 'ein-events)
|
||||
|
||||
(declare-function ein:get-notebook "ein:notebook")
|
||||
(declare-function ein:notebook-opened-buffer-names "ein:notebook")
|
||||
(declare-function ein:list-available-kernels "ein:notebook")
|
||||
(declare-function ein:notebook-switch-kernel "ein:notebook")
|
||||
|
||||
(define-obsolete-variable-alias 'ein:@notification 'ein:%notification% "0.1.2")
|
||||
(ein:deflocal ein:%notification% nil
|
||||
"Buffer local variable to hold an instance of `ein:notification'.")
|
||||
|
||||
(defvar ein:header-line-format '(:eval (ein:header-line)))
|
||||
(defvar ein:header-line-switch-kernel-map (make-sparse-keymap))
|
||||
|
||||
(cl-defmethod ein:notification-status-set ((ns ein:notification-status) status)
|
||||
(let* ((message (cdr (assoc status (slot-value ns 's2m)))))
|
||||
(setf (slot-value ns 'status) status)
|
||||
(setf (slot-value ns 'message) (substitute-command-keys message))
|
||||
(force-mode-line-update t)))
|
||||
|
||||
(cl-defmethod ein:notification-bind-events ((notification ein:notification) events)
|
||||
"Bind a callback to events of the event handler EVENTS which
|
||||
just set the status (= event-type):
|
||||
(ein:notification-status-set NS EVENT-TYPE)
|
||||
where NS is `:kernel' or `:notebook' slot of NOTIFICATION."
|
||||
(cl-loop for ns in (list (slot-value notification 'kernel)
|
||||
(slot-value notification 'notebook))
|
||||
for statuses = (mapcar #'car (slot-value ns 's2m))
|
||||
do (cl-loop for st in statuses
|
||||
do (ein:events-on events
|
||||
st ; = event-type
|
||||
#'ein:notification--callback
|
||||
(cons ns st))))
|
||||
(ein:events-on events
|
||||
'notebook_saved.Notebook
|
||||
#'ein:notification--fadeout-callback
|
||||
(list (slot-value notification 'notebook)
|
||||
"Notebook is saved"
|
||||
'notebook_saved.Notebook
|
||||
nil))
|
||||
(ein:events-on events
|
||||
'execution_count.Kernel
|
||||
#'ein:notification--set-execution-count
|
||||
notification))
|
||||
|
||||
(defun ein:notification--callback (packed _data)
|
||||
(let ((ns (car packed))
|
||||
(status (cdr packed)))
|
||||
(ein:notification-status-set ns status)))
|
||||
|
||||
(defun ein:notification--set-execution-count (notification count)
|
||||
(setf (oref notification :execution-count) count))
|
||||
|
||||
(defun ein:notification--fadeout-callback (packed _data)
|
||||
;; FIXME: I can simplify this.
|
||||
;; Do not pass around message, for exmaple.
|
||||
(cl-destructuring-bind (ns message status &rest) packed
|
||||
(setf (oref ns :status) status)
|
||||
(setf (oref ns :message) message)
|
||||
(apply #'run-at-time
|
||||
1 nil
|
||||
(lambda (ns _message status next)
|
||||
(when (equal (slot-value ns 'status) status)
|
||||
(ein:notification-status-set ns next)
|
||||
;; (ein:with-live-buffer (slot-value ns :buffer)
|
||||
;; (force-mode-line-update))
|
||||
))
|
||||
packed)))
|
||||
|
||||
(defun ein:notification-setup (buffer events &rest tab-slots)
|
||||
"Setup a new notification widget in the BUFFER.
|
||||
This function saves the new notification widget instance in the
|
||||
local variable of the BUFFER.
|
||||
|
||||
Rest of the arguments are for TABs in `header-line'.
|
||||
|
||||
GET-LIST : function
|
||||
Return a list of worksheets.
|
||||
|
||||
GET-CURRENT : function
|
||||
Return the current worksheet.
|
||||
|
||||
GET-NAME : function
|
||||
Return a name of the worksheet given as its argument.
|
||||
|
||||
\(fn buffer events &key get-list get-current)"
|
||||
(with-current-buffer buffer
|
||||
(setq ein:%notification%
|
||||
(make-instance 'ein:notification
|
||||
:buffer buffer))
|
||||
(setq header-line-format ein:header-line-format)
|
||||
(ein:notification-bind-events ein:%notification% events)
|
||||
(setf (oref ein:%notification% :tab)
|
||||
(apply #'make-instance 'ein:notification-tab tab-slots))
|
||||
ein:%notification%))
|
||||
|
||||
(defface ein:notification-tab-normal
|
||||
'((t :inherit (header-line) :underline t :height 0.8))
|
||||
"Face for headline selected tab."
|
||||
:group 'ein)
|
||||
|
||||
(define-key ein:header-line-switch-kernel-map
|
||||
[header-line mouse-1] 'ein:header-line-switch-kernel)
|
||||
|
||||
(defmacro ein:with-destructuring-bind-key-event (key-event &rest body)
|
||||
(declare (debug (form &rest form))
|
||||
(indent 1))
|
||||
;; See: (info "(elisp) Click Events")
|
||||
`(cl-destructuring-bind
|
||||
(event-type
|
||||
(window pos-or-area (x . y) timestamp
|
||||
object text-pos (col . row)
|
||||
image (dx . dy) (width . height)))
|
||||
,key-event
|
||||
,@body))
|
||||
|
||||
(defun ein:header-line-switch-kernel (_key-event)
|
||||
(interactive "e")
|
||||
(let* ((notebook (or (ein:get-notebook)
|
||||
(ein:completing-read
|
||||
"Select notebook: "
|
||||
(ein:notebook-opened-buffer-names))))
|
||||
(kernel-name (ein:completing-read
|
||||
"Select kernel: "
|
||||
(ein:list-available-kernels (ein:$notebook-url-or-port notebook)))))
|
||||
(ein:notebook-switch-kernel notebook kernel-name)))
|
||||
|
||||
(defun ein:header-line ()
|
||||
(format
|
||||
"IP[%s]: %s"
|
||||
(slot-value ein:%notification% 'execution-count)
|
||||
(ein:join-str
|
||||
" | "
|
||||
(cl-remove-if-not
|
||||
#'identity
|
||||
(list (slot-value (slot-value ein:%notification% 'notebook) 'message)
|
||||
(slot-value (slot-value ein:%notification% 'kernel) 'message)
|
||||
(propertize (aif (aand (ein:get-notebook) (ein:$notebook-kernelspec it))
|
||||
(format "|%s|" (ein:$kernelspec-name it))
|
||||
"|unknown: please click and select a kernel|")
|
||||
'keymap ein:header-line-switch-kernel-map
|
||||
'help-echo "Click (mouse-1) to change the running kernel."
|
||||
'mouse-face 'highlight
|
||||
'face 'ein:notification-tab-normal))))))
|
||||
|
||||
(provide 'ein-notification)
|
||||
|
||||
;;; ein-notification.el ends here
|
||||
187
lisp/ein/ein-output-area.el
Normal file
187
lisp/ein/ein-output-area.el
Normal file
@@ -0,0 +1,187 @@
|
||||
;;; ein-output-area.el --- Output area module
|
||||
|
||||
;; Copyright (C) 2012 Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-output-area.el is free software: you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation, either version 3 of the
|
||||
;; License, or (at your option) any later version.
|
||||
|
||||
;; ein-output-area.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-output-area.el.
|
||||
;; If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'xml)
|
||||
(require 'shr)
|
||||
(require 'ein-core)
|
||||
|
||||
|
||||
(defvar ein:output-area-case-types '(:image/svg+xml :image/png :image/jpeg :text/plain :text/html :application/latex :application/tex :application/javascript)
|
||||
"Prefer :text/plain.
|
||||
Unless it's a single line \"<IPython.core.display.HTML object>\" or
|
||||
\"TemporalData[TimeSeries, <<1>>]\" in which case prefer :text/html.")
|
||||
|
||||
(defcustom ein:output-area-inlined-images nil
|
||||
"Turn on to insert images into buffer. Default spawns external viewer."
|
||||
:type 'boolean
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:output-area-inlined-image-properties '(:foreground "black" :background "white")
|
||||
"Additional properties for inlined images.
|
||||
This is passed to `create-image' for some supported image types,
|
||||
such as SVG ones whose foregrounds are taken from the current
|
||||
frame by default and may appear unreadable."
|
||||
:type '(plist :value-type color)
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:shr-env
|
||||
'((shr-table-horizontal-line ?-)
|
||||
(shr-table-vertical-line ?|)
|
||||
(shr-table-corner ?+))
|
||||
"Variables let-bound while calling `shr-insert-document'.
|
||||
|
||||
To use default shr setting:
|
||||
|
||||
(setq ein:shr-env nil)
|
||||
|
||||
Draw boundaries for table (default):
|
||||
|
||||
(setq ein:shr-env
|
||||
\\='((shr-table-horizontal-line ?-)
|
||||
(shr-table-vertical-line ?|)
|
||||
(shr-table-corner ?+)))
|
||||
"
|
||||
:type '(sexp)
|
||||
:group 'ein)
|
||||
|
||||
;;; XML/HTML utils
|
||||
|
||||
(defun ein:xml-parse-html-string (html-string)
|
||||
"Parse HTML-STRING and return a dom object which
|
||||
can be handled by the xml module."
|
||||
(with-temp-buffer
|
||||
(insert html-string)
|
||||
(when (fboundp 'libxml-parse-html-region)
|
||||
(cl-loop with result
|
||||
repeat 3
|
||||
do (setq result
|
||||
(libxml-parse-html-region (point-min) (point-max)))
|
||||
until result
|
||||
finally return result))))
|
||||
|
||||
(defalias 'ein:xml-node-p 'listp)
|
||||
|
||||
(defun ein:xml-tree-apply (dom operation)
|
||||
"Apply OPERATION on nodes in DOM. Apply the same OPERATION on
|
||||
the next level children when it returns `nil'."
|
||||
(cl-loop for child in (xml-node-children dom)
|
||||
if (and (not (funcall operation child))
|
||||
(ein:xml-node-p child))
|
||||
do (ein:xml-tree-apply child operation)))
|
||||
|
||||
(defun ein:xml-replace-attributes (dom tag attr replace-p replacer)
|
||||
"Replace value of ATTR of TAG in DOM using REPLACER
|
||||
when REPLACE-P returns non-`nil'."
|
||||
(ein:xml-tree-apply
|
||||
dom
|
||||
(lambda (node)
|
||||
(ein:and-let* (((ein:xml-node-p node))
|
||||
((eq (xml-node-name node) tag))
|
||||
(attr-cell (assoc attr (xml-node-attributes node)))
|
||||
(val (cdr attr-cell))
|
||||
((funcall replace-p val)))
|
||||
(setcdr attr-cell (funcall replacer val))
|
||||
t))))
|
||||
|
||||
|
||||
(defun ein:output-area-get-html-renderer ()
|
||||
(if (fboundp 'libxml-parse-xml-region)
|
||||
#'ein:insert-html-shr
|
||||
#'ein:insert-read-only))
|
||||
|
||||
(defun ein:shr-insert-document (dom)
|
||||
"`shr-insert-document' with EIN setting."
|
||||
(eval `(let ,ein:shr-env (shr-insert-document dom))))
|
||||
|
||||
(defun ein:insert-html-shr (html-string)
|
||||
"Render HTML-STRING using `shr-insert-document'.
|
||||
|
||||
Usage::
|
||||
|
||||
(ein:insert-html-shr \"<b>HTML</b> string\")
|
||||
|
||||
"
|
||||
(let ((dom (ein:xml-parse-html-string html-string))
|
||||
(start (point))
|
||||
end
|
||||
(buffer-undo-list t))
|
||||
(ein:insert-html--fix-urls dom)
|
||||
(ein:shr-insert-document dom)
|
||||
(setq end (point))
|
||||
(put-text-property start end 'read-only t)
|
||||
(put-text-property start end 'front-sticky t)))
|
||||
|
||||
(defun ein:insert-html--fix-urls (dom &optional url-or-port)
|
||||
"Destructively prepend notebook server URL to local URLs in DOM."
|
||||
(ein:and-let* ((url-or-port (or url-or-port (ein:get-url-or-port)))
|
||||
(replace-p (lambda (val) (string-match-p "^/?files/" val)))
|
||||
(replacer (lambda (val) (ein:url url-or-port val))))
|
||||
(ein:xml-replace-attributes dom 'a 'href replace-p replacer)
|
||||
(ein:xml-replace-attributes dom 'img 'src replace-p replacer)))
|
||||
|
||||
(defun ein:output-area-type (mime-type)
|
||||
"Investigate why :image/svg+xml to :svg and :text/plain to :text"
|
||||
(let* ((mime-str (if (symbolp mime-type) (symbol-name mime-type) mime-type))
|
||||
(minor-kw (car (nreverse (split-string mime-str "/"))))
|
||||
(minor (car (nreverse (split-string minor-kw ":")))))
|
||||
(intern (concat ":"
|
||||
(cond ((string= minor "plain") "text")
|
||||
(t (cl-subseq minor 0 (cl-search "+" minor))))))))
|
||||
|
||||
(defun ein:output-area-convert-mime-types (json data)
|
||||
(let ((known-mimes (cl-remove-if-not
|
||||
#'identity
|
||||
(mapcar (lambda (x) (intern-soft (concat ":" x)))
|
||||
(mailcap-mime-types)))))
|
||||
(mapc (lambda (x)
|
||||
(-when-let* ((mime-val (plist-get data x))
|
||||
(minor-kw (ein:output-area-type x)))
|
||||
(setq json (plist-put json minor-kw mime-val))))
|
||||
known-mimes)
|
||||
json))
|
||||
|
||||
(defmacro ein:output-area-case-type (json &rest case-body)
|
||||
`(let* ((types (cl-copy-list ein:output-area-case-types))
|
||||
(heuristic-p (and (memq :text/plain types)
|
||||
(memq :text/html types)))
|
||||
(,json (or (plist-get ,json :data) ,json))
|
||||
(plain (plist-get ,json :text/plain))
|
||||
(html (plist-get ,json :text/html)))
|
||||
(when (and heuristic-p
|
||||
(stringp plain) (< (length plain) 60)
|
||||
(stringp html) (> (length html) 300))
|
||||
(delq :text/plain types))
|
||||
(seq-some (lambda (type)
|
||||
(when-let ((value (plist-get ,json type)))
|
||||
,@case-body
|
||||
t))
|
||||
types)))
|
||||
|
||||
(provide 'ein-output-area)
|
||||
|
||||
;;; ein-output-area.el ends here
|
||||
98
lisp/ein/ein-pager.el
Normal file
98
lisp/ein/ein-pager.el
Normal file
@@ -0,0 +1,98 @@
|
||||
;;; ein-pager.el --- Pager module -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-pager.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-pager.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-pager.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ansi-color)
|
||||
|
||||
(require 'ein-core)
|
||||
(require 'ein-events)
|
||||
(require 'view)
|
||||
|
||||
;; FIXME: Make a class with `:get-notebook-name' slot like `ein:worksheet'
|
||||
|
||||
(declare-function ess-help-underline "ess-help")
|
||||
|
||||
(defun ein:pager-new (name events)
|
||||
;; currently pager = name.
|
||||
(ein:pager-bind-events name events)
|
||||
name)
|
||||
|
||||
(defun ein:pager-bind-events (pager events)
|
||||
"Bind events related to PAGER to the event handler EVENTS."
|
||||
(ein:events-on events
|
||||
'open_with_text.Pager
|
||||
#'ein:pager--open-with-text
|
||||
pager))
|
||||
|
||||
(defun ein:pager--open-with-text (pager data)
|
||||
(let ((text (plist-get data :text)))
|
||||
(unless (equal (ein:trim text) "")
|
||||
(ein:pager-clear pager)
|
||||
(ein:pager-expand pager)
|
||||
(ein:pager-append-text pager text))))
|
||||
|
||||
(defun ein:pager-clear (pager)
|
||||
(ein:with-read-only-buffer (get-buffer-create pager)
|
||||
(erase-buffer)))
|
||||
|
||||
(defun ein:pager-expand (pager)
|
||||
(pop-to-buffer (get-buffer-create pager))
|
||||
(goto-char (point-min)))
|
||||
|
||||
(defun ein:pager-append-text (pager text)
|
||||
(ein:with-read-only-buffer (get-buffer-create pager)
|
||||
(insert (ansi-color-apply text))
|
||||
(if (featurep 'ess-help)
|
||||
(ess-help-underline))
|
||||
(unless (eql 'ein:pager-mode major-mode)
|
||||
(ein:pager-mode))))
|
||||
|
||||
;; FIXME: this should be automatically called when opening pager.
|
||||
(defun ein:pager-goto-docstring-bset-loc ()
|
||||
"Goto the best location of the documentation."
|
||||
(interactive)
|
||||
(goto-char (point-min))
|
||||
(search-forward-regexp "^Docstring:")
|
||||
(beginning-of-line 0)
|
||||
(recenter 0))
|
||||
|
||||
(defvar ein:pager-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map "\C-c\C-b" 'ein:pager-goto-docstring-bset-loc)
|
||||
map)
|
||||
"Keymap for ein:pager-mode.")
|
||||
|
||||
(define-derived-mode ein:pager-mode view-mode "ein:pager"
|
||||
"IPython notebook pager mode.
|
||||
Commands:
|
||||
\\{ein:pager-mode-map}"
|
||||
(setq-local view-no-disable-on-exit t)
|
||||
(font-lock-mode))
|
||||
|
||||
|
||||
(provide 'ein-pager)
|
||||
|
||||
;;; ein-pager.el ends here
|
||||
15
lisp/ein/ein-pkg.el
Normal file
15
lisp/ein/ein-pkg.el
Normal file
@@ -0,0 +1,15 @@
|
||||
(define-package "ein" "20230827.325" "jupyter notebook client"
|
||||
'((emacs "26.1")
|
||||
(websocket "1.12")
|
||||
(anaphora "1.0.4")
|
||||
(request "0.3.3")
|
||||
(deferred "0.5")
|
||||
(polymode "0.2.2")
|
||||
(dash "2.13.0")
|
||||
(with-editor "0pre"))
|
||||
:commit "ac92eb92eac35a9542485969487e43f5318825a1" :keywords
|
||||
'("jupyter" "literate programming" "reproducible research")
|
||||
:url "https://github.com/dickmao/emacs-ipython-notebook")
|
||||
;; Local Variables:
|
||||
;; no-byte-compile: t
|
||||
;; End:
|
||||
215
lisp/ein/ein-process.el
Normal file
215
lisp/ein/ein-process.el
Normal file
@@ -0,0 +1,215 @@
|
||||
;;; ein-process.el --- Notebook list buffer -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2018- John M. Miller
|
||||
|
||||
;; Authors: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
;; John M. Miller <millejoh at mac.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-process.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-process.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-process.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein-jupyter)
|
||||
|
||||
(defcustom ein:process-jupyter-regexp "\\(jupyter\\|ipython\\)\\(-\\|\\s-+\\)note"
|
||||
"Regexp by which we recognize notebook servers."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ein:process-lsof "lsof"
|
||||
"Executable for lsof command."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
(defun ein:process-divine-dir (pid args &optional error-buffer)
|
||||
"Returns notebook-dir or cwd of PID. Supply ERROR-BUFFER to capture stderr"
|
||||
(if (string-match "\\bnotebook-dir\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
||||
(directory-file-name (match-string 2 args))
|
||||
(if (executable-find ein:process-lsof)
|
||||
(ein:trim-right
|
||||
(with-output-to-string
|
||||
(shell-command (format "%s -p %d -a -d cwd -Fn | grep ^n | tail -c +2"
|
||||
ein:process-lsof pid)
|
||||
standard-output error-buffer))))))
|
||||
|
||||
(defun ein:process-divine-port (pid args &optional error-buffer)
|
||||
"Returns port on which PID is listening or 0 if none.
|
||||
Supply ERROR-BUFFER to capture stderr."
|
||||
(if (string-match "\\bport\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
||||
(string-to-number (match-string 2 args))
|
||||
(if (executable-find ein:process-lsof)
|
||||
(string-to-number
|
||||
(ein:trim-right
|
||||
(with-output-to-string
|
||||
(shell-command (format "%s -p %d -a -iTCP -sTCP:LISTEN -Fn | grep ^n | sed \"s/[^0-9]//g\""
|
||||
ein:process-lsof pid)
|
||||
standard-output error-buffer)))))))
|
||||
|
||||
(defun ein:process-divine-ip (_pid args)
|
||||
"Returns notebook-ip of PID"
|
||||
(if (string-match "\\bip\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
||||
(match-string 2 args)
|
||||
ein:url-localhost))
|
||||
|
||||
(defcustom ein:process-jupyter-regexp "\\(jupyter\\|ipython\\)\\(-\\|\\s-+\\)note"
|
||||
"Regexp by which we recognize notebook servers."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
|
||||
(defcustom ein:process-lsof "lsof"
|
||||
"Executable for lsof command."
|
||||
:type 'string
|
||||
:group 'ein)
|
||||
|
||||
(cl-defstruct ein:$process
|
||||
"Hold process variables.
|
||||
|
||||
`ein:$process-pid' : integer
|
||||
PID.
|
||||
|
||||
`ein:$process-url': string
|
||||
URL
|
||||
|
||||
`ein:$process-dir' : string
|
||||
Arg of --notebook-dir or 'readlink -e /proc/<pid>/cwd'."
|
||||
pid
|
||||
url
|
||||
dir
|
||||
)
|
||||
|
||||
(ein:deflocal ein:%processes% (make-hash-table :test #'equal)
|
||||
"Process table of `ein:$process' keyed on dir.")
|
||||
|
||||
(defun ein:process-processes ()
|
||||
(hash-table-values ein:%processes%))
|
||||
|
||||
(defun ein:process-alive-p (proc)
|
||||
(process-attributes (ein:$process-pid proc)))
|
||||
|
||||
(defun ein:process-suitable-notebook-dir (filename)
|
||||
"Return the uppermost parent dir of DIR that contains ipynb files."
|
||||
(let ((fn (expand-file-name filename)))
|
||||
(cl-loop with directory = (directory-file-name
|
||||
(if (file-regular-p fn)
|
||||
(file-name-directory (directory-file-name fn))
|
||||
fn))
|
||||
with suitable = directory
|
||||
until (string= (file-name-nondirectory directory) "")
|
||||
do (if (directory-files directory nil "\\.ipynb$")
|
||||
(setq suitable directory))
|
||||
(setq directory (directory-file-name (file-name-directory directory)))
|
||||
finally return suitable)))
|
||||
|
||||
(defun ein:process-refresh-processes ()
|
||||
"Use `jupyter notebook list --json` to populate ein:%processes%"
|
||||
(clrhash ein:%processes%)
|
||||
(cl-loop for line in (condition-case err
|
||||
(apply #'process-lines
|
||||
ein:jupyter-server-command
|
||||
(append (split-string (or ein:jupyter-server-use-subcommand ""))
|
||||
'("list" "--json")))
|
||||
;; often there is no local jupyter installation
|
||||
(error (ein:log 'info "ein:process-refresh-processes: %s" err) nil))
|
||||
do (cl-destructuring-bind
|
||||
(&key pid url notebook_dir &allow-other-keys)
|
||||
(ein:json-read-from-string line)
|
||||
(puthash (directory-file-name notebook_dir)
|
||||
(make-ein:$process :pid pid
|
||||
:url (ein:url url)
|
||||
:dir (directory-file-name notebook_dir))
|
||||
ein:%processes%))))
|
||||
|
||||
(defun ein:process-dir-match (filename)
|
||||
"Return ein:process whose directory is prefix of FILENAME."
|
||||
(cl-loop for dir in (hash-table-keys ein:%processes%)
|
||||
when (cl-search dir filename)
|
||||
return (gethash dir ein:%processes%)))
|
||||
|
||||
(defun ein:process-url-match (url-or-port)
|
||||
"Return ein:process whose url matches URL-OR-PORT."
|
||||
(cl-loop with parsed-url-or-port = (url-generic-parse-url url-or-port)
|
||||
for proc in (ein:process-processes)
|
||||
for parsed-url-proc = (url-generic-parse-url (ein:process-url-or-port proc))
|
||||
when (and (string= (url-host parsed-url-or-port) (url-host parsed-url-proc))
|
||||
(= (url-port parsed-url-or-port) (url-port parsed-url-proc)))
|
||||
return proc))
|
||||
|
||||
(defsubst ein:process-url-or-port (proc)
|
||||
"Naively construct url-or-port from ein:process PROC's port and ip fields"
|
||||
(ein:$process-url proc))
|
||||
|
||||
(defsubst ein:process-path (proc filename)
|
||||
"Construct path by eliding PROC's dir from filename."
|
||||
(cl-subseq filename (length (file-name-as-directory (ein:$process-dir proc)))))
|
||||
|
||||
(defun ein:process-open-notebook* (filename callback)
|
||||
"Open FILENAME as a notebook and start a notebook server if necessary.
|
||||
CALLBACK with arity 2 (passed into `ein:notebook-open--callback')."
|
||||
(ein:process-refresh-processes)
|
||||
(let* ((proc (ein:process-dir-match filename)))
|
||||
(if proc
|
||||
(let* ((url-or-port (ein:process-url-or-port proc))
|
||||
(path (ein:process-path proc filename))
|
||||
(callback2 (apply-partially (lambda (path* callback* _buffer url-or-port)
|
||||
(ein:notebook-open
|
||||
url-or-port path* nil callback*))
|
||||
path callback)))
|
||||
(if (ein:notebooklist-list-get url-or-port)
|
||||
(ein:notebook-open url-or-port path nil callback)
|
||||
(ein:notebooklist-login url-or-port callback2)))
|
||||
(let* ((nbdir (read-directory-name "Notebook directory: "
|
||||
(ein:process-suitable-notebook-dir filename)))
|
||||
(path
|
||||
(concat (if ein:jupyter-use-containers
|
||||
(file-name-as-directory (file-name-base ein:jupyter-docker-mount-point))
|
||||
"")
|
||||
(cl-subseq (expand-file-name filename)
|
||||
(length (file-name-as-directory (expand-file-name nbdir))))))
|
||||
(callback2 (apply-partially (lambda (path* callback* buffer url-or-port)
|
||||
(pop-to-buffer buffer)
|
||||
(ein:notebook-open url-or-port
|
||||
path* nil callback*))
|
||||
path callback)))
|
||||
(ein:jupyter-server-start (executable-find ein:jupyter-server-command)
|
||||
nbdir nil callback2)))))
|
||||
|
||||
(defun ein:process-open-notebook (&optional filename buffer-callback)
|
||||
"When FILENAME is unspecified the variable `buffer-file-name'
|
||||
is used instead. BUFFER-CALLBACK is called after notebook opened."
|
||||
(interactive)
|
||||
(unless filename (setq filename buffer-file-name))
|
||||
(cl-assert filename nil "Not visiting a file")
|
||||
(let ((callback2 (apply-partially (lambda (buffer buffer-callback* _notebook _created
|
||||
&rest _args)
|
||||
(when (buffer-live-p buffer)
|
||||
(funcall buffer-callback* buffer)))
|
||||
(current-buffer) (or buffer-callback #'ignore))))
|
||||
(ein:process-open-notebook* (expand-file-name filename) callback2)))
|
||||
|
||||
(defun ein:process-find-file-callback ()
|
||||
"A callback function for `find-file-hook' to open notebook."
|
||||
(interactive)
|
||||
(-when-let* ((filename buffer-file-name)
|
||||
(match-p (string-match-p "\\.ipynb$" filename)))
|
||||
(ein:process-open-notebook filename #'kill-buffer-if-not-modified)))
|
||||
|
||||
(provide 'ein-process)
|
||||
|
||||
;;; ein-process.el ends here
|
||||
160
lisp/ein/ein-python-send.el
Normal file
160
lisp/ein/ein-python-send.el
Normal file
@@ -0,0 +1,160 @@
|
||||
;;; ein-python-send.el --- Ad hoc sending of code fragments to kernel -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2012- The Authors
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-python-send.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-python-send.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-python-send.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; python parsing code by authors of elpy (Schaefer et al)
|
||||
|
||||
;;; Code:
|
||||
|
||||
(autoload 'ein:get-notebook "ein-notebook")
|
||||
|
||||
(defvar ein:python-send-map)
|
||||
|
||||
(defun ein:python-send--prepare (&optional reset)
|
||||
(cl-assert (boundp 'ein:python-send-map) nil
|
||||
"ein:python-send--prepare: %s not called"
|
||||
"ein:python-send--init")
|
||||
(unless (and (buffer-live-p (current-buffer))
|
||||
(eq major-mode 'python-mode))
|
||||
(error "ein:python-send--prepare: %s is not a python buffer" (buffer-name)))
|
||||
(when (or (not (ein:get-notebook)) reset)
|
||||
(aif (ein:notebook-opened-notebooks)
|
||||
(let ((choice
|
||||
(ein:completing-read
|
||||
"Notebook: "
|
||||
(mapcar (lambda (nb) (ein:$notebook-notebook-name nb)) it))))
|
||||
(setq ein:%notebook% (seq-find
|
||||
(lambda (nb)
|
||||
(string= choice (ein:$notebook-notebook-name nb)))
|
||||
it)))
|
||||
(error "ein:python-send--prepare: No open notebooks"))))
|
||||
|
||||
(defun ein:python-send-region-or-buffer (&optional reset)
|
||||
"Based on `elpy-shell--send-region-or-buffer-internal' by Schaefer et al."
|
||||
(interactive "P")
|
||||
(ein:python-send--prepare reset)
|
||||
(if (use-region-p)
|
||||
(let ((region (python-shell-buffer-substring
|
||||
(region-beginning) (region-end))))
|
||||
(when (string-match "\t" region)
|
||||
(message "Region contained tabs, this might cause weird errors"))
|
||||
;; python-shell-buffer-substring (intentionally?) does not accurately
|
||||
;; respect (region-beginning); it always start on the first character
|
||||
;; of the respective line even if that's before the region beginning
|
||||
;; Here we post-process the output to remove the characters before
|
||||
;; (region-beginning) and the start of the line. The end of the region
|
||||
;; is handled correctly and needs no special treatment.
|
||||
(let* ((bounds (save-excursion
|
||||
(goto-char (region-beginning))
|
||||
(bounds-of-thing-at-point 'line)))
|
||||
(used-part (string-trim
|
||||
(buffer-substring-no-properties
|
||||
(car bounds)
|
||||
(min (cdr bounds) (region-end)))))
|
||||
(relevant-part (string-trim
|
||||
(buffer-substring-no-properties
|
||||
(max (car bounds) (region-beginning))
|
||||
(min (cdr bounds) (region-end))))))
|
||||
(setq region
|
||||
;; replace just first match
|
||||
(replace-regexp-in-string
|
||||
(concat "\\(" (regexp-quote used-part) "\\)\\(?:.*\n?\\)*\\'")
|
||||
relevant-part
|
||||
region t t 1))
|
||||
(ein:shared-output-eval-string (ein:get-kernel) region)))
|
||||
(ein:shared-output-eval-string (ein:get-kernel) (buffer-string)))
|
||||
(if (use-region-p)
|
||||
(progn
|
||||
(goto-char (region-end))
|
||||
(deactivate-mark))
|
||||
(goto-char (point-max))))
|
||||
|
||||
(defun ein:python-send-statement (&optional reset)
|
||||
"Based on `elpy-shell-send-statement' by Schaefer et al."
|
||||
(interactive "P")
|
||||
(ein:python-send--prepare reset)
|
||||
(python-nav-beginning-of-statement)
|
||||
(unless (looking-at "[[:space:]]*$")
|
||||
(let ((beg (save-excursion (beginning-of-line) (point)))
|
||||
(end (progn (ein:python-send--nav-end-of-statement) (point))))
|
||||
(unless (eq beg end)
|
||||
(ein:shared-output-eval-string (ein:get-kernel)
|
||||
(buffer-substring beg end))))))
|
||||
|
||||
(defun ein:python-send--nav-end-of-statement ()
|
||||
"Based on `elpy-shell--nav-end-of-statement' by Schaefer et al."
|
||||
(let ((continue t)
|
||||
p)
|
||||
(while (and (not (eq p (point))) continue)
|
||||
;; is there another block at same indentation level?
|
||||
(setq p (point))
|
||||
(ein:python-send--nav-forward-block)
|
||||
(if (eq p (point))
|
||||
(progn
|
||||
;; nope, go to the end of the block and done
|
||||
(python-nav-end-of-block)
|
||||
(setq continue nil))
|
||||
(unless (eq 0 (string-match-p "\\s-*el\\(?:se:\\|if[^\w]\\)"
|
||||
(thing-at-point 'line)))
|
||||
(forward-line -1)
|
||||
(while (and (or (eq (string-match-p "\\s-*$" (thing-at-point 'line)) 0)
|
||||
(python-info-current-line-comment-p))
|
||||
(not (eq (point) (point-min))))
|
||||
(forward-line -1))
|
||||
(setq continue nil)))))
|
||||
(end-of-line))
|
||||
|
||||
(defun ein:python-send--nav-forward-block ()
|
||||
"Based on `elpy-shell--nav-forward-block' by Schaefer et al.
|
||||
|
||||
Move to the next line indented like point. This will skip over lines and
|
||||
statements with different indentation levels."
|
||||
(interactive "^")
|
||||
(let ((indent (current-column))
|
||||
(start (point))
|
||||
(cur nil))
|
||||
(when (/= (% indent python-indent-offset)
|
||||
0)
|
||||
(setq indent (* (1+ (/ indent python-indent-offset))
|
||||
python-indent-offset)))
|
||||
(python-nav-forward-statement)
|
||||
(while (and (< indent (current-indentation))
|
||||
(not (eobp)))
|
||||
(when (equal (point) cur)
|
||||
(error "Statement does not finish"))
|
||||
(setq cur (point))
|
||||
(python-nav-forward-statement))
|
||||
(when (< (current-indentation)
|
||||
indent)
|
||||
(goto-char start))))
|
||||
|
||||
(defun ein:python-send--init ()
|
||||
(unless (boundp 'ein:python-send-map)
|
||||
(require 'python)
|
||||
(setq ein:python-send-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "e") 'ein:python-send-statement)
|
||||
(define-key map (kbd "r") 'ein:python-send-region-or-buffer)
|
||||
map))
|
||||
(define-key python-mode-map (kbd "C-c C-/") ein:python-send-map)))
|
||||
|
||||
(provide 'ein-python-send)
|
||||
|
||||
;;; ein-python-send.el ends here
|
||||
56
lisp/ein/ein-pytools.el
Normal file
56
lisp/ein/ein-pytools.el
Normal file
@@ -0,0 +1,56 @@
|
||||
;;; ein-pytools.el --- Python tools build on top of kernel -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-pytools.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-pytools.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-pytools.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein-kernel)
|
||||
|
||||
(defun ein:pytools-jump-to-source-command (&optional other-window)
|
||||
"Jump to the source code of the object at point.
|
||||
When the prefix argument ``C-u`` is given, open the source code
|
||||
in the other window. You can explicitly specify the object by
|
||||
selecting it."
|
||||
(interactive "P")
|
||||
(cl-letf (((symbol-function 'xref--prompt-p) #'ignore))
|
||||
(if other-window
|
||||
(call-interactively #'xref-find-definitions-other-window)
|
||||
(call-interactively #'xref-find-definitions))))
|
||||
|
||||
(defun ein:pytools-jump-back-command (&optional _other-window)
|
||||
"Go back to the point where `ein:pytools-jump-to-source-command'
|
||||
is executed last time. When the prefix argument ``C-u`` is
|
||||
given, open the last point in the other window."
|
||||
(interactive "P")
|
||||
(call-interactively (if (fboundp 'xref-go-back)
|
||||
#'xref-go-back
|
||||
(symbol-function 'xref-pop-marker-stack))))
|
||||
|
||||
(define-obsolete-function-alias
|
||||
'ein:pytools-eval-string-internal
|
||||
'ein:shared-output-eval-string "0.1.2")
|
||||
|
||||
(provide 'ein-pytools)
|
||||
|
||||
;;; ein-pytools.el ends here
|
||||
223
lisp/ein/ein-query.el
Normal file
223
lisp/ein/ein-query.el
Normal file
@@ -0,0 +1,223 @@
|
||||
;;; ein-query.el --- jQuery like interface on top of curl -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-query.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-query.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-query.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'request)
|
||||
(require 'url)
|
||||
(require 'ein-core)
|
||||
(require 'ein-log)
|
||||
|
||||
(defcustom ein:query-timeout 10000
|
||||
"Default query timeout for HTTP access in millisecond."
|
||||
:type '(choice (integer :tag "Timeout [ms]" 1000)
|
||||
(const :tag "No timeout" nil))
|
||||
:group 'ein)
|
||||
|
||||
(defvar ein:query-xsrf-cache (make-hash-table :test 'equal)
|
||||
"Remember the last xsrf token by host.
|
||||
This is a hack in case we catch cookie jar in transition.
|
||||
The proper fix is to sempahore between competing curl processes.")
|
||||
|
||||
(defvar ein:query-authorization-tokens (make-hash-table :test 'equal)
|
||||
"Jupyterhub authorization token by (host . username).")
|
||||
|
||||
(defun ein:query-get-cookies (host path-prefix)
|
||||
"Return (:path :expire :name :value) for HOST, matching PATH-PREFIX."
|
||||
(when-let ((filename (request--curl-cookie-jar)))
|
||||
(with-temp-buffer
|
||||
(insert-file-contents filename)
|
||||
(cl-loop for (domain _flag path _secure _http-only expire name value)
|
||||
in (request--netscape-cookie-parse)
|
||||
when (and (string= domain host)
|
||||
(cl-search path-prefix path))
|
||||
collect `(:path ,path :expire ,expire :name ,name :value ,value)))))
|
||||
|
||||
(defun ein:query-prepare-header (url settings &optional securep)
|
||||
"Ensure that REST calls to the jupyter server have the correct _xsrf argument."
|
||||
(let* ((host (url-host (url-generic-parse-url url)))
|
||||
(paths* (let* ((warning-minimum-level :emergency)
|
||||
(warning-minimum-log-level :emergency)
|
||||
(root-url (car (ein:notebooklist-parse-nbpath url))))
|
||||
(if root-url
|
||||
(let ((root-path (url-filename (url-generic-parse-url root-url))))
|
||||
(unless (zerop (length root-path))
|
||||
(list (file-name-as-directory root-path))))
|
||||
(let* ((url* url)
|
||||
(parsed-url* (url-generic-parse-url url*))
|
||||
paths*)
|
||||
(while (not (zerop (length (url-filename parsed-url*))))
|
||||
(push (file-name-as-directory (url-filename parsed-url*)) paths*)
|
||||
(setq url* (file-name-directory (directory-file-name url*))
|
||||
parsed-url* (url-generic-parse-url url*)))
|
||||
paths*))))
|
||||
(paths (progn (cl-pushnew "/" paths* :test #'equal) (reverse paths*)))
|
||||
(cookies (cl-some (lambda (path)
|
||||
(request-cookie-alist host path securep))
|
||||
paths))
|
||||
(xsrf (or (cdr (assoc-string "_xsrf" cookies))
|
||||
(gethash host ein:query-xsrf-cache)))
|
||||
(key (ein:query-divine-authorization-tokens-key url))
|
||||
(token (aand key
|
||||
(gethash key ein:query-authorization-tokens)
|
||||
(cons "Authorization" (format "token %s" it)))))
|
||||
(setq settings (plist-put settings :headers
|
||||
(append (plist-get settings :headers)
|
||||
(list (cons "User-Agent" "Mozilla/5.0")))))
|
||||
(when token
|
||||
(setq settings (plist-put settings :headers
|
||||
(append (plist-get settings :headers)
|
||||
(list token)))))
|
||||
(when xsrf
|
||||
(setq settings (plist-put settings :headers
|
||||
(append (plist-get settings :headers)
|
||||
(list (cons "X-XSRFTOKEN" xsrf)))))
|
||||
(setf (gethash host ein:query-xsrf-cache) xsrf))
|
||||
(setq settings (plist-put settings :encoding 'binary))
|
||||
settings))
|
||||
|
||||
(defun ein:query-divine-authorization-tokens-key (url)
|
||||
"Infer semblance of jupyterhub root.
|
||||
From https://hub.data8x.berkeley.edu/hub/user/806b3e7/notebooks/Untitled.ipynb,
|
||||
get (\"hub.data8x.berkeley.edu\" . \"806b3e7\")"
|
||||
(-when-let* ((parsed-url (url-generic-parse-url url))
|
||||
(url-host (url-host parsed-url))
|
||||
(slash-path (car (url-path-and-query parsed-url)))
|
||||
(components (split-string slash-path "/" t)))
|
||||
(awhen (member "user" components)
|
||||
(cons url-host (cl-second it)))))
|
||||
|
||||
(cl-defun ein:query-singleton-ajax (url &rest settings
|
||||
&key (timeout ein:query-timeout)
|
||||
&allow-other-keys)
|
||||
(if (executable-find request-curl)
|
||||
(let ((request-backend 'curl))
|
||||
(when timeout
|
||||
(setq settings (plist-put settings :timeout (/ timeout 1000.0))))
|
||||
(unless (plist-member settings :sync)
|
||||
(setq settings (plist-put settings :sync ein:force-sync)))
|
||||
(apply #'request (url-encode-url url) (ein:query-prepare-header url settings)))
|
||||
(ein:display-warning
|
||||
(format "The %s program was not found, aborting" request-curl)
|
||||
:error)))
|
||||
|
||||
(defun ein:query-kernelspecs (url-or-port callback &optional iteration)
|
||||
"Send for kernelspecs of URL-OR-PORT with CALLBACK arity 0 (just a semaphore)"
|
||||
(setq iteration (or iteration 0))
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url url-or-port "api/kernelspecs")
|
||||
:type "GET"
|
||||
:parser #'ein:json-read
|
||||
:complete (apply-partially #'ein:query-kernelspecs--complete url-or-port)
|
||||
:success (apply-partially #'ein:query-kernelspecs--success url-or-port callback)
|
||||
:error (apply-partially #'ein:query-kernelspecs--error url-or-port callback iteration)))
|
||||
|
||||
(defun ein:normalize-kernelspec-language (name)
|
||||
"Normalize the kernelspec language string"
|
||||
(if (stringp name)
|
||||
(replace-regexp-in-string "[ ]" "-" name)
|
||||
name))
|
||||
|
||||
(cl-defun ein:query-kernelspecs--success (url-or-port callback
|
||||
&key data _symbol-status _response
|
||||
&allow-other-keys)
|
||||
(let ((ks (list :default (plist-get data :default)))
|
||||
(specs (ein:plist-iter (plist-get data :kernelspecs))))
|
||||
(setf (gethash url-or-port *ein:kernelspecs*)
|
||||
(ein:flatten (dolist (spec specs ks)
|
||||
(let ((name (car spec))
|
||||
(info (cdr spec)))
|
||||
(push (list name (make-ein:$kernelspec :name (plist-get info :name)
|
||||
:display-name (plist-get (plist-get info :spec)
|
||||
:display_name)
|
||||
:resources (plist-get info :resources)
|
||||
:language (ein:normalize-kernelspec-language
|
||||
(plist-get (plist-get info :spec)
|
||||
:language))
|
||||
:spec (plist-get info :spec)))
|
||||
ks))))))
|
||||
(when callback (funcall callback)))
|
||||
|
||||
(cl-defun ein:query-kernelspecs--error
|
||||
(url-or-port callback iteration
|
||||
&key data response error-thrown &allow-other-keys
|
||||
&aux
|
||||
(response-status (request-response-status-code response))
|
||||
(hub-p (request-response-header response "x-jupyterhub-version")))
|
||||
(if (< iteration 3)
|
||||
(if (and hub-p (eq response-status 405))
|
||||
(ein:query-kernelspecs--success url-or-port callback :data data)
|
||||
(ein:log 'verbose "Retry kernelspecs #%s in response to %s"
|
||||
iteration response-status)
|
||||
(ein:query-kernelspecs url-or-port callback (1+ iteration)))
|
||||
(ein:log 'error
|
||||
"ein:query-kernelspecs--error %s: ERROR %s DATA %s"
|
||||
url-or-port (car error-thrown) (cdr error-thrown))
|
||||
(when callback (funcall callback))))
|
||||
|
||||
(cl-defun ein:query-kernelspecs--complete (_url-or-port &key data response &allow-other-keys
|
||||
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
|
||||
(ein:log 'debug "ein:query-kernelspecs--complete %s" resp-string))
|
||||
|
||||
(defun ein:query-notebook-api-version (url-or-port callback)
|
||||
"Get notebook version of URL-OR-PORT with CALLBACK arity 0 (a semaphore)."
|
||||
(ein:query-singleton-ajax
|
||||
(ein:url url-or-port "api/spec.yaml")
|
||||
;; the melpa yaml package was taking too long, unfortunately
|
||||
:parser (lambda ()
|
||||
(if (re-search-forward "api\\s-+version: \\(\\S-+\\)"
|
||||
nil t)
|
||||
;; emacs-25.3 doesn't have the right string-trim
|
||||
(string-remove-prefix
|
||||
"\"" (string-remove-suffix
|
||||
"\"" (match-string-no-properties 1)))
|
||||
""))
|
||||
:complete (apply-partially #'ein:query-notebook-api-version--complete
|
||||
url-or-port callback)))
|
||||
|
||||
(cl-defun ein:query-notebook-api-version--complete
|
||||
(url-or-port callback
|
||||
&key data response
|
||||
&allow-other-keys &aux
|
||||
(resp-string (format "STATUS: %s DATA: %s"
|
||||
(request-response-status-code response) data))
|
||||
(hub-p (request-response-header response "x-jupyterhub-version")))
|
||||
(ein:log 'debug "ein:query-notebook-api-version--complete %s" resp-string)
|
||||
(if (not (zerop (string-to-number data)))
|
||||
(setf (gethash url-or-port *ein:notebook-api-version*) data)
|
||||
(if hub-p
|
||||
(let ((key (ein:query-divine-authorization-tokens-key url-or-port)))
|
||||
(remhash key ein:query-authorization-tokens)
|
||||
(ein:display-warning
|
||||
(format "Server for user %s requires start, aborting"
|
||||
(or (cdr key) "unknown"))
|
||||
:error)
|
||||
(setq callback nil))
|
||||
(ein:log 'warn "notebook api version currently unknowable")))
|
||||
(when callback (funcall callback)))
|
||||
|
||||
(provide 'ein-query)
|
||||
|
||||
;;; ein-query.el ends here
|
||||
50
lisp/ein/ein-scratchsheet.el
Normal file
50
lisp/ein/ein-scratchsheet.el
Normal file
@@ -0,0 +1,50 @@
|
||||
;;; ein-scratchsheet.el --- Worksheet without needs for saving -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012 Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-scratchsheet.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-scratchsheet.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-scratchsheet.el.
|
||||
;; If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ein-worksheet)
|
||||
|
||||
(defclass ein:scratchsheet (ein:worksheet)
|
||||
((data :initarg :data :initform nil))
|
||||
:documentation "Worksheet without needs for saving.")
|
||||
|
||||
(defun ein:scratchsheet-new (nbformat notebook-path kernel events &rest args)
|
||||
(apply #'make-instance 'ein:scratchsheet
|
||||
:nbformat nbformat
|
||||
:notebook-path notebook-path
|
||||
:kernel kernel
|
||||
:events events
|
||||
args))
|
||||
|
||||
(cl-defmethod ein:worksheet--buffer-name ((ws ein:scratchsheet))
|
||||
(format "*ein:scratch %s/%s*"
|
||||
(ein:worksheet-url-or-port ws)
|
||||
(ein:worksheet-notebook-path ws)))
|
||||
|
||||
(provide 'ein-scratchsheet)
|
||||
|
||||
;;; ein-scratchsheet.el ends here
|
||||
230
lisp/ein/ein-shared-output.el
Normal file
230
lisp/ein/ein-shared-output.el
Normal file
@@ -0,0 +1,230 @@
|
||||
;; -*- lexical-binding: t -*-
|
||||
;;; ein-shared-output.el --- Output buffer for ob-ein and ein-python-send
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-shared-output.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-shared-output.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-shared-output.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; When executing code from outside of notebook, some place for output
|
||||
;; is needed. This module buffer containing one special cell for that
|
||||
;; purpose.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'eieio)
|
||||
|
||||
(defclass ein:shared-output-cell (ein:codecell)
|
||||
((cell-type :initarg :cell-type :initform "shared-output")
|
||||
;; (element-names :initform (:prompt :output :footer))
|
||||
(callback :initarg :callback :initform #'ignore :type function)
|
||||
(clear :initarg :clear :initform #'ignore :type function)
|
||||
(results-inserted :initarg :results-inserted :initform nil :type boolean))
|
||||
"A singleton cell to show output from non-notebook buffers.")
|
||||
|
||||
(defclass ein:shared-output ()
|
||||
((cell :initarg :cell :type ein:shared-output-cell)
|
||||
(events :initarg :events :type ein:events)
|
||||
(ewoc :initarg :ewoc :type ewoc)))
|
||||
|
||||
(defvar *ein:shared-output* nil
|
||||
"Hold an instance of `ein:shared-output'.")
|
||||
|
||||
(defconst ein:shared-output-buffer-name "*ein:shared-output*")
|
||||
|
||||
(cl-defmethod ein:cell-insert-prompt ((cell ein:shared-output-cell))
|
||||
"Insert prompt of the CELL in the buffer.
|
||||
Called from ewoc pretty printer via `ein:cell-pp'."
|
||||
;; Newline is inserted in `ein:cell-insert-input'.
|
||||
(ein:insert-read-only
|
||||
(format "In [%s]" (or (ein:oref-safe cell 'input-prompt-number) " "))
|
||||
'font-lock-face (ein:cell-input-prompt-face cell)))
|
||||
|
||||
(cl-defmethod ein:cell-execute ((cell ein:shared-output-cell) kernel code
|
||||
&rest args)
|
||||
(unless (plist-get args :silent)
|
||||
(setq args (plist-put args :silent nil)))
|
||||
(setf (slot-value cell 'kernel) kernel)
|
||||
(apply #'ein:cell-execute-internal cell kernel code args))
|
||||
|
||||
(cl-defmethod ein:cell-append-display-data ((_cell ein:shared-output-cell) _json)
|
||||
"Do not display the plot in the shared output context.")
|
||||
|
||||
(cl-defmethod ein:cell--handle-output ((cell ein:shared-output-cell)
|
||||
msg-type _content _metadata)
|
||||
(ein:log 'debug
|
||||
"ein:cell--handle-output (cell ein:shared-output-cell): %s" msg-type)
|
||||
(cl-call-next-method)
|
||||
(awhen (ein:oref-safe cell 'callback)
|
||||
(when (funcall it cell)
|
||||
(setf (slot-value cell 'results-inserted) t))))
|
||||
|
||||
(cl-defmethod ein:cell--handle-execute-reply ((cell ein:shared-output-cell)
|
||||
content _metadata)
|
||||
(ein:log 'debug
|
||||
"ein:cell--handle-execute-reply (cell ein:shared-output-cell): %s"
|
||||
content)
|
||||
(cl-call-next-method)
|
||||
(awhen (ein:oref-safe cell 'callback)
|
||||
(when (funcall it cell)
|
||||
(setf (slot-value cell 'results-inserted) t)))
|
||||
(unless (slot-value cell 'results-inserted)
|
||||
(awhen (ein:oref-safe cell 'clear)
|
||||
(funcall it)))
|
||||
;; clear the way for waiting block in `ob-ein--execute-async'
|
||||
;; but only after 2 seconds to allow for handle-output stragglers
|
||||
;; TODO avoid this hack
|
||||
(run-at-time 2 nil (lambda ()
|
||||
(ein:log 'debug "Clearing callback shared output cell")
|
||||
(setf (slot-value cell 'callback) #'ignore)
|
||||
(setf (slot-value cell 'clear) #'ignore)
|
||||
(setf (slot-value cell 'results-inserted) nil))))
|
||||
|
||||
(defun ein:shared-output-create-buffer ()
|
||||
"Get or create the shared output buffer."
|
||||
(get-buffer-create ein:shared-output-buffer-name))
|
||||
|
||||
(defun ein:shared-output-buffer ()
|
||||
"Get the buffer associated with `*ein:shared-output*'."
|
||||
(ewoc-buffer (slot-value *ein:shared-output* 'ewoc)))
|
||||
|
||||
(defun ein:shared-output-buffer-p (&optional buffer)
|
||||
"Return non-`nil' when BUFFER (or current buffer) is shared-output buffer."
|
||||
(eq (or buffer (current-buffer)) (ein:shared-output-buffer)))
|
||||
|
||||
(defun ein:shared-output-healthy-p ()
|
||||
(and (ein:shared-output-p *ein:shared-output*)
|
||||
(buffer-live-p (ein:shared-output-buffer))))
|
||||
|
||||
(defun ein:shared-output-get-or-create ()
|
||||
(if (ein:shared-output-healthy-p)
|
||||
*ein:shared-output*
|
||||
(with-current-buffer (ein:shared-output-create-buffer)
|
||||
;; FIXME: This is a duplication of `ein:worksheet-render'.
|
||||
(let* ((inhibit-read-only t)
|
||||
;; Apply read-only text property to newlines by
|
||||
;; setting nonsep flag to `ein:ewoc-create'
|
||||
(ewoc (let ((buffer-undo-list t))
|
||||
(ein:ewoc-create 'ein:worksheet-pp
|
||||
(ein:propertize-read-only "\n")
|
||||
nil t)))
|
||||
(events (ein:events-new))
|
||||
(cell (ein:shared-output-cell :ewoc ewoc
|
||||
:events events)))
|
||||
(erase-buffer)
|
||||
(ein:shared-output-bind-events events)
|
||||
(setq *ein:shared-output*
|
||||
(ein:shared-output :ewoc ewoc :cell cell
|
||||
:events events))
|
||||
(ein:cell-enter-last cell))
|
||||
(setq buffer-read-only t)
|
||||
(ein:shared-output-mode)
|
||||
*ein:shared-output*)))
|
||||
|
||||
(defun ein:shared-output-bind-events (events)
|
||||
"Add dummy event handlers."
|
||||
(ein:events-on events 'set_dirty.Worksheet #'ignore)
|
||||
(ein:events-on events 'maybe_reset_undo.Worksheet #'ignore))
|
||||
|
||||
(defun ein:shared-output-get-cell ()
|
||||
"Get the singleton shared output cell.
|
||||
Create a cell if the buffer has none."
|
||||
(slot-value (ein:shared-output-get-or-create) 'cell))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:shared-output-pop-to-buffer ()
|
||||
"Open shared output buffer."
|
||||
(interactive)
|
||||
(ein:shared-output-get-or-create)
|
||||
(pop-to-buffer (ein:shared-output-create-buffer)))
|
||||
|
||||
(cl-defmethod ein:shared-output-show-code-cell ((cell ein:codecell))
|
||||
"Show code CELL in shared-output buffer."
|
||||
(let ((new (ein:cell-convert cell "shared-output")))
|
||||
;; Make sure `*ein:shared-output*' is initialized:
|
||||
(ein:shared-output-get-or-create)
|
||||
(with-current-buffer (ein:shared-output-create-buffer)
|
||||
(let ((inhibit-read-only t)
|
||||
(ein:cell-max-num-outputs nil))
|
||||
(setf (slot-value new 'ewoc) (slot-value *ein:shared-output* 'ewoc))
|
||||
(setf (slot-value new 'events) (slot-value *ein:shared-output* 'events))
|
||||
(erase-buffer) ; because there are only one cell anyway
|
||||
(setf (slot-value *ein:shared-output* 'cell) new)
|
||||
(ein:cell-enter-last new)
|
||||
(pop-to-buffer (current-buffer))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:shared-output-show-code-cell-at-point ()
|
||||
"Show code cell at point in shared-output buffer.
|
||||
It is useful when the output of the cell at point is truncated.
|
||||
See also `ein:cell-max-num-outputs'."
|
||||
(interactive)
|
||||
(let ((cell (ein:get-cell-at-point)))
|
||||
(if (ein:codecell-p cell)
|
||||
(ein:shared-output-show-code-cell cell)
|
||||
(error "No code cell at point."))))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:shared-output-eval-string (kernel code &rest args)
|
||||
"Entry to `ein:cell-execute-internal' from the shared output cell."
|
||||
(unless kernel (setq kernel (ein:get-kernel-or-error)))
|
||||
(let ((cell (ein:shared-output-get-cell)))
|
||||
(ein:kernel-when-ready
|
||||
kernel
|
||||
(lambda (ready-kernel)
|
||||
(apply #'ein:cell-execute cell ready-kernel (ein:trim-indent code) args)))))
|
||||
|
||||
;;; Generic getter
|
||||
|
||||
(defun ein:get-url-or-port--shared-output ()
|
||||
(ein:aand (ein:get-kernel--shared-output) (ein:kernel-url-or-port it)))
|
||||
|
||||
;; (defun ein:get-notebook--shared-output ())
|
||||
|
||||
(defun ein:get-kernel--shared-output ()
|
||||
(let ((cell (ein:get-cell-at-point--shared-output)))
|
||||
(when (and (eieio-object-p cell) (slot-boundp cell :kernel))
|
||||
(slot-value cell 'kernel))))
|
||||
|
||||
(defun ein:get-cell-at-point--shared-output ()
|
||||
(when (and (ein:shared-output-p *ein:shared-output*)
|
||||
(ein:shared-output-buffer-p))
|
||||
(slot-value *ein:shared-output* 'cell)))
|
||||
|
||||
(defun ein:get-traceback-data--shared-output ()
|
||||
(ein:aand (ein:get-cell-at-point--shared-output) (ein:cell-get-tb-data it)))
|
||||
|
||||
(defvar ein:shared-output-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map "\C-c\C-x" 'ein:tb-show)
|
||||
(define-key map "\M-." 'ein:pytools-jump-to-source-command)
|
||||
(define-key map (kbd "C-c C-.") 'ein:pytools-jump-to-source-command)
|
||||
map)
|
||||
"The map for ein:shared-output-mode-map.")
|
||||
|
||||
(define-derived-mode ein:shared-output-mode special-mode "ein:so"
|
||||
"Shared output mode."
|
||||
(font-lock-mode))
|
||||
|
||||
(add-hook 'ein:shared-output-mode-hook 'ein:truncate-lines-on)
|
||||
|
||||
|
||||
(provide 'ein-shared-output)
|
||||
|
||||
;;; ein-shared-output.el ends here
|
||||
193
lisp/ein/ein-traceback.el
Normal file
193
lisp/ein/ein-traceback.el
Normal file
@@ -0,0 +1,193 @@
|
||||
;;; ein-traceback.el --- Traceback module -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-traceback.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-traceback.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-traceback.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'eieio)
|
||||
(require 'ewoc)
|
||||
(require 'ansi-color)
|
||||
|
||||
(require 'ein-core)
|
||||
(require 'ein-shared-output)
|
||||
|
||||
(declare-function ein:get-notebook "ein-notebook")
|
||||
(declare-function ein:notebook-buffer "ein-notebook")
|
||||
|
||||
(defclass ein:traceback ()
|
||||
((tb-data :initarg :tb-data :type list)
|
||||
(notebook :initarg :source-notebook ;; :type ein:$notebook
|
||||
:accessor ein:traceback-notebook)
|
||||
(buffer-name :initarg :buffer-name :type string)
|
||||
(buffer :initarg :buffer :type buffer)
|
||||
(ewoc :initarg :ewoc :type ewoc)))
|
||||
|
||||
(ein:deflocal ein:%traceback% nil
|
||||
"Buffer local variable to store an instance of `ein:traceback'.")
|
||||
|
||||
(defvar ein:tb-buffer-name-template "*ein:tb %s/%s*")
|
||||
|
||||
(defun ein:tb-new (buffer-name notebook)
|
||||
(make-instance 'ein:traceback
|
||||
:buffer-name buffer-name
|
||||
:source-notebook notebook))
|
||||
|
||||
(cl-defmethod ein:tb-get-buffer ((traceback ein:traceback))
|
||||
(unless (and (slot-boundp traceback :buffer)
|
||||
(buffer-live-p (slot-value traceback 'buffer)))
|
||||
(let ((buf (get-buffer-create (slot-value traceback 'buffer-name))))
|
||||
(setf (slot-value traceback 'buffer) buf)))
|
||||
(slot-value traceback 'buffer))
|
||||
|
||||
(defun ein:tb-pp (ewoc-data)
|
||||
(insert (ansi-color-apply ewoc-data)))
|
||||
|
||||
(cl-defmethod ein:tb-render ((traceback ein:traceback) tb-data)
|
||||
(with-current-buffer (ein:tb-get-buffer traceback)
|
||||
(setq ein:%traceback% traceback)
|
||||
(setq buffer-read-only t)
|
||||
(let ((inhibit-read-only t)
|
||||
(ewoc (ein:ewoc-create #'ein:tb-pp)))
|
||||
(erase-buffer)
|
||||
(setf (slot-value traceback 'ewoc) ewoc)
|
||||
(setf (slot-value traceback 'tb-data) tb-data)
|
||||
(mapc (lambda (data) (ewoc-enter-last ewoc data)) tb-data))
|
||||
(ein:traceback-mode)))
|
||||
|
||||
(cl-defmethod ein:tb-popup ((traceback ein:traceback) tb-data)
|
||||
(ein:tb-render traceback tb-data)
|
||||
(pop-to-buffer (ein:tb-get-buffer traceback)))
|
||||
|
||||
;;;###autoload
|
||||
(defun ein:tb-show ()
|
||||
"Show full traceback in traceback viewer."
|
||||
(interactive)
|
||||
(unless
|
||||
(ein:and-let* ((tb-data (ein:get-traceback-data))
|
||||
(url-or-port (or (ein:get-url-or-port)
|
||||
(ein:get-url-or-port--shared-output)))
|
||||
(kernel (or (ein:get-kernel)
|
||||
(ein:get-kernel--shared-output)))
|
||||
(kr-id (ein:kernel-id kernel))
|
||||
(tb-name (format ein:tb-buffer-name-template
|
||||
url-or-port kr-id)))
|
||||
(ein:tb-popup (ein:tb-new tb-name (ein:get-notebook)) tb-data)
|
||||
t)
|
||||
(error "No traceback is available.")))
|
||||
|
||||
(cl-defmethod ein:tb-range-of-node-at-point ((traceback ein:traceback))
|
||||
(let* ((ewoc (slot-value traceback 'ewoc))
|
||||
(ewoc-node (ewoc-locate ewoc))
|
||||
(beg (ewoc-location ewoc-node))
|
||||
(end (ein:aand (ewoc-next ewoc ewoc-node) (ewoc-location it))))
|
||||
(list beg end)))
|
||||
|
||||
(cl-defmethod ein:tb-file-path-at-point ((traceback ein:traceback))
|
||||
(cl-destructuring-bind (beg end)
|
||||
(ein:tb-range-of-node-at-point traceback)
|
||||
(let* ((file-tail
|
||||
(next-single-property-change beg 'font-lock-face nil end))
|
||||
(file (when file-tail
|
||||
(buffer-substring-no-properties beg file-tail))))
|
||||
(if (string-match "\\.pyc$" file)
|
||||
(concat (file-name-sans-extension file) ".py")
|
||||
file))))
|
||||
|
||||
(cl-defmethod ein:tb-file-lineno-at-point ((traceback ein:traceback))
|
||||
(cl-destructuring-bind (beg end)
|
||||
(ein:tb-range-of-node-at-point traceback)
|
||||
(when (save-excursion
|
||||
(goto-char beg)
|
||||
(search-forward-regexp "^[-]+> \\([0-9]+\\)" end t))
|
||||
(string-to-number (match-string 1)))))
|
||||
|
||||
(cl-defmethod ein:tb-jump-to-source-at-point ((traceback ein:traceback)
|
||||
&optional select)
|
||||
(let ((file (ein:tb-file-path-at-point traceback))
|
||||
(lineno (ein:tb-file-lineno-at-point traceback)))
|
||||
(if (string-match "<ipython-input-\\([0-9]+\\)-.*" file)
|
||||
(let* ((cellnum (string-to-number (match-string 1 file)))
|
||||
(nb (slot-value traceback 'notebook))
|
||||
(ws (cl-first (ein:$notebook-worksheets nb)))
|
||||
(cells (ein:worksheet-get-cells ws))
|
||||
(it (cl-find cellnum cells :key #'(lambda (x)
|
||||
(if (same-class-p x 'ein:codecell)
|
||||
(slot-value x 'input-prompt-number))))))
|
||||
(if it
|
||||
(progn
|
||||
(pop-to-buffer (ein:notebook-buffer nb))
|
||||
(ein:cell-goto-line it lineno))))
|
||||
(let ((url-or-port (ein:$notebook-url-or-port (ein:traceback-notebook traceback))))
|
||||
(cond
|
||||
((numberp url-or-port) (ein:tb-jtsap--local file lineno select))
|
||||
((string-match "localhost" url-or-port) (ein:tb-jtsap--local file lineno select))
|
||||
((string-match "127.0.0.1" url-or-port) (ein:tb-jtsap--local file lineno select))
|
||||
(t (ein:tb-jtsap--remote url-or-port file lineno select)))))))
|
||||
|
||||
(defun ein:tb-jtsap--local (file lineno select)
|
||||
(cl-assert (file-exists-p file) nil "File %s does not exist." file)
|
||||
(let ((buf (find-file-noselect file))
|
||||
(scroll (lambda ()
|
||||
(goto-char (point-min))
|
||||
(forward-line (1- lineno)))))
|
||||
(if select
|
||||
(progn (pop-to-buffer buf)
|
||||
(funcall scroll))
|
||||
(with-selected-window (display-buffer buf)
|
||||
(funcall scroll)))))
|
||||
|
||||
(defun ein:tb-jtsap--remote (uri path lineno select)
|
||||
(let* ((uri (url-generic-parse-url uri))
|
||||
(host-path (concat "/" (url-host uri)
|
||||
":" path)))
|
||||
(ein:tb-jtsap--local host-path lineno select)))
|
||||
|
||||
(defun ein:tb-jump-to-source-at-point-command (&optional select)
|
||||
(interactive "P")
|
||||
(ein:tb-jump-to-source-at-point ein:%traceback% select))
|
||||
|
||||
(defun ein:tb-prev-item ()
|
||||
(interactive)
|
||||
(ewoc-goto-prev (slot-value ein:%traceback% 'ewoc) 1))
|
||||
|
||||
(defun ein:tb-next-item ()
|
||||
(interactive)
|
||||
(ewoc-goto-next (slot-value ein:%traceback% 'ewoc) 1))
|
||||
|
||||
(defvar ein:traceback-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "RET") 'ein:tb-jump-to-source-at-point-command)
|
||||
(define-key map "p" 'ein:tb-prev-item)
|
||||
(define-key map "n" 'ein:tb-next-item)
|
||||
map)
|
||||
"Keymap for ein:traceback-mode.")
|
||||
|
||||
(define-derived-mode ein:traceback-mode special-mode "ein:tb"
|
||||
(font-lock-mode))
|
||||
|
||||
(add-hook 'ein:traceback-mode-hook 'ein:truncate-lines-on)
|
||||
|
||||
(provide 'ein-traceback)
|
||||
|
||||
;;; ein-traceback.el ends here
|
||||
721
lisp/ein/ein-utils.el
Normal file
721
lisp/ein/ein-utils.el
Normal file
@@ -0,0 +1,721 @@
|
||||
;;; ein-utils.el --- Utility module -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-utils.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-utils.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-utils.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cc-mode)
|
||||
(require 'json)
|
||||
(require 'dash)
|
||||
(require 'url)
|
||||
(require 'deferred)
|
||||
|
||||
(make-obsolete-variable 'ein:enable-gc-adjust nil "0.17.0")
|
||||
|
||||
;;; Macros and core functions/variables
|
||||
|
||||
(defmacro ein:with-undo-disabled (&rest body)
|
||||
"Temporarily disable undo recording while executing `body`
|
||||
while maintaining the undo list for the current buffer."
|
||||
`(let ((buffer-undo-list t))
|
||||
,@body))
|
||||
|
||||
(defmacro ein:aand (test &rest rest)
|
||||
"Anaphoric AND. Adapted from `e2wm:aand'."
|
||||
(declare (debug (form &rest form)))
|
||||
`(let ((it ,test))
|
||||
(if it ,(if rest (macroexpand-all `(ein:aand ,@rest)) 'it))))
|
||||
|
||||
(defmacro ein:and-let* (bindings &rest form)
|
||||
"Gauche's `and-let*'."
|
||||
(declare (debug ((&rest &or symbolp (form) (gate symbolp &optional form))
|
||||
body))
|
||||
;; See: (info "(elisp) Specification List")
|
||||
(indent 1))
|
||||
(if (null bindings)
|
||||
`(progn ,@form)
|
||||
(let* ((head (car bindings))
|
||||
(tail (cdr bindings))
|
||||
(rest (macroexpand-all `(ein:and-let* ,tail ,@form))))
|
||||
(cond
|
||||
((symbolp head) `(if ,head ,rest))
|
||||
((= (length head) 1) `(if ,(car head) ,rest))
|
||||
(t `(let (,head) (if ,(car head) ,rest)))))))
|
||||
|
||||
(defvar ein:local-variables '()
|
||||
"Modified by `ein:deflocal'")
|
||||
|
||||
(defmacro ein:deflocal (name &optional initvalue docstring)
|
||||
"Define permanent buffer local variable named NAME.
|
||||
INITVALUE and DOCSTRING are passed to `defvar'."
|
||||
(declare (indent defun)
|
||||
(doc-string 3))
|
||||
`(progn
|
||||
(defvar ,name ,initvalue ,docstring)
|
||||
(make-variable-buffer-local ',name)
|
||||
(put ',name 'permanent-local t)
|
||||
(setq ein:local-variables (append ein:local-variables '(,name)))))
|
||||
|
||||
(defmacro ein:with-read-only-buffer (buffer &rest body)
|
||||
(declare (indent 1))
|
||||
`(with-current-buffer ,buffer
|
||||
(setq buffer-read-only t)
|
||||
(save-excursion
|
||||
(let ((inhibit-read-only t))
|
||||
,@body))))
|
||||
|
||||
(defmacro ein:with-live-buffer (buffer &rest body)
|
||||
"Execute BODY in BUFFER if BUFFER is alive."
|
||||
(declare (indent 1) (debug t))
|
||||
`(when (buffer-live-p ,buffer)
|
||||
(with-current-buffer ,buffer
|
||||
,@body)))
|
||||
|
||||
(defmacro ein:with-possibly-killed-buffer (buffer &rest body)
|
||||
"Execute BODY in BUFFER if BUFFER is live.
|
||||
Execute BODY if BUFFER is not live anyway."
|
||||
(declare (indent 1) (debug t))
|
||||
`(if (buffer-live-p ,buffer)
|
||||
(with-current-buffer ,buffer
|
||||
,@body)
|
||||
,@body))
|
||||
|
||||
(defvar ein:dotty-syntax-table
|
||||
(let ((table (make-syntax-table c-mode-syntax-table)))
|
||||
(modify-syntax-entry ?. "w" table)
|
||||
(modify-syntax-entry ?_ "w" table)
|
||||
(modify-syntax-entry ?% "w" table)
|
||||
table)
|
||||
"Adapted from `python-dotty-syntax-table'.")
|
||||
|
||||
(defun ein:beginning-of-object (&optional code-syntax-table)
|
||||
"Move to the beginning of the dotty.word.at.point.
|
||||
User may specify a custom syntax table. If one is not supplied
|
||||
`ein:dotty-syntax-table' will be assumed."
|
||||
(with-syntax-table (or code-syntax-table ein:dotty-syntax-table)
|
||||
(while (re-search-backward "\\(\\sw\\|\\s_\\|\\s\\.\\|\\s\\\\|[%@|]\\)\\="
|
||||
(when (> (point) 2000) (- (point) 2000))
|
||||
t))
|
||||
(re-search-forward "\\=#[-+.<|]" nil t)
|
||||
(when (and (looking-at "@"))
|
||||
(forward-char))))
|
||||
|
||||
(defun ein:end-of-object (&optional code-syntax-table)
|
||||
"Move to the end of the dotty.word.at.point. User may specify a
|
||||
custom syntax table. If one is not supplied
|
||||
`ein:dotty-syntax-table' will be assumed."
|
||||
(with-syntax-table (or code-syntax-table ein:dotty-syntax-table)
|
||||
(re-search-forward "\\=\\(\\sw\\|\\s_\\|\\s\\.\\|#:\\|[%|]\\)*")))
|
||||
|
||||
(defun ein:object-start-pos ()
|
||||
"Return the starting position of the symbol under point.
|
||||
The result is unspecified if there isn't a symbol under the point."
|
||||
(save-excursion (ein:beginning-of-object) (point)))
|
||||
|
||||
(defun ein:object-end-pos ()
|
||||
(save-excursion (ein:end-of-object) (point)))
|
||||
|
||||
|
||||
(defun ein:object-prefix-at-point ()
|
||||
"Like `ein:object-at-point', but only return substring up to point.
|
||||
For example, given pd.Series, if the cursor is at the S then
|
||||
pd.S will be returned."
|
||||
(ein:and-let* ((obj (ein:object-at-point))
|
||||
(delta (- (point) (ein:object-start-pos))))
|
||||
(substring obj 0 delta)))
|
||||
|
||||
(defun ein:object-at-point ()
|
||||
"Return dotty.words.at.point.
|
||||
When region is active, text in region is returned after trimmed
|
||||
white spaces, newlines and dots. When object is not found at the
|
||||
point, return the object just before previous opening
|
||||
parenthesis.
|
||||
|
||||
For auto popup tooltip (or something like eldoc), probably it is
|
||||
better to return function (any word before left parenthesis). I
|
||||
should write another function or add option to this function when
|
||||
the auto popup tooltip is implemented."
|
||||
(if (region-active-p)
|
||||
(ein:trim (buffer-substring (region-beginning) (region-end))
|
||||
"\\s-\\|\n\\|\\.")
|
||||
(save-excursion
|
||||
(with-syntax-table ein:dotty-syntax-table
|
||||
(aif (thing-at-point 'symbol)
|
||||
it
|
||||
(unless (looking-at "(")
|
||||
(search-backward "(" (line-beginning-position) t))
|
||||
(thing-at-point 'symbol))))))
|
||||
|
||||
(defun ein:function-at-point ()
|
||||
"Similar to `ein:object-at-point', but instead will looking for the function
|
||||
at point, i.e. any word before then \"(\", if it is present."
|
||||
(save-excursion
|
||||
(unless (looking-at "(")
|
||||
(search-backward "(" (line-beginning-position) t))
|
||||
(ein:object-at-point)))
|
||||
|
||||
(defun ein:object-at-point-or-error ()
|
||||
(or (ein:object-at-point) (error "No object found at the point")))
|
||||
|
||||
(defun ein:flatten (tree)
|
||||
"Traverses the tree in order, collecting non-null leaves into a list."
|
||||
(let (list)
|
||||
(cl-labels ((traverse (subtree)
|
||||
(when subtree
|
||||
(if (consp subtree)
|
||||
(progn
|
||||
(traverse (car subtree))
|
||||
(traverse (cdr subtree)))
|
||||
(push subtree list)))))
|
||||
(traverse tree))
|
||||
(nreverse list)))
|
||||
|
||||
(defvar ein:url-localhost "127.0.0.1")
|
||||
|
||||
(defsubst ein:glom-paths (&rest paths)
|
||||
(cl-loop with result = ""
|
||||
for p in paths
|
||||
if (not (zerop (length p)))
|
||||
do (setq result (concat result (ein:trim-left (directory-file-name p) "/") "/"))
|
||||
end
|
||||
finally return (directory-file-name result)))
|
||||
|
||||
(defun ein:url (url-or-port &rest paths)
|
||||
(when url-or-port
|
||||
(when (or (integerp url-or-port)
|
||||
(and (stringp url-or-port) (string-match "^[0-9]+$" url-or-port)))
|
||||
(setq url-or-port (format "http://localhost:%s" url-or-port)))
|
||||
(setq url-or-port (string-trim url-or-port))
|
||||
(cl-flet ((localhost-p (host) (or (string= host "localhost")
|
||||
(string= host ein:url-localhost)
|
||||
(string= host ""))))
|
||||
(let ((parsed-url (url-generic-parse-url url-or-port)))
|
||||
(unless (url-host parsed-url)
|
||||
(setq url-or-port (format "%s://%s" (if (localhost-p url-or-port)
|
||||
"http" "https")
|
||||
url-or-port))
|
||||
(setq parsed-url (url-generic-parse-url url-or-port)))
|
||||
(when (localhost-p (url-host parsed-url))
|
||||
(setf (url-host parsed-url) ein:url-localhost))
|
||||
(directory-file-name (concat (file-name-as-directory (url-recreate-url parsed-url))
|
||||
(apply #'ein:glom-paths paths)))))))
|
||||
|
||||
(defun ein:url-no-cache (url)
|
||||
"Imitate `cache=false' of `jQuery.ajax'.
|
||||
See: http://api.jquery.com/jQuery.ajax/"
|
||||
(concat url (format-time-string "?_=%s")))
|
||||
|
||||
(defun ein:html-get-data-in-body-tag (key)
|
||||
"Very ad-hoc parser to get data in body tag."
|
||||
(ignore-errors
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(search-forward "<body")
|
||||
(search-forward-regexp (format "%s=\\([^[:space:]\n]+\\)" key))
|
||||
(match-string 1))))
|
||||
|
||||
(defmacro ein:with-json-setting (&rest body)
|
||||
`(let ((json-object-type 'plist)
|
||||
(json-array-type 'list))
|
||||
,@body))
|
||||
|
||||
(defsubst ein:json-read ()
|
||||
"Read json from `url-retrieve'-ed buffer.
|
||||
|
||||
* `json-object-type' is `plist'. This is mainly for readability.
|
||||
* `json-array-type' is `list'. Notebook data is edited locally thus
|
||||
data type must be edit-friendly. `vector' type is not."
|
||||
(goto-char (point-max))
|
||||
(backward-sexp)
|
||||
(if (fboundp 'json-parse-buffer)
|
||||
(json-parse-buffer :object-type 'plist :array-type 'array :null-object json-null :false-object json-false)
|
||||
(ein:with-json-setting (json-read))))
|
||||
|
||||
(defsubst ein:json-read-from-string (string)
|
||||
(if (fboundp 'json-parse-string)
|
||||
(json-parse-string string :object-type 'plist :array-type 'array :null-object json-null :false-object json-false)
|
||||
(ein:with-json-setting (json-read-from-string string))))
|
||||
|
||||
(defsubst ein:json-insert (obj)
|
||||
(if (fboundp 'json-insert)
|
||||
(json-insert obj :null-object json-null :false-object json-false)
|
||||
(insert (json-encode obj))))
|
||||
|
||||
(defsubst ein:json-encode (obj)
|
||||
(if (fboundp 'json-serialize)
|
||||
(json-serialize obj :null-object json-null :false-object json-false)
|
||||
(json-encode obj)))
|
||||
|
||||
(defsubst ein:json-any-to-bool (obj)
|
||||
(if (and obj (not (eq obj json-false))) t json-false))
|
||||
|
||||
(defun ein:ewoc-create (pretty-printer &optional header footer nosep)
|
||||
"Do nothing wrapper of `ewoc-create' to provide better error message."
|
||||
(condition-case nil
|
||||
(ewoc-create pretty-printer header footer nosep)
|
||||
((debug wrong-number-of-arguments)
|
||||
(ein:display-warning "Incompatible EWOC version.
|
||||
The version of ewoc.el you are using is too old for EIN.
|
||||
Please install the newer version.
|
||||
See also: https://github.com/tkf/emacs-ipython-notebook/issues/49")
|
||||
(error "Incompatible EWOC version."))))
|
||||
|
||||
(defun ein:propertize-read-only (string &rest properties)
|
||||
(apply #'propertize string 'read-only t 'front-sticky t properties))
|
||||
|
||||
(defvar ein:truncate-long-cell-output) ; defined in ein-cell - but cannot require it because of circularity
|
||||
(defun ein:insert-read-only (string &rest properties)
|
||||
(let ((buffer-undo-list t)
|
||||
(start (point)))
|
||||
(insert (apply #'ein:propertize-read-only
|
||||
(ein:maybe-truncate-string-lines string ein:truncate-long-cell-output)
|
||||
properties))
|
||||
(comint-carriage-motion start (point))))
|
||||
|
||||
(defun ein:maybe-truncate-string-lines (string nlines)
|
||||
"Truncate multi-line `string' to NLINES."
|
||||
(if nlines
|
||||
(let ((lines (split-string string "[\n]")))
|
||||
(if (> (length lines) nlines)
|
||||
(ein:join-str "\n" (append (butlast lines (- (length lines) nlines))
|
||||
(list "...")))
|
||||
string))
|
||||
string))
|
||||
|
||||
(defun ein:trim (string &optional regexp)
|
||||
(ein:trim-left (ein:trim-right string regexp) regexp))
|
||||
|
||||
(defun ein:trim-left (string &optional regexp)
|
||||
(unless regexp (setq regexp "\\s-\\|\n"))
|
||||
(ein:trim-regexp string (format "^\\(%s\\)+" regexp)))
|
||||
|
||||
(defun ein:trim-right (string &optional regexp)
|
||||
(unless regexp (setq regexp "\\s-\\|\n"))
|
||||
(ein:trim-regexp string (format "\\(%s\\)+$" regexp)))
|
||||
|
||||
(defun ein:trim-regexp (string regexp)
|
||||
(if (string-match regexp string)
|
||||
(replace-match "" t t string)
|
||||
string))
|
||||
|
||||
(defun ein:trim-indent (string)
|
||||
"Strip uniform amount of indentation from lines in STRING."
|
||||
(let* ((lines (split-string string "\n"))
|
||||
(indent
|
||||
(let ((lens
|
||||
(cl-loop for line in lines
|
||||
for stripped = (ein:trim-left line)
|
||||
unless (equal stripped "")
|
||||
collect (- (length line) (length stripped)))))
|
||||
(if lens (apply #'min lens) 0)))
|
||||
(trimmed
|
||||
(cl-loop for line in lines
|
||||
if (> (length line) indent)
|
||||
collect (ein:trim-right (substring line indent))
|
||||
else
|
||||
collect line)))
|
||||
(ein:join-str "\n" trimmed)))
|
||||
|
||||
(defun ein:join-str (sep strings)
|
||||
(mapconcat 'identity strings sep))
|
||||
|
||||
(defun ein:join-path (paths)
|
||||
(mapconcat 'file-name-as-directory paths ""))
|
||||
|
||||
(defun ein:string-fill-paragraph (string &optional justify)
|
||||
(with-temp-buffer
|
||||
(erase-buffer)
|
||||
(insert string)
|
||||
(goto-char (point-min))
|
||||
(fill-paragraph justify)
|
||||
(buffer-string)))
|
||||
|
||||
(cl-defmacro ein:case-equal (str &rest clauses)
|
||||
"Similar to `case' but comparison is done by `equal'.
|
||||
Adapted from twittering-mode.el's `case-string'."
|
||||
(declare (indent 1))
|
||||
`(cond
|
||||
,@(mapcar
|
||||
(lambda (clause)
|
||||
(let ((keylist (car clause))
|
||||
(body (cdr clause)))
|
||||
`(,(if (listp keylist)
|
||||
`(or ,@(mapcar (lambda (key) `(equal ,str ,key))
|
||||
keylist))
|
||||
't)
|
||||
,@body)))
|
||||
clauses)))
|
||||
|
||||
;;; Text manipulation on buffer
|
||||
|
||||
(defun ein:find-leftmost-column (beg end)
|
||||
"Return the leftmost column in region BEG to END."
|
||||
(save-excursion
|
||||
(let (mincol)
|
||||
(goto-char beg)
|
||||
(while (< (point) end)
|
||||
(back-to-indentation)
|
||||
(unless (= (point) (line-end-position))
|
||||
(setq mincol (if mincol
|
||||
(min mincol (current-column))
|
||||
(current-column))))
|
||||
(unless (= (forward-line 1) 0)
|
||||
(cl-return-from ein:find-leftmost-column mincol)))
|
||||
mincol)))
|
||||
|
||||
;;; Misc
|
||||
|
||||
(defun ein:completing-read (&rest args)
|
||||
(cond (noninteractive (if (consp (cl-second args))
|
||||
(car (cl-second args))
|
||||
(cl-second args)))
|
||||
(t (apply completing-read-function args))))
|
||||
|
||||
(defun ein:plist-iter (plist)
|
||||
"Return list of (key . value) in PLIST."
|
||||
;; FIXME: this is not needed. See: `ein:plist-exclude'.
|
||||
(cl-loop for p in plist
|
||||
for i from 0
|
||||
for key-p = (= (% i 2) 0)
|
||||
with key = nil
|
||||
if key-p do (setq key p)
|
||||
else collect `(,key . ,p)))
|
||||
|
||||
(defun ein:plist-exclude (plist keys)
|
||||
"Exclude entries specified by KEYS in PLIST.
|
||||
|
||||
Example:
|
||||
|
||||
(ein:plist-exclude \\='(:a 1 :b 2 :c 3 :d 4) \\='(:b :c))"
|
||||
(cl-loop for (k v) on plist by 'cddr
|
||||
unless (memq k keys)
|
||||
nconc (list k v)))
|
||||
|
||||
(defun ein:clip-list (list first last)
|
||||
"Return elements in region of the LIST specified by FIRST and LAST element.
|
||||
|
||||
Example:
|
||||
|
||||
(ein:clip-list \\='(1 2 3 4 5 6) 2 4) ;=> (2 3 4)"
|
||||
(cl-loop for elem in list
|
||||
with clipped
|
||||
with in-region-p = nil
|
||||
when (eq elem first)
|
||||
do (setq in-region-p t)
|
||||
when in-region-p
|
||||
do (push elem clipped)
|
||||
when (eq elem last)
|
||||
return (reverse clipped)))
|
||||
|
||||
(cl-defun ein:list-insert-after (list pivot new &key (test #'eq))
|
||||
"Insert NEW after PIVOT in LIST destructively.
|
||||
Note: do not rely on that `ein:list-insert-after' change LIST in place.
|
||||
Elements are compared using the function TEST (default: `eq')."
|
||||
(cl-loop for rest on list
|
||||
when (funcall test (car rest) pivot)
|
||||
return (progn (push new (cdr rest)) list)
|
||||
finally do (error "PIVOT %S is not in LIST %S" pivot list)))
|
||||
|
||||
(cl-defun ein:list-insert-before (list pivot new &key (test #'eq))
|
||||
"Insert NEW before PIVOT in LIST destructively.
|
||||
Note: do not rely on that `ein:list-insert-before' change LIST in place.
|
||||
Elements are compared using the function TEST (default: `eq')."
|
||||
(if (and list (funcall test (car list) pivot))
|
||||
(cons new list)
|
||||
(cl-loop for rest on list
|
||||
when (funcall test (cadr rest) pivot)
|
||||
return (progn (push new (cdr rest)) list)
|
||||
finally do (error "PIVOT %S is not in LIST %S" pivot list))))
|
||||
|
||||
(cl-defun ein:list-move-left (list elem &key (test #'eq))
|
||||
"Move ELEM in LIST left. TEST is used to compare elements"
|
||||
(cl-macrolet ((== (a b) `(funcall test ,a ,b)))
|
||||
(cond
|
||||
((== (car list) elem)
|
||||
(append (cdr list) (list (car list))))
|
||||
(t
|
||||
(cl-loop for rest on list
|
||||
when (== (cadr rest) elem)
|
||||
return (let ((prev (car rest)))
|
||||
(setf (car rest) elem)
|
||||
(setf (cadr rest) prev)
|
||||
list)
|
||||
finally do (error "ELEM %S is not in LIST %S" elem list))))))
|
||||
|
||||
(cl-defun ein:list-move-right (list elem &key (test #'eq))
|
||||
"Move ELEM in LIST right. TEST is used to compare elements"
|
||||
(cl-loop with first = t
|
||||
for rest on list
|
||||
when (funcall test (car rest) elem)
|
||||
return (if (cdr rest)
|
||||
(let ((next (cadr rest)))
|
||||
(setf (car rest) next)
|
||||
(setf (cadr rest) elem)
|
||||
list)
|
||||
(if first
|
||||
list
|
||||
(setcdr rest-1 nil)
|
||||
(cons elem list)))
|
||||
finally do (error "ELEM %S is not in LIST %S" elem list)
|
||||
for rest-1 = rest
|
||||
do (setq first nil)))
|
||||
|
||||
(defun ein:get-value (obj)
|
||||
"Get value from obj if it is a variable or function."
|
||||
(cond
|
||||
((not (symbolp obj)) obj)
|
||||
((boundp obj) (symbol-value obj))
|
||||
((fboundp obj) (funcall obj))))
|
||||
|
||||
(defun ein:choose-setting (symbol value &optional single-p)
|
||||
"Choose setting in stored in SYMBOL based on VALUE.
|
||||
The value of SYMBOL can be string, alist or function.
|
||||
SINGLE-P is a function which takes one argument. It must
|
||||
return t when the value of SYMBOL can be used as a setting.
|
||||
SINGLE-P is `stringp' by default."
|
||||
(let ((setting (symbol-value symbol)))
|
||||
(cond
|
||||
((funcall (or single-p 'stringp) setting) setting)
|
||||
((functionp setting) (funcall setting value))
|
||||
((listp setting)
|
||||
(ein:get-value (or (assoc-default value setting)
|
||||
(assoc-default 'default setting))))
|
||||
(t (error "Unsupported type of `%s': %s" symbol (type-of setting))))))
|
||||
|
||||
(defmacro ein:setf-default (place val)
|
||||
"Set VAL to PLACE using `setf' if the value of PLACE is `nil'."
|
||||
`(unless ,place
|
||||
(setf ,place ,val)))
|
||||
|
||||
(defun ein:funcall-packed (func-arg &rest args)
|
||||
"Call \"packed\" function.
|
||||
FUNC-ARG is a `cons' of the form: (FUNC ARG).
|
||||
FUNC is called as (apply FUNC ARG ARGS)."
|
||||
(apply (car func-arg) (cdr func-arg) args))
|
||||
|
||||
(defun ein:eval-if-bound (symbol)
|
||||
(and (boundp symbol) (symbol-value symbol)))
|
||||
|
||||
(defun ein:remove-by-index (list indices)
|
||||
"Remove elements from LIST if its index is in INDICES.
|
||||
NOTE: This function creates new list."
|
||||
(cl-loop for l in list
|
||||
for i from 0
|
||||
when (not (memq i indices))
|
||||
collect l))
|
||||
|
||||
(defun ein:ask-choice-char (prompt choices)
|
||||
"Show PROMPT and read one of acceptable key specified as CHOICES."
|
||||
(let ((char-list (cl-loop for i from 0 below (length choices)
|
||||
collect (elt choices i)))
|
||||
(answer 'recenter))
|
||||
(while
|
||||
(let ((key
|
||||
(let ((cursor-in-echo-area t))
|
||||
(read-key (propertize (if (eq answer 'recenter)
|
||||
prompt
|
||||
(concat "Please choose answer from"
|
||||
(format " %s. " choices)
|
||||
prompt))
|
||||
'face 'minibuffer-prompt)))))
|
||||
(setq answer (lookup-key query-replace-map (vector key) t))
|
||||
(cond
|
||||
((memq key char-list) (setq answer key) nil)
|
||||
((eq answer 'recenter) (recenter) t)
|
||||
((memq answer '(exit-prefix quit)) (signal 'quit nil) t)
|
||||
(t t)))
|
||||
(ding)
|
||||
(discard-input))
|
||||
answer))
|
||||
|
||||
(defun ein:truncate-lines-on ()
|
||||
"Set `truncate-lines' on (set it to `t')."
|
||||
(setq truncate-lines t))
|
||||
|
||||
(defun ein:wait-until (predicate &optional predargs timeout-seconds)
|
||||
"Wait until PREDICATE function returns non-`nil'.
|
||||
PREDARGS is argument list for the PREDICATE function.
|
||||
Make TIMEOUT-SECONDS larger \(default 5) to wait longer before timeout."
|
||||
(ein:log 'debug "WAIT-UNTIL start")
|
||||
(unless timeout-seconds (setq timeout-seconds 5))
|
||||
(unless (cl-loop repeat (/ timeout-seconds 0.05)
|
||||
when (apply predicate predargs)
|
||||
return t
|
||||
;; borrowed from `deferred:sync!':
|
||||
do (sit-for 0.05)
|
||||
do (sleep-for 0.05))
|
||||
(warn "Timeout"))
|
||||
(ein:log 'debug "WAIT-UNTIL end"))
|
||||
|
||||
(defun ein:format-time-string (format time)
|
||||
"Apply format to time.
|
||||
If `format' is a string, call `format-time-string',
|
||||
otherwise it should be a function, which is called on `time'."
|
||||
(cl-etypecase format
|
||||
(string (format-time-string format time))
|
||||
(function (funcall format time))))
|
||||
|
||||
;;; Emacs utilities
|
||||
(defmacro ein:message-whir (mesg callback &rest body)
|
||||
"Display MESG with a modest animation until ASYNC-CALL completes."
|
||||
`(let* (done-p
|
||||
(done-callback (lambda (&rest _args) (setf done-p t)))
|
||||
(errback (lambda (&rest _args) (setf done-p 'error))))
|
||||
(ignore errback) ; make errback ignorable
|
||||
;; again, how can done-callback remove itself after running?
|
||||
(add-function :before ,callback done-callback)
|
||||
(unless noninteractive
|
||||
(ein:message-whir-subr ,mesg (lambda () done-p)))
|
||||
,@body))
|
||||
|
||||
(defun ein:message-whir-subr (mesg doneback)
|
||||
"Display MESG with a modest animation until done-p returns t.
|
||||
|
||||
DONEBACK returns t or \\='error when calling process is done, and nil if not done."
|
||||
(let* ((mesg mesg)
|
||||
(doneback doneback)
|
||||
(count -1))
|
||||
(message "%s%s" mesg (make-string (1+ (% (cl-incf count) 3)) ?.))
|
||||
;; https://github.com/kiwanami/emacs-deferred/issues/28
|
||||
;; "complicated timings of macro expansion lexical-let, deferred:lambda"
|
||||
;; using deferred:loop instead
|
||||
(deferred:$
|
||||
(deferred:loop (cl-loop for i from 1 below 60 by 1 collect i)
|
||||
(lambda ()
|
||||
(deferred:$
|
||||
(deferred:next
|
||||
(lambda ()
|
||||
(aif (funcall doneback) it
|
||||
(message "%s%s" mesg (make-string (1+ (% (cl-incf count) 3)) ?.))
|
||||
(sleep-for 0 365)))))))
|
||||
(deferred:nextc it
|
||||
(lambda (status)
|
||||
(message "%s... %s" mesg
|
||||
(if (or (null status) (eq status 'error)) "failed" "done")))))))
|
||||
|
||||
|
||||
(defun ein:display-warning (message &optional level)
|
||||
"Simple wrapper around `display-warning'.
|
||||
LEVEL must be one of :emergency, :error or :warning (default).
|
||||
This must be used only for notifying user.
|
||||
Use `ein:log' for debugging and logging."
|
||||
;; FIXME: Probably set BUFFER-NAME per notebook?
|
||||
;; FIXME: Call `ein:log' here (but do not display in minibuffer).
|
||||
(display-warning 'ein message level))
|
||||
|
||||
(defvar ein:display-warning-once--db
|
||||
(make-hash-table :test 'equal))
|
||||
|
||||
(defun ein:display-warning-once (message &optional level)
|
||||
"Call `ein:display-warning' once for same MESSAGE and LEVEL."
|
||||
(let ((key (list message level)))
|
||||
(unless (gethash key ein:display-warning-once--db)
|
||||
(ein:display-warning message level)
|
||||
(puthash key t ein:display-warning-once--db))))
|
||||
|
||||
(defvar help-xref-following) ; defined in help-mode
|
||||
(defun ein:get-docstring (function)
|
||||
"Return docstring of FUNCTION."
|
||||
(with-temp-buffer
|
||||
(erase-buffer)
|
||||
(let ((standard-output (current-buffer))
|
||||
(help-xref-following t)
|
||||
(major-mode 'help-mode)) ; avoid error in Emacs 24
|
||||
(describe-function-1 function))
|
||||
(buffer-string)))
|
||||
|
||||
(defun ein:generate-menu (list-name-callback)
|
||||
(mapcar (lambda (name-callback)
|
||||
(cl-destructuring-bind (name callback &rest args) name-callback
|
||||
`[,name
|
||||
,(let ((km (intern-soft (concat (symbol-name callback) "-km"))))
|
||||
(if (commandp km) km callback))
|
||||
:help ,(ein:get-docstring callback) ,@args]))
|
||||
list-name-callback))
|
||||
|
||||
;;; Git utilities
|
||||
|
||||
(defun ein:call-process (command &optional args)
|
||||
"Call COMMAND with ARGS and return its stdout as string or
|
||||
`nil' if COMMAND fails. It also checks if COMMAND executable
|
||||
exists or not."
|
||||
(with-temp-buffer
|
||||
(erase-buffer)
|
||||
(and (executable-find command)
|
||||
(= (apply #'call-process command nil t nil args) 0)
|
||||
(buffer-string))))
|
||||
|
||||
(defun ein:git-root-p (&optional dir)
|
||||
"Return `t' when DIR is root of git repository."
|
||||
(file-directory-p (expand-file-name ".git" (or dir default-directory))))
|
||||
|
||||
(defun ein:git-dirty-p ()
|
||||
"Return `t' if the current directory is in git repository and it is dirty."
|
||||
(not (equal (ein:call-process
|
||||
"git" '("--no-pager" "status" "--porcelain"))
|
||||
"")))
|
||||
|
||||
(defun ein:git-revision ()
|
||||
"Return abbreviated git revision if the current directory is in
|
||||
git repository."
|
||||
(ein:call-process "git" '("--no-pager" "log" "-n1" "--format=format:%h")))
|
||||
|
||||
(defun ein:git-revision-dirty ()
|
||||
"Return `ein:git-revision' + \"-dirty\" suffix if the current
|
||||
directory is in a dirty git repository."
|
||||
(ein:aand (ein:git-revision)
|
||||
(concat it (if (ein:git-dirty-p) "-dirty" ""))))
|
||||
|
||||
|
||||
;;; utils.js compatible
|
||||
|
||||
(defun ein:utils-uuid ()
|
||||
"Return string with random (version 4) UUID.
|
||||
Adapted from org-mode's `org-id-uuid'."
|
||||
(let ((rnd (md5 (format "%s%s%s%s%s%s%s"
|
||||
(random t)
|
||||
(current-time)
|
||||
(user-uid)
|
||||
(emacs-pid)
|
||||
(user-full-name)
|
||||
user-mail-address
|
||||
(recent-keys)))))
|
||||
(format "%s-%s-4%s-%s%s-%s"
|
||||
(substring rnd 0 8)
|
||||
(substring rnd 8 12)
|
||||
(substring rnd 13 16)
|
||||
(format "%x"
|
||||
(logior
|
||||
#b10000000
|
||||
(logand
|
||||
#b10111111
|
||||
(string-to-number
|
||||
(substring rnd 16 18) 16))))
|
||||
(substring rnd 18 20)
|
||||
(substring rnd 20 32))))
|
||||
|
||||
|
||||
(provide 'ein-utils)
|
||||
|
||||
;;; ein-utils.el ends here
|
||||
119
lisp/ein/ein-websocket.el
Normal file
119
lisp/ein/ein-websocket.el
Normal file
@@ -0,0 +1,119 @@
|
||||
;;; ein-websocket.el --- Wrapper of websocket.el -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012- Takafumi Arakaki
|
||||
|
||||
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein-websocket.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein-websocket.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein-websocket.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'websocket)
|
||||
(require 'ein-core)
|
||||
(require 'ein-classes)
|
||||
(require 'url-cookie)
|
||||
(require 'request)
|
||||
|
||||
(defun ein:websocket-store-cookie (c host-port url-filename securep)
|
||||
(url-cookie-store (car c) (cdr c) nil host-port url-filename securep))
|
||||
|
||||
(defun ein:maybe-get-jhconn-user (url)
|
||||
(let ((paths (cl-rest (split-string (url-filename (url-generic-parse-url url)) "/"))))
|
||||
(when (string= (cl-first paths) "user")
|
||||
(list (format "/%s/%s/" (cl-first paths) (cl-second paths))))))
|
||||
|
||||
(defun ein:websocket--prepare-cookies (url)
|
||||
"Websocket gets its cookies using the url-cookie API, so we need
|
||||
to transcribe any cookies stored in `request-cookie-alist' during
|
||||
earlier calls to `request' (request.el)."
|
||||
(let* ((parsed-url (url-generic-parse-url url))
|
||||
(host-port (format "%s:%s" (url-host parsed-url) (url-port parsed-url)))
|
||||
(base-url (file-name-as-directory (url-filename parsed-url)))
|
||||
(securep (string-match "^wss://" url))
|
||||
(read-cookies-func (lambda (path)
|
||||
(request-cookie-alist
|
||||
(url-host parsed-url) path securep)))
|
||||
(cookies (cl-loop
|
||||
repeat 4
|
||||
for cand = (cl-mapcan read-cookies-func
|
||||
`("/"
|
||||
"/hub/"
|
||||
,base-url
|
||||
,@(ein:maybe-get-jhconn-user url)))
|
||||
until (cl-some (lambda (x) (string= "_xsrf" (car x))) cand)
|
||||
do (ein:log 'info
|
||||
"ein:websocket--prepare-cookies: no _xsrf among %s, retrying."
|
||||
cand)
|
||||
do (sleep-for 0 300)
|
||||
finally return cand)))
|
||||
(dolist (c cookies)
|
||||
(ein:websocket-store-cookie
|
||||
c host-port (car (url-path-and-query parsed-url)) securep))))
|
||||
|
||||
(defun ein:websocket (url kernel on-message on-close on-open)
|
||||
(ein:websocket--prepare-cookies (ein:$kernel-ws-url kernel))
|
||||
(let* ((ws (websocket-open url
|
||||
:on-open on-open
|
||||
:on-message on-message
|
||||
:on-close on-close
|
||||
:on-error (lambda (ws action err)
|
||||
(ein:log 'info "WS action [%s] %s (%s)"
|
||||
err action (websocket-url ws)))))
|
||||
(websocket (make-ein:$websocket :ws ws :kernel kernel :closed-by-client nil)))
|
||||
(setf (websocket-client-data ws) websocket)
|
||||
websocket))
|
||||
|
||||
(defun ein:websocket-open-p (websocket)
|
||||
(eql (websocket-ready-state (ein:$websocket-ws websocket)) 'open))
|
||||
|
||||
|
||||
(defun ein:websocket-send (websocket text)
|
||||
;; (ein:log 'info "WS: Sent message %s" text)
|
||||
(condition-case-unless-debug err
|
||||
(websocket-send-text (ein:$websocket-ws websocket) text)
|
||||
(error (message "Error %s on sending websocket message %s." err text))))
|
||||
|
||||
|
||||
(defun ein:websocket-close (websocket)
|
||||
(setf (ein:$websocket-closed-by-client websocket) t)
|
||||
(websocket-close (ein:$websocket-ws websocket)))
|
||||
|
||||
|
||||
(defun ein:websocket-send-shell-channel (kernel msg)
|
||||
(cond ((= (ein:$kernel-api-version kernel) 2)
|
||||
(ein:websocket-send
|
||||
(ein:$kernel-shell-channel kernel)
|
||||
(ein:json-encode msg)))
|
||||
((>= (ein:$kernel-api-version kernel) 3)
|
||||
(ein:websocket-send
|
||||
(ein:$kernel-websocket kernel)
|
||||
(ein:json-encode (plist-put msg :channel "shell"))))))
|
||||
|
||||
(defun ein:websocket-send-stdin-channel (kernel msg)
|
||||
(cond ((= (ein:$kernel-api-version kernel) 2)
|
||||
(ein:log 'warn "Stdin messages only supported with IPython 3."))
|
||||
((>= (ein:$kernel-api-version kernel) 3)
|
||||
(ein:websocket-send
|
||||
(ein:$kernel-websocket kernel)
|
||||
(ein:json-encode (plist-put msg :channel "stdin"))))))
|
||||
|
||||
(provide 'ein-websocket)
|
||||
|
||||
;;; ein-websocket.el ends here
|
||||
1161
lisp/ein/ein-worksheet.el
Normal file
1161
lisp/ein/ein-worksheet.el
Normal file
File diff suppressed because it is too large
Load Diff
52
lisp/ein/ein.el
Normal file
52
lisp/ein/ein.el
Normal file
@@ -0,0 +1,52 @@
|
||||
;;; ein.el --- jupyter notebook client -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2012-2019 The Authors of the Emacs IPython Notebook (EIN)
|
||||
|
||||
;; Authors: dickmao <github id: dickmao>
|
||||
;; John Miller <millejoh at millejoh.com>
|
||||
;; Takafumi Arakaki <aka.tkf at gmail.com>
|
||||
;; Version: 0.17.1pre
|
||||
;; Package-Requires: ((emacs "26.1") (websocket "1.12") (anaphora "1.0.4") (request "0.3.3") (deferred "0.5") (polymode "0.2.2") (dash "2.13.0") (with-editor "0pre"))
|
||||
;; URL: https://github.com/dickmao/emacs-ipython-notebook
|
||||
;; Keywords: jupyter, literate programming, reproducible research
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ein.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ein.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ein.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Emacs IPython Notebook (EIN), despite its name, is a jupyter client for all
|
||||
;; languages. It does not work under non-WSL Windows environments.
|
||||
;;
|
||||
;; As of 2023, EIN has been sunset for a number of years having been
|
||||
;; unable to keep up with jupyter's web-first ecosystem. Even during
|
||||
;; its heyday EIN never fully reconciled emac's monolithic buffer
|
||||
;; architecture to the notebook's by-cell discretization, leaving
|
||||
;; gaping functional holes like crippled undo.
|
||||
;;
|
||||
;; Certainly in 2012 when jupyter was much smaller, an emacs client
|
||||
;; made perfect sense. With many years of hindsight, it's now clear
|
||||
;; the json-driven, git-averse notebook format is anathema to emacs's
|
||||
;; plain text ethos.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(when (boundp 'mouse-buffer-menu-mode-groups)
|
||||
(add-to-list 'mouse-buffer-menu-mode-groups
|
||||
'("^ein:" . "ein")))
|
||||
|
||||
(provide 'ein)
|
||||
|
||||
;;; ein.el ends here
|
||||
458
lisp/ein/ob-ein.el
Normal file
458
lisp/ein/ob-ein.el
Normal file
@@ -0,0 +1,458 @@
|
||||
;; -*- lexical-binding: t -*-
|
||||
;;; ob-ein.el --- org-babel functions for template evaluation
|
||||
|
||||
;; Copyright (C) John M. Miller
|
||||
|
||||
;; Author: John M. Miller <millejoh at mac.com>
|
||||
;;
|
||||
|
||||
;;; License:
|
||||
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; ob-ein.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; ob-ein.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with ob-ein.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Support executing org-babel source blocks using EIN worksheets.
|
||||
|
||||
;;; Credits:
|
||||
|
||||
;; Uses code from https://github.com/gregsexton/ob-ipython (MIT License)
|
||||
|
||||
;;; Code:
|
||||
(require 'ob)
|
||||
(require 'ein-utils)
|
||||
(require 'ein-cell)
|
||||
(require 'anaphora)
|
||||
|
||||
(autoload 'org-element-property "org-element")
|
||||
(autoload 'org-element-context "org-element")
|
||||
(autoload 'org-element-type "org-element")
|
||||
(autoload 'org-id-new "org-id")
|
||||
(autoload 'org-redisplay-inline-images "org" nil t)
|
||||
(autoload 'ein:notebooklist-new-notebook-with-name "ein-notebooklist")
|
||||
(autoload 'ein:notebooklist-canonical-url-or-port "ein-notebooklist")
|
||||
(autoload 'ein:notebooklist-login "ein-notebooklist" nil t)
|
||||
(autoload 'ein:notebook-get-opened-notebook "ein-notebook")
|
||||
(autoload 'ein:notebook-url "ein-notebook")
|
||||
(autoload 'ein:notebook-open "ein-notebook")
|
||||
(autoload 'ein:notebook-close "ein-notebook")
|
||||
(autoload 'ein:process-url-or-port "ein-process")
|
||||
(autoload 'ein:process-url-match "ein-process")
|
||||
(autoload 'ein:process-refresh-processes "ein-process")
|
||||
(autoload 'ein:jupyter-my-url-or-port "ein-jupyter")
|
||||
(autoload 'ein:jupyter-server-start "ein-jupyter" nil t)
|
||||
(autoload 'ein:shared-output-get-cell "ein-shared-output")
|
||||
(autoload 'ein:shared-output-eval-string "ein-shared-output")
|
||||
(autoload 'ein:kernel-live-p "ein-kernel")
|
||||
(autoload 'ein:query-singleton-ajax "ein-query")
|
||||
(autoload 'ein:output-area-case-type "ein-output-area")
|
||||
(autoload 'ein:log "ein-log")
|
||||
|
||||
(defvar *ob-ein-sentinel* "[....]"
|
||||
"Placeholder string replaced after async cell execution")
|
||||
|
||||
(defcustom ob-ein-timeout-seconds 600
|
||||
"Maximum seconds to wait for block to finish (for synchronous operations)."
|
||||
:type 'integer
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ob-ein-languages
|
||||
'(("ein" . python)
|
||||
("ein-python" . python)
|
||||
("ein-R" . R)
|
||||
("ein-r" . R)
|
||||
("ein-julia" . julia))
|
||||
"ob-ein has knowledge of these (ein-LANG . LANG-MODE) pairs."
|
||||
:type '(repeat (cons string symbol))
|
||||
:group 'ein)
|
||||
|
||||
(defcustom ob-ein-anonymous-path "ob-%s.ipynb"
|
||||
"Applies when session header doesn't specify ipynb.
|
||||
Prosecute all interactions for a given language in this throwaway
|
||||
notebook (substitute %s with language)."
|
||||
:type '(string)
|
||||
:group 'ein)
|
||||
|
||||
(defun ob-ein-anonymous-p (path)
|
||||
"Return t if PATH looks like ob-ein-anonymous-path. Fragile"
|
||||
(string-match (replace-regexp-in-string "%s" ".+"
|
||||
(replace-regexp-in-string "\\." "\\\\." ob-ein-anonymous-path))
|
||||
path))
|
||||
|
||||
(defcustom ob-ein-inline-image-directory "ein-images"
|
||||
"Store ob-ein images here."
|
||||
:group 'ein
|
||||
:type 'directory)
|
||||
|
||||
(defcustom ob-ein-default-header-args:ein nil
|
||||
"No documentation."
|
||||
:group 'ein
|
||||
:type '(repeat string))
|
||||
|
||||
(defun ob-ein--inline-image-info (value)
|
||||
(let* ((f (md5 value))
|
||||
(d ob-ein-inline-image-directory)
|
||||
(tf (concat d "/ob-ein-" f ".png")))
|
||||
(unless (file-directory-p d)
|
||||
(make-directory d 'parents))
|
||||
tf))
|
||||
|
||||
(defun ob-ein--write-base64-image (img-string file)
|
||||
(with-temp-file file
|
||||
(let ((buffer-read-only nil)
|
||||
(buffer-file-coding-system 'binary)
|
||||
(require-final-newline nil)
|
||||
(file-precious-flag t))
|
||||
(insert img-string)
|
||||
(base64-decode-region (point-min) (point-max)))))
|
||||
|
||||
(defun ob-ein--proxy-images (json explicit-file)
|
||||
(let (result
|
||||
(ein:output-area-case-types '(:image/svg+xml :image/png :image/jpeg :text/plain :application/latex :application/tex :application/javascript)))
|
||||
(ein:output-area-case-type
|
||||
json
|
||||
(cl-case type
|
||||
((:image/svg+xml :image/png :image/jpeg)
|
||||
(let ((file (or explicit-file (ob-ein--inline-image-info value))))
|
||||
(ob-ein--write-base64-image value file)
|
||||
(setq result (format "[[file:%s]]" file))))
|
||||
(otherwise
|
||||
(setq result value))))
|
||||
result))
|
||||
|
||||
(defun ob-ein--process-outputs (result-type cell params)
|
||||
(let* ((session (aand (cdr (assoc :session params))
|
||||
(unless (string= "none" it)
|
||||
(format "%s" it))))
|
||||
(render (let ((stdout-p
|
||||
(lambda (out)
|
||||
(and (equal "stream" (plist-get out :output_type))
|
||||
(equal "stdout" (plist-get out :name))))))
|
||||
(if (eq result-type 'output)
|
||||
(lambda (out)
|
||||
(if (funcall stdout-p out)
|
||||
(plist-get out :text)
|
||||
(when session ;; should aways be true under ob-ein
|
||||
(concat (ob-ein--proxy-images
|
||||
out (cdr (assoc :image params)))
|
||||
"\n"))))
|
||||
(lambda (out)
|
||||
(and (not (funcall stdout-p out))
|
||||
(concat (ob-ein--proxy-images
|
||||
out (cdr (assoc :image params)))
|
||||
"\n"))))))
|
||||
(outputs (cl-loop for out in (ein:oref-safe cell 'outputs)
|
||||
collect (funcall render out))))
|
||||
(when outputs
|
||||
(ansi-color-apply (ein:join-str "" outputs)))))
|
||||
|
||||
(defun ob-ein--get-name-create (src-block-info)
|
||||
"Get the name of a src block or add a uuid as the name."
|
||||
(if-let ((name (cl-fifth src-block-info)))
|
||||
name
|
||||
(save-excursion
|
||||
(let ((el (org-element-context))
|
||||
(id (org-id-new 'none)))
|
||||
(goto-char (org-element-property :begin el))
|
||||
(back-to-indentation)
|
||||
(split-line)
|
||||
(insert (format "#+NAME: %s" id))
|
||||
id))))
|
||||
|
||||
(defun ob-ein--babelize-lang (lang-name lang-mode)
|
||||
"Stand-up LANG-NAME as a babelized language with LANG-MODE syntax table.
|
||||
|
||||
Based on ob-ipython--configure-kernel."
|
||||
(add-to-list 'org-src-lang-modes `(,lang-name . ,lang-mode))
|
||||
(defvaralias (intern (concat "org-babel-default-header-args:" lang-name))
|
||||
'ob-ein-default-header-args:ein)
|
||||
(fset (intern (concat "org-babel-execute:" lang-name))
|
||||
`(lambda (body params)
|
||||
"Should get rid of accommodating org-babel-variable-assignments.
|
||||
We don't test it, and finding a module named ob-LANG-MODE won't work generally,
|
||||
e.g., ob-c++ is not ob-C.el."
|
||||
(require (quote ,(intern (format "ob-%s" lang-mode))) nil t)
|
||||
;; hack because ob-ein loads independently of ein
|
||||
(custom-set-variables '(python-indent-guess-indent-offset-verbose nil))
|
||||
(let ((parser
|
||||
(quote
|
||||
,(intern (format "org-babel-variable-assignments:%s" lang-mode)))))
|
||||
(ob-ein--execute-body
|
||||
(if (fboundp parser)
|
||||
(org-babel-expand-body:generic
|
||||
body params (funcall (symbol-function parser) params))
|
||||
body)
|
||||
params)))))
|
||||
|
||||
(defun ob-ein--execute-body (body params)
|
||||
(let* ((buffer (current-buffer))
|
||||
(result-type (cdr (assq :result-type params)))
|
||||
(result-params (cdr (assq :result-params params)))
|
||||
(session (or (aand (cdr (assoc :session params))
|
||||
(unless (string= "none" it)
|
||||
(format "%s" it)))
|
||||
ein:url-localhost))
|
||||
(lang (nth 0 (org-babel-get-src-block-info)))
|
||||
(kernelspec (or (cdr (assoc :kernelspec params))
|
||||
(aif (cdr (assoc lang org-src-lang-modes))
|
||||
(cons 'language (format "%s" it))
|
||||
(error "ob-ein--execute-body: %s not among %s"
|
||||
lang (mapcar #'car org-src-lang-modes)))))
|
||||
(name (ob-ein--get-name-create (org-babel-get-src-block-info)))
|
||||
(callback (lambda (notebook)
|
||||
(ob-ein--execute-async
|
||||
buffer
|
||||
body
|
||||
(ein:$notebook-kernel notebook)
|
||||
params
|
||||
result-type
|
||||
result-params
|
||||
name))))
|
||||
(save-excursion
|
||||
(cl-assert (not (stringp (org-babel-goto-named-src-block name))))
|
||||
(org-babel-insert-result *ob-ein-sentinel* result-params))
|
||||
(ob-ein--initiate-session session kernelspec callback)
|
||||
(if (ein:eval-if-bound 'org-current-export-file)
|
||||
(save-excursion
|
||||
(cl-loop with interval = 2000
|
||||
with pending = t
|
||||
repeat (/ (* ob-ein-timeout-seconds 1000) interval)
|
||||
do (progn
|
||||
(org-babel-goto-named-result name)
|
||||
(forward-line 1)
|
||||
(setq pending (re-search-forward
|
||||
(regexp-quote *ob-ein-sentinel*)
|
||||
(org-babel-result-end) t)))
|
||||
until (not pending)
|
||||
do (sleep-for 0 interval)
|
||||
finally return
|
||||
(if pending
|
||||
(prog1 ""
|
||||
(ein:log 'error "ob-ein--execute-body: %s timed out" name))
|
||||
(ob-ein--process-outputs result-type
|
||||
(ein:shared-output-get-cell)
|
||||
params))))
|
||||
(org-babel-remove-result)
|
||||
*ob-ein-sentinel*)))
|
||||
|
||||
(defun ob-ein--execute-async-callback (buffer params result-type result-params name)
|
||||
"Return callback of 1-arity (the shared output cell) to update org buffer when
|
||||
`ein:shared-output-eval-string' completes.
|
||||
|
||||
The callback returns t if results containt RESULT-TYPE outputs, nil otherwise."
|
||||
(apply-partially
|
||||
(lambda (buffer* params* result-type* result-params* name* cell)
|
||||
(when-let ((raw (aif (ein:oref-safe cell 'traceback)
|
||||
(ansi-color-apply (ein:join-str "\n" it))
|
||||
(ob-ein--process-outputs result-type* cell params*))))
|
||||
(prog1 t
|
||||
(let ((result
|
||||
(let ((tmp-file (org-babel-temp-file "ein-")))
|
||||
(with-temp-file tmp-file (insert raw))
|
||||
(org-babel-result-cond result-params*
|
||||
raw (org-babel-import-elisp-from-file tmp-file '(16)))))
|
||||
(info (org-babel-get-src-block-info 'light)))
|
||||
(ein:log 'debug "ob-ein--execute-async-callback %s \"%s\" %s"
|
||||
name* result buffer*)
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(with-current-buffer buffer*
|
||||
(unless (stringp (org-babel-goto-named-src-block name*)) ;; stringp=error
|
||||
(when (version-list-< (version-to-list (org-release)) '(9))
|
||||
(when info ;; kill #+RESULTS: (no-name)
|
||||
(setf (nth 4 info) nil)
|
||||
(org-babel-remove-result info))
|
||||
(org-babel-remove-result)) ;; kill #+RESULTS: name
|
||||
(org-babel-insert-result
|
||||
result
|
||||
(cdr (assoc :result-params
|
||||
(cl-third (org-babel-get-src-block-info)))))
|
||||
(org-redisplay-inline-images)))))))))
|
||||
buffer params result-type result-params name))
|
||||
|
||||
(defun ob-ein--execute-async-clear (buffer result-params name)
|
||||
"Return function of 0-arity to clear *ob-ein-sentinel*."
|
||||
(apply-partially
|
||||
(lambda (buffer* result-params* name*)
|
||||
(let ((info (org-babel-get-src-block-info 'light)))
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(with-current-buffer buffer*
|
||||
(unless (stringp (org-babel-goto-named-src-block name*)) ;; stringp=error
|
||||
(when info ;; kill #+RESULTS: (no-name)
|
||||
(setf (nth 4 info) nil)
|
||||
(org-babel-remove-result info))
|
||||
(org-babel-remove-result) ;; kill #+RESULTS: name
|
||||
(org-babel-insert-result "" result-params*)
|
||||
(org-redisplay-inline-images)))))))
|
||||
buffer result-params name))
|
||||
|
||||
(defun ob-ein--execute-async (buffer body kernel params result-type result-params name)
|
||||
"As `ein:shared-output-get-cell' is a singleton, ob-ein can only execute blocks
|
||||
one at a time. Further, we do not order the queued up blocks!"
|
||||
(deferred:$
|
||||
(deferred:next
|
||||
(deferred:lambda ()
|
||||
(let ((cell (ein:shared-output-get-cell)))
|
||||
(if (eq (slot-value cell 'callback) #'ignore)
|
||||
(let ((callback (ob-ein--execute-async-callback
|
||||
buffer params result-type
|
||||
result-params name))
|
||||
(clear (ob-ein--execute-async-clear buffer result-params name)))
|
||||
(setf (slot-value cell 'callback) callback)
|
||||
(setf (slot-value cell 'clear) clear))
|
||||
;; still pending previous callback
|
||||
(deferred:nextc (deferred:wait 1200) self)))))
|
||||
(deferred:nextc it
|
||||
(lambda (_x)
|
||||
(ein:shared-output-eval-string kernel body)))))
|
||||
|
||||
(defun ob-ein--parse-session (session)
|
||||
(let* ((url-or-port (ein:jupyter-my-url-or-port))
|
||||
(tokens (split-string session "/"))
|
||||
(parsed-url (url-generic-parse-url session))
|
||||
(url-host (url-host parsed-url)))
|
||||
(cond ((null url-host)
|
||||
(let* ((candidate (apply #'ein:url (car tokens) (cdr tokens)))
|
||||
(parsed-candidate (url-generic-parse-url candidate))
|
||||
(missing (url-scheme-get-property
|
||||
(url-type parsed-candidate)
|
||||
'default-port)))
|
||||
(if (and url-or-port
|
||||
(= (url-port parsed-candidate) missing))
|
||||
(apply #'ein:url url-or-port (cdr tokens))
|
||||
candidate)))
|
||||
(t (ein:url session)))))
|
||||
|
||||
(defun ob-ein--initiate-session (session kernelspec callback)
|
||||
"Retrieve notebook of SESSION path and KERNELSPEC.
|
||||
Start jupyter instance if necessary.
|
||||
Install CALLBACK (i.e., cell execution) upon notebook retrieval."
|
||||
(let* ((nbpath (ob-ein--parse-session session))
|
||||
(info (org-babel-get-src-block-info))
|
||||
(anonymous-path (format ob-ein-anonymous-path (nth 0 info)))
|
||||
(parsed-url (url-generic-parse-url nbpath))
|
||||
(slash-path (car (url-path-and-query parsed-url)))
|
||||
(_ (awhen (cdr (url-path-and-query parsed-url))
|
||||
(error "Cannot handle :session `%s`" it)))
|
||||
(ipynb-p (file-name-extension (file-name-nondirectory slash-path)))
|
||||
(path (if ipynb-p
|
||||
(file-name-nondirectory slash-path)
|
||||
anonymous-path))
|
||||
(url-or-port (directory-file-name
|
||||
(if ipynb-p
|
||||
(cl-subseq nbpath 0 (- (length path)))
|
||||
nbpath)))
|
||||
(notebook (ein:notebook-get-opened-notebook url-or-port path))
|
||||
(callback-nbopen (lambda (nb _created)
|
||||
(cl-loop repeat 50
|
||||
for live-p = (ein:kernel-live-p (ein:$notebook-kernel nb))
|
||||
until live-p
|
||||
do (sleep-for 0 300)
|
||||
finally
|
||||
do (if (not live-p)
|
||||
(ein:log 'error
|
||||
"Kernel for %s failed to launch"
|
||||
(ein:$notebook-notebook-name nb))
|
||||
(funcall callback nb)))))
|
||||
(errback-nbopen (lambda (url-or-port status-code)
|
||||
(if (eq status-code 404)
|
||||
(ein:notebooklist-new-notebook-with-name
|
||||
url-or-port kernelspec path callback-nbopen t))))
|
||||
(callback-login (lambda (_buffer url-or-port)
|
||||
(ein:notebook-open url-or-port path kernelspec
|
||||
callback-nbopen errback-nbopen t))))
|
||||
(cond ((and notebook
|
||||
(string= path anonymous-path)
|
||||
(stringp kernelspec)
|
||||
(not (equal (ein:$kernelspec-name (ein:$notebook-kernelspec notebook))
|
||||
kernelspec)))
|
||||
(ein:log 'debug "ob-ein--initiate-session: switching %s from %s to %s"
|
||||
path (ein:$kernelspec-name (ein:$notebook-kernelspec notebook))
|
||||
kernelspec)
|
||||
(cl-letf (((symbol-function 'y-or-n-p) #'ignore))
|
||||
(ein:notebook-close notebook))
|
||||
(ein:query-singleton-ajax (ein:notebook-url notebook)
|
||||
:type "DELETE")
|
||||
(cl-loop repeat 8
|
||||
with fullpath = (concat (file-name-as-directory nbpath) path)
|
||||
for extant = (file-exists-p fullpath)
|
||||
until (not extant)
|
||||
do (sleep-for 0 500)
|
||||
finally do (if extant
|
||||
(ein:display-warning
|
||||
(format "cannot delete path=%s nbpath=%s"
|
||||
fullpath nbpath))
|
||||
(ob-ein--initiate-session session kernelspec callback))))
|
||||
(notebook (funcall callback notebook))
|
||||
((string= (url-host parsed-url) ein:url-localhost)
|
||||
(ein:process-refresh-processes)
|
||||
(aif (ein:process-url-match nbpath)
|
||||
(ein:notebooklist-login (ein:process-url-or-port it) callback-login)
|
||||
(ein:jupyter-server-start
|
||||
(executable-find (or (ein:eval-if-bound 'ein:jupyter-server-command)
|
||||
"jupyter"))
|
||||
(read-directory-name "Notebook directory: " default-directory)
|
||||
nil
|
||||
callback-login
|
||||
(let* ((port (url-port parsed-url))
|
||||
(avoid (url-scheme-get-property (url-type parsed-url) 'default-port)))
|
||||
(cond ((= port avoid) nil)
|
||||
(t (url-port parsed-url)))))))
|
||||
(t (ein:notebooklist-login url-or-port callback-login)))))
|
||||
|
||||
(cl-loop for (lang . mode) in ob-ein-languages
|
||||
do (ob-ein--babelize-lang lang mode))
|
||||
|
||||
(defun ob-ein-kernel-interrupt ()
|
||||
"Interrupt kernel associated with session."
|
||||
(interactive)
|
||||
(org-babel-when-in-src-block
|
||||
(-if-let* ((info (org-babel-get-src-block-info))
|
||||
(pparams (cl-callf org-babel-process-params (nth 2 info)))
|
||||
(params (nth 2 info))
|
||||
(session (or (aand (cdr (assoc :session params))
|
||||
(unless (string= "none" it)
|
||||
(format "%s" it)))
|
||||
ein:url-localhost))
|
||||
(nbpath (ob-ein--parse-session session))
|
||||
(anonymous-path (format ob-ein-anonymous-path (nth 0 info)))
|
||||
(parsed-url (url-generic-parse-url nbpath))
|
||||
(slash-path (car (url-path-and-query parsed-url)))
|
||||
(path (if (string= slash-path "") anonymous-path
|
||||
(substring slash-path 1)))
|
||||
(url-or-port (if (string= slash-path "")
|
||||
nbpath
|
||||
(substring nbpath 0 (- (length slash-path)))))
|
||||
(notebook (ein:notebook-get-opened-notebook url-or-port path))
|
||||
(kernel (ein:$notebook-kernel notebook)))
|
||||
(ein:kernel-interrupt kernel)
|
||||
(ein:log 'info "ob-ein-kernel-interrupt: nothing to interrupt"))))
|
||||
|
||||
(define-key org-babel-map "\C-k" 'ob-ein-kernel-interrupt)
|
||||
|
||||
;;;###autoload
|
||||
(when (featurep 'org)
|
||||
(let* ((orig (get 'org-babel-load-languages 'custom-type))
|
||||
(orig-cdr (cdr orig))
|
||||
(choices (plist-get orig-cdr :key-type)))
|
||||
(push '(const :tag "Ein" ein) (nthcdr 1 choices))
|
||||
(put 'org-babel-load-languages 'custom-type
|
||||
(cons (car orig) (plist-put orig-cdr :key-type choices)))))
|
||||
|
||||
(provide 'ob-ein)
|
||||
496
lisp/ein/poly-ein.el
Normal file
496
lisp/ein/poly-ein.el
Normal file
@@ -0,0 +1,496 @@
|
||||
;;; poly-ein.el --- polymode for EIN -*- lexical-binding:t -*-
|
||||
|
||||
;; Copyright (C) 2019- The Authors
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; poly-ein.el is free software: you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; poly-ein.el is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with poly-ein.el. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;;
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'polymode)
|
||||
(require 'ein-cell)
|
||||
(require 'jit-lock)
|
||||
(require 'quail)
|
||||
(require 'display-line-numbers nil t)
|
||||
(require 'undo-tree nil t)
|
||||
|
||||
(declare-function ein:get-notebook "ein-notebook")
|
||||
(declare-function ein:notebook-mode "ein-notebook")
|
||||
|
||||
(declare-function polymode-inhibit-during-initialization "polymode-core")
|
||||
|
||||
(defmacro poly-ein--remove-hook (label functions)
|
||||
"Remove any hooks saying LABEL from FUNCTIONS"
|
||||
`(mapc (lambda (x) (when (and (symbolp x) (cl-search ,label (symbol-name x)))
|
||||
(remove-hook (quote ,functions) x t)))
|
||||
,functions))
|
||||
|
||||
(defun poly-ein--narrow-to-inner (modifier f &rest args)
|
||||
(if (or pm-initialization-in-progress (not poly-ein-mode))
|
||||
(apply f args)
|
||||
(save-restriction
|
||||
(widen)
|
||||
(let ((range (pm-innermost-range
|
||||
(or (if (numberp (car args))
|
||||
(max (funcall modifier (car args)) (point-min)))
|
||||
(point)))))
|
||||
(narrow-to-region (car range) (cdr range))
|
||||
(apply f args)))))
|
||||
|
||||
(defun poly-ein--decorate-functions ()
|
||||
"Affect global definitions of ppss and jit-lock rather intrusively."
|
||||
(mapc (lambda (fun)
|
||||
(dolist (adv (list 'poly-lock-no-jit-lock-in-polymode-buffers
|
||||
'polymode-inhibit-during-initialization))
|
||||
(when (advice-member-p adv fun)
|
||||
;; must set log level at toplevel to show following
|
||||
(ein:log 'debug "poly-ein--decorate-functions: removing %s from %s"
|
||||
adv fun)
|
||||
(advice-remove fun adv))))
|
||||
(list 'jit-lock-mode
|
||||
'font-lock-fontify-region
|
||||
'font-lock-fontify-buffer
|
||||
'font-lock-ensure))
|
||||
|
||||
;; https://github.com/millejoh/emacs-ipython-notebook/issues/537
|
||||
;; alternatively, filter-args on ad-should-compile but then we'd have to
|
||||
;; match on function name
|
||||
(custom-set-default 'ad-default-compilation-action 'never)
|
||||
|
||||
(add-function
|
||||
:before-until (symbol-function 'pm-select-buffer)
|
||||
(lambda (span &optional visibly)
|
||||
(prog1 poly-ein-mode
|
||||
(when poly-ein-mode
|
||||
(let ((src-buf (current-buffer))
|
||||
(dest-buf (pm-span-buffer span)))
|
||||
(unless (eq src-buf dest-buf)
|
||||
(poly-ein-set-buffer src-buf dest-buf visibly)))))))
|
||||
|
||||
(fmakunbound 'poly-lock-mode)
|
||||
(defalias 'poly-lock-mode (symbol-function (default-value 'font-lock-function)))
|
||||
|
||||
(defun poly-ein--syntax-propertize (pos)
|
||||
(prog1 poly-ein-mode
|
||||
(when (and poly-ein-mode (< syntax-propertize--done pos))
|
||||
(save-excursion
|
||||
;; pared down from default `syntax-propertize'
|
||||
(with-silent-modifications
|
||||
(let ((parse-sexp-lookup-properties t)
|
||||
(start (point-min)) ;; i've narrowed in the :around
|
||||
(end (point-max))
|
||||
(span (pm-innermost-span pos)))
|
||||
(setq syntax-propertize--done end)
|
||||
(when (eq 'body (nth 0 span))
|
||||
(remove-text-properties start end
|
||||
'(syntax-table nil syntax-multiline nil))
|
||||
;; avoid recursion if syntax-propertize-function calls me (syntax-propertize)
|
||||
(when syntax-propertize-function
|
||||
(let ((syntax-propertize--done most-positive-fixnum))
|
||||
(funcall syntax-propertize-function start end))))))))))
|
||||
(add-function
|
||||
:before-until (symbol-function 'syntax-propertize)
|
||||
#'poly-ein--syntax-propertize)
|
||||
|
||||
(add-function
|
||||
:around (symbol-function 'syntax-propertize)
|
||||
(apply-partially #'poly-ein--narrow-to-inner #'identity))
|
||||
|
||||
(add-function
|
||||
:around (symbol-function 'syntax-ppss)
|
||||
(apply-partially #'poly-ein--narrow-to-inner #'identity))
|
||||
|
||||
(add-function
|
||||
:around (symbol-function 'pm--mode-setup)
|
||||
(lambda (f &rest args)
|
||||
;; global-font-lock-mode will call an after-change-mode-hook
|
||||
;; that calls font-lock-initial-fontify, which fontifies the entire buffer!
|
||||
(cl-letf (((symbol-function 'global-font-lock-mode-enable-in-buffers) #'ignore))
|
||||
(when-let (b (or (cl-second args) (current-buffer)))
|
||||
(with-current-buffer b
|
||||
(unless (eq font-lock-support-mode 'jit-lock-mode)
|
||||
(ein:log 'info "pm--mode-setup: deactivating %s in %s"
|
||||
font-lock-support-mode (buffer-name))
|
||||
(setq-local font-lock-support-mode 'jit-lock-mode))))
|
||||
(apply f args))))
|
||||
|
||||
(add-function
|
||||
:around (symbol-function 'pm--common-setup)
|
||||
(lambda (f &rest args)
|
||||
"somewhere between pm--mode-setup and pm--common-setup is a
|
||||
kill-all-local-variables that douses any early attempt at
|
||||
overriding font-lock-support-mode."
|
||||
(when-let (b (or (cl-second args) (current-buffer)))
|
||||
(with-current-buffer b
|
||||
(unless (eq font-lock-support-mode 'jit-lock-mode)
|
||||
(ein:log 'info "pm--common-setup: deactivating %s in %s"
|
||||
font-lock-support-mode (buffer-name))
|
||||
(setq-local font-lock-support-mode 'jit-lock-mode))))
|
||||
(apply f args)))
|
||||
|
||||
(add-function
|
||||
:around (symbol-function 'jit-lock-mode)
|
||||
(lambda (f &rest args)
|
||||
;; Override jit-lock.el.gz deliberately skipping indirect buffers
|
||||
(cl-letf (((symbol-function 'buffer-base-buffer) #'ignore)) (apply f args))))
|
||||
|
||||
;; :before-until before :filter-args (reversed order when executed)
|
||||
|
||||
(add-function :before-until (symbol-function 'jit-lock-refontify)
|
||||
#'poly-ein--unrelated-span)
|
||||
|
||||
(add-function :before-until (symbol-function 'jit-lock-fontify-now)
|
||||
#'poly-ein--unrelated-span)
|
||||
|
||||
(add-function :filter-args (symbol-function 'jit-lock-refontify)
|
||||
#'poly-ein--span-start-end)
|
||||
|
||||
(add-function :filter-args (symbol-function 'jit-lock-fontify-now)
|
||||
#'poly-ein--span-start-end)
|
||||
|
||||
(add-function :filter-args (symbol-function 'font-lock-flush)
|
||||
#'poly-ein--span-start-end)
|
||||
|
||||
(add-function :filter-args (symbol-function 'jit-lock-after-change)
|
||||
#'poly-ein--span-start-end)
|
||||
|
||||
(add-function :before-until
|
||||
(symbol-function 'pm--synchronize-points)
|
||||
(lambda (&rest _args) poly-ein-mode))
|
||||
|
||||
(let ((dont-lookup-props
|
||||
(lambda (f &rest args)
|
||||
(let ((parse-sexp-lookup-properties (if poly-ein-mode
|
||||
nil
|
||||
parse-sexp-lookup-properties)))
|
||||
(apply f args)))))
|
||||
(add-function :around (symbol-function 'scan-lists) dont-lookup-props)
|
||||
(add-function :around (symbol-function 'scan-sexps) dont-lookup-props))
|
||||
|
||||
(advice-add 'other-buffer
|
||||
:filter-args
|
||||
(lambda (args)
|
||||
"Avoid switching to indirect buffers."
|
||||
(if poly-ein-mode
|
||||
(cons (or (buffer-base-buffer (car args)) (car args))
|
||||
(cdr args))
|
||||
args)))
|
||||
|
||||
(let* ((unadorned (symbol-function 'isearch-done))
|
||||
(after-isearch-done
|
||||
(lambda (&rest _args)
|
||||
"Clear `isearch-mode' for all base and indirect buffers."
|
||||
(-when-let* ((poly-ein-mode-p poly-ein-mode)
|
||||
(notebook (ein:get-notebook))
|
||||
(buffers (cl-remove-if (apply-partially #'string= (buffer-name))
|
||||
(ein:notebook-buffer-list notebook))))
|
||||
;; could just call unadorned, but what if `isearch-done' calls itself?
|
||||
(cl-letf (((symbol-function 'isearch-done) unadorned))
|
||||
(mapc (lambda (b) (with-current-buffer b (isearch-done))) buffers))))))
|
||||
(add-function :after (symbol-function 'isearch-done) after-isearch-done)))
|
||||
|
||||
(defmacro poly-ein-base (&rest body)
|
||||
"Copy the undo accounting to the base buffer and run BODY in it.
|
||||
This is a bottleneck as we do this on every `pm-get-span'."
|
||||
`(let ((base-buffer (pm-base-buffer))
|
||||
(derived-buffer (current-buffer))
|
||||
(pm-allow-post-command-hook nil)
|
||||
(quail (aand (overlayp quail-overlay)
|
||||
(overlay-start quail-overlay)
|
||||
(list it (overlay-end quail-overlay))))
|
||||
(quail-conv (aand (overlayp quail-conv-overlay)
|
||||
(overlay-start quail-conv-overlay)
|
||||
(list it (overlay-end quail-conv-overlay)))))
|
||||
(poly-ein-set-buffer derived-buffer base-buffer)
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'poly-ein--copy-state) #'ignore))
|
||||
,@body)
|
||||
(save-current-buffer
|
||||
(with-current-buffer derived-buffer
|
||||
(poly-ein-set-buffer base-buffer derived-buffer)
|
||||
(when quail
|
||||
(apply #'move-overlay quail-overlay quail))
|
||||
(when quail-conv
|
||||
(apply #'move-overlay quail-conv-overlay quail-conv)))))))
|
||||
|
||||
(defclass pm-inner-overlay-chunkmode (pm-inner-auto-chunkmode)
|
||||
()
|
||||
"Inner chunkmode delimited by cell overlays.")
|
||||
|
||||
(cl-defmethod pm-get-span ((cm pm-inner-overlay-chunkmode) &optional pos)
|
||||
"Return a list of the form (TYPE POS-START POS-END RESULT-CM).
|
||||
|
||||
TYPE can be \\='body, nil."
|
||||
(poly-ein-base
|
||||
(setq pos (or pos (point)))
|
||||
(when-let ((result-cm cm)
|
||||
(span `(nil ,(point-min) ,(point-min)))
|
||||
(cell (ein:worksheet-get-current-cell :pos pos :noerror t)))
|
||||
;; Change :mode if necessary
|
||||
(-when-let* ((nb (ein:get-notebook))
|
||||
(lang
|
||||
(condition-case err
|
||||
(ein:$kernelspec-language
|
||||
(ein:$notebook-kernelspec nb))
|
||||
(error (message "%s: defaulting language to python"
|
||||
(error-message-string err))
|
||||
"python")))
|
||||
(what (cond ((ein:codecell-p cell) lang)
|
||||
((ein:markdowncell-p cell) "ein:markdown")
|
||||
(t "fundamental")))
|
||||
(mode (pm-get-mode-symbol-from-name what))
|
||||
(f (not (equal mode (ein:oref-safe cm 'mode)))))
|
||||
(when (eq mode 'poly-fallback-mode)
|
||||
(let ((warning (format (concat "pm-get-span: Add (%s . [mode-prefix]) to "
|
||||
"polymode-mode-name-aliases")
|
||||
what)))
|
||||
(when (or (not (get-buffer "*Warnings*"))
|
||||
(not (with-current-buffer "*Warnings*"
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(re-search-forward (regexp-quote warning) nil t)))))
|
||||
(ein:display-warning warning))))
|
||||
(setq result-cm
|
||||
(cl-loop for ocm in (eieio-oref pm/polymode '-auto-innermodes)
|
||||
when (equal mode (ein:oref-safe ocm 'mode))
|
||||
return ocm
|
||||
finally return (let ((new-mode (clone cm :mode mode)))
|
||||
(object-add-to-list pm/polymode '-auto-innermodes
|
||||
new-mode)
|
||||
new-mode))))
|
||||
;; Span is a zebra pattern of "body" (within input cell) and "nil"
|
||||
;; (outside input cell). Decide boundaries of span and return it.
|
||||
(let ((rel (poly-ein--relative-to-input pos cell)))
|
||||
(cond ((zerop rel)
|
||||
(setq span `(body
|
||||
,(ein:cell-input-pos-min cell)
|
||||
,(1+ (ein:cell-input-pos-max cell)))))
|
||||
((< rel 0)
|
||||
(setq span `(nil
|
||||
,(or (ein:aand (ein:cell-prev cell)
|
||||
(1+ (ein:cell-input-pos-max it)))
|
||||
(point-min))
|
||||
,(ein:cell-input-pos-min cell))))
|
||||
(t
|
||||
(setq span `(nil
|
||||
,(1+ (ein:cell-input-pos-max cell))
|
||||
,(or (ein:aand (ein:cell-next cell)
|
||||
(ein:cell-input-pos-min it))
|
||||
(point-max)))))))
|
||||
(append span (list result-cm)))))
|
||||
|
||||
(defun poly-ein-fontify-buffer (buffer)
|
||||
"Called from `ein:notebook--worksheet-render'"
|
||||
(with-current-buffer buffer
|
||||
(save-excursion
|
||||
(pm-map-over-spans
|
||||
(lambda (span)
|
||||
(with-current-buffer (pm-span-buffer span)
|
||||
(cl-assert (eq font-lock-function 'poly-lock-mode))
|
||||
(ignore-errors (jit-lock-function (nth 1 span)))))))))
|
||||
|
||||
(defun poly-ein--relative-to-input (pos cell)
|
||||
"Return -1 if POS before input, 1 if after input, 0 if within"
|
||||
(let* ((input-pos-min (ein:cell-input-pos-min cell))
|
||||
(input-pos-max (ein:cell-input-pos-max cell)))
|
||||
(cond ((< pos input-pos-min) -1)
|
||||
((> pos input-pos-max) 1)
|
||||
(t 0))))
|
||||
|
||||
(defvar jit-lock-start)
|
||||
(defvar jit-lock-end)
|
||||
(defun poly-ein--hem-jit-lock (start _end _old-len)
|
||||
(when (and poly-ein-mode (not pm-initialization-in-progress))
|
||||
(let ((range (pm-innermost-range (or start (point)))))
|
||||
(setq jit-lock-start (max jit-lock-start (car range)))
|
||||
(setq jit-lock-end (min jit-lock-end (cdr range))))))
|
||||
|
||||
(defun poly-ein-initialize (type)
|
||||
(poly-ein--remove-hook "polymode" after-change-functions)
|
||||
(poly-ein--remove-hook "polymode" syntax-propertize-extend-region-functions)
|
||||
(add-hook 'jit-lock-after-change-extend-region-functions #'poly-ein--hem-jit-lock t t)
|
||||
(setq jit-lock-contextually nil) ; else recenter font-lock-fontify-keywords-region
|
||||
(setq jit-lock-context-unfontify-pos nil)
|
||||
(when (ein:eval-if-bound 'display-line-numbers-mode)
|
||||
(when (fboundp 'display-line-numbers-mode)
|
||||
(display-line-numbers-mode -1)))
|
||||
(when (ein:eval-if-bound 'linum-mode)
|
||||
(when (fboundp 'linum-mode)
|
||||
(linum-mode -1)))
|
||||
(when (ein:eval-if-bound 'undo-tree-mode)
|
||||
(when (fboundp 'undo-tree-mode)
|
||||
(undo-tree-mode -1)))
|
||||
(when visual-line-mode
|
||||
(visual-line-mode -1))
|
||||
(if (eq type 'host)
|
||||
(setq syntax-propertize-function nil)
|
||||
(aif pm--syntax-propertize-function-original
|
||||
(progn
|
||||
(setq syntax-propertize-function it)
|
||||
(add-function :before-until (local 'syntax-propertize-function)
|
||||
#'poly-ein--unrelated-span)
|
||||
(add-function :filter-args (local 'syntax-propertize-function)
|
||||
#'poly-ein--span-start-end)))
|
||||
(add-function :around (local 'font-lock-syntactic-face-function)
|
||||
(apply-partially #'poly-ein--narrow-to-inner #'identity))))
|
||||
|
||||
(defun poly-ein--record-window-buffer ()
|
||||
"(pm--visible-buffer-name) needs to get onto window's prev-buffers.
|
||||
But `C-x b` seems to consult `buffer-list' and not the C (window)->prev_buffers."
|
||||
(when (buffer-base-buffer)
|
||||
(let* ((buffer-list (frame-parameter nil 'buffer-list))
|
||||
(pos-visible (seq-position
|
||||
buffer-list
|
||||
(pm--visible-buffer-name)
|
||||
(lambda (x visible*)
|
||||
(string-prefix-p (buffer-name x) visible*)))))
|
||||
;; no way to know if i've switched in or out of indirect buf.
|
||||
;; (if in, I *don't* want to add visible to buffer-list)
|
||||
(cond ((and (numberp pos-visible) (> pos-visible 0))
|
||||
(let ((visible-buffer (nth pos-visible buffer-list)))
|
||||
(setcdr (nthcdr (1- pos-visible) buffer-list)
|
||||
(nthcdr (1+ pos-visible) buffer-list))
|
||||
(set-frame-parameter nil 'buffer-list (cons visible-buffer buffer-list))))
|
||||
((null pos-visible)
|
||||
(set-frame-parameter nil 'buffer-list
|
||||
(cons (buffer-base-buffer) buffer-list)))))))
|
||||
|
||||
(defun poly-ein-init-input-cell (_type)
|
||||
"Contrary to intuition, this inits the entire buffer of input cells
|
||||
(collectively denoted by the chunkmode pm-inner/ein-input-cell), not each individual one."
|
||||
(mapc (lambda (f) (add-hook 'after-change-functions f t t))
|
||||
(buffer-local-value 'after-change-functions (pm-base-buffer)))
|
||||
(setq-local font-lock-dont-widen t)
|
||||
(setq-local syntax-propertize-chunks 0) ;; internal--syntax-propertize too far
|
||||
(add-hook 'buffer-list-update-hook #'poly-ein--record-window-buffer nil t)
|
||||
(add-hook 'ido-make-buffer-list-hook
|
||||
(lambda ()
|
||||
(defvar ido-temp-list)
|
||||
(when-let ((visible (pm--visible-buffer-name)))
|
||||
(ido-to-end (delq nil
|
||||
(mapcar (lambda (x)
|
||||
(when (string-prefix-p x visible) x))
|
||||
ido-temp-list)))))
|
||||
nil t)
|
||||
(ein:notebook-mode)
|
||||
(unless (eq 'ein:notebook-mode (caar minor-mode-map-alist))
|
||||
;; move `ein:notebook-mode' to the head of `minor-mode-map-alist'
|
||||
(when-let ((entry (assq 'ein:notebook-mode minor-mode-map-alist)))
|
||||
(setf minor-mode-map-alist
|
||||
(cons entry
|
||||
(assq-delete-all 'ein:notebook-mode minor-mode-map-alist))))))
|
||||
|
||||
(defcustom pm-host/ein
|
||||
(pm-host-chunkmode :name "ein"
|
||||
:init-functions '(poly-ein-initialize))
|
||||
"EIN host chunkmode"
|
||||
:group 'poly-hostmodes
|
||||
:type 'object)
|
||||
|
||||
(defcustom pm-inner/ein-input-cell
|
||||
(pm-inner-overlay-chunkmode :name "ein-input-cell"
|
||||
:init-functions '(poly-ein-initialize poly-ein-init-input-cell))
|
||||
"EIN input cell."
|
||||
:group 'poly-innermodes
|
||||
:type 'object)
|
||||
|
||||
(defcustom poly-ein-mode-hook nil
|
||||
"Hook for poly-ein-mode"
|
||||
:type 'hook :group 'poly-ein)
|
||||
|
||||
(unless (fboundp 'with-suppressed-warnings)
|
||||
(defmacro with-suppressed-warnings (warnings &rest body)
|
||||
`(progn (ignore ',warnings) ,@body)))
|
||||
|
||||
;;;###autoload (autoload 'poly-ein-mode "poly-ein")
|
||||
(with-suppressed-warnings ((obsolete easy-mmode-define-keymap))
|
||||
(define-polymode poly-ein-mode
|
||||
:lighter " PM-ipynb"
|
||||
:hostmode 'pm-host/ein
|
||||
:innermodes '(pm-inner/ein-input-cell)))
|
||||
|
||||
(defun poly-ein--copy-state (src-buf dest-buf)
|
||||
"Dangerous to call this outside `poly-ein-set-buffer' (loses overlays)."
|
||||
(unless (eq src-buf dest-buf)
|
||||
(dolist (b (eieio-oref pm/polymode '-buffers))
|
||||
(unless (eq b dest-buf)
|
||||
(with-current-buffer b
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(widen)
|
||||
(dolist (ol (overlays-in (point-min) (point-max)))
|
||||
(move-overlay ol (overlay-start ol) (overlay-end ol) dest-buf)))))))
|
||||
(pm--move-vars (append ein:local-variables
|
||||
'(header-line-format buffer-undo-list isearch-mode))
|
||||
src-buf dest-buf)))
|
||||
|
||||
(defun poly-ein-set-buffer (src-buf dest-buf &optional switch)
|
||||
(let ((pm-initialization-in-progress t))
|
||||
(when (and (not (eq src-buf dest-buf))
|
||||
(buffer-live-p src-buf)
|
||||
(buffer-live-p dest-buf))
|
||||
(cl-destructuring-bind (point window-start region-begin pos-visible _)
|
||||
(with-current-buffer src-buf (list (point)
|
||||
(window-start)
|
||||
(and switch (region-active-p) (mark))
|
||||
(pos-visible-in-window-p)
|
||||
(when switch (deactivate-mark))))
|
||||
(poly-ein--copy-state src-buf dest-buf)
|
||||
(if switch
|
||||
(switch-to-buffer dest-buf)
|
||||
(set-buffer dest-buf))
|
||||
(when region-begin
|
||||
(setq deactivate-mark nil) ;; someone is setting this, I don't know who
|
||||
(push-mark region-begin t t))
|
||||
(goto-char point)
|
||||
(setq syntax-propertize--done (point-min))
|
||||
(when switch
|
||||
(when pos-visible
|
||||
(set-window-start (get-buffer-window) window-start))
|
||||
(bury-buffer-internal src-buf)
|
||||
(set-window-prev-buffers
|
||||
nil
|
||||
(assq-delete-all src-buf (window-prev-buffers nil)))
|
||||
(run-hook-with-args 'polymode-switch-buffer-hook src-buf dest-buf)
|
||||
(pm--run-hooks pm/polymode :switch-buffer-functions src-buf dest-buf)
|
||||
(pm--run-hooks pm/chunkmode :switch-buffer-functions src-buf dest-buf))))))
|
||||
|
||||
(defsubst poly-ein--span-start-end (args)
|
||||
(if (or pm-initialization-in-progress (not poly-ein-mode))
|
||||
args
|
||||
(let* ((span-start (cl-first args))
|
||||
(span-end (cl-second args))
|
||||
(range (pm-innermost-range (or span-start (point)))))
|
||||
(setq span-start (max (or span-start (car range)) (car range)))
|
||||
(setq span-end (min (or span-end (cdr range)) (cdr range)))
|
||||
(append (list span-start span-end) (cddr args)))))
|
||||
|
||||
(defsubst poly-ein--unrelated-span (&optional beg _end)
|
||||
(or pm-initialization-in-progress
|
||||
(and poly-ein-mode
|
||||
(let* ((span (pm-innermost-span (or beg (point))))
|
||||
(span-mode (eieio-oref (nth 3 span) 'mode)))
|
||||
;; only fontify type 'body (the other type is nil)
|
||||
(or (null (nth 0 span)) (not (eq major-mode span-mode)))))))
|
||||
|
||||
(make-variable-buffer-local 'parse-sexp-lookup-properties)
|
||||
|
||||
(poly-ein--decorate-functions)
|
||||
|
||||
(provide 'poly-ein)
|
||||
50
lisp/jupyter/Makefile
Normal file
50
lisp/jupyter/Makefile
Normal file
@@ -0,0 +1,50 @@
|
||||
EMACS ?= emacs
|
||||
ELDEV ?= $(shell command -v eldev)
|
||||
|
||||
FILES = $(wildcard *.el)
|
||||
ELCFILES = $(FILES:.el=.elc)
|
||||
TESTFILES = $(foreach file,$(wildcard test/*.el),-l $(file))
|
||||
TESTSELECTORS =
|
||||
|
||||
ifneq ($(TAGS),)
|
||||
comma := ,
|
||||
TESTSELECTORS := $(foreach tag,$(subst $(comma), ,$(TAGS)),"(tag $(tag))")
|
||||
endif
|
||||
|
||||
ifneq ($(PATTERN),)
|
||||
TESTSELECTORS := $(TESTSELECTORS) \"$(PATTERN)\"
|
||||
endif
|
||||
|
||||
# ifneq ($(TESTSELECTORS),)
|
||||
# TESTSELECTORS := (quote (or $(TESTSELECTORS)))
|
||||
# endif
|
||||
|
||||
.PHONY: all
|
||||
all: compile
|
||||
|
||||
.PHONY: eldev
|
||||
eldev:
|
||||
ifeq ($(ELDEV),)
|
||||
$(error "Install eldev (https://github.com/doublep/eldev)")
|
||||
endif
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(ELDEV) test $(TESTSELECTORS)
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
make -C js clean
|
||||
@rm $(ELCFILES) 2>/dev/null || true
|
||||
|
||||
.PHONY: clean-eldev
|
||||
clean-eldev:
|
||||
@rm -rf .eldev/ 2>/dev/null || true
|
||||
|
||||
.PHONY: widgets
|
||||
widgets:
|
||||
make -C js
|
||||
|
||||
.PHONY: compile
|
||||
compile:
|
||||
$(ELDEV) compile
|
||||
28
lisp/jupyter/js/Makefile
Normal file
28
lisp/jupyter/js/Makefile
Normal file
@@ -0,0 +1,28 @@
|
||||
SHELL = bash
|
||||
|
||||
NPM ?= $(shell command -v npm)
|
||||
ifeq ($(NPM),)
|
||||
$(error "Node not installed (https://nodejs.org/en/)")
|
||||
endif
|
||||
|
||||
YARN ?= $(shell command -v yarn)
|
||||
ifeq ($(YARN),)
|
||||
# If yarn isn't already installed, it is built locally
|
||||
YARN = ./node_modules/.bin/yarn
|
||||
endif
|
||||
|
||||
.PHONY: all build clean
|
||||
|
||||
all: build
|
||||
|
||||
clean:
|
||||
@rm -rf built/ 2>/dev/null || true
|
||||
|
||||
really-clean: clean
|
||||
@rm -rf node_modules 2>/dev/null || true
|
||||
|
||||
build: built/index.built.js
|
||||
|
||||
built/index.built.js:
|
||||
$(NPM) install
|
||||
$(YARN) run build --progress
|
||||
342
lisp/jupyter/js/emacs-jupyter.js
Normal file
342
lisp/jupyter/js/emacs-jupyter.js
Normal file
@@ -0,0 +1,342 @@
|
||||
// NOTE: Info on widgets http://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Low%20Level.html
|
||||
var disposable = require('@phosphor/disposable');
|
||||
var coreutils = require('@jupyterlab/coreutils');
|
||||
// The KernelFutureHandler allows comms to register their callbacks to be
|
||||
// called when messages are received in response to a request sent to the
|
||||
// kernel.
|
||||
var KernelFutureHandler = require('@jupyterlab/services/kernel/future').KernelFutureHandler;
|
||||
// The CommHandler object handles comm interaction to/from the kernel. It takes
|
||||
// a target_name, usually jupyter.widget, and a comm_id. It takes care of
|
||||
// sending comm messages to the kernel and calls the callback methods of a Comm
|
||||
// when a comm_msg is received from the kernel.
|
||||
//
|
||||
// A Comm object is essentially a wrapper around a CommHandler that updates the
|
||||
// CommHandler callbacks and registers callbacks on the futures created when a
|
||||
// Comm sends a message on the shell channel.
|
||||
var CommHandler = require('@jupyterlab/services/kernel/comm').CommHandler;
|
||||
|
||||
|
||||
// A CommManager takes care of registering new comm targets and creating new
|
||||
// comms and holding a list of all the live comms.
|
||||
|
||||
// It looks like I just ned to implement the IKernel interface and pass the
|
||||
// object that implements it to CommManager, this way I can create new comms
|
||||
// with CommManager.new_comm when handling comm_open messages. In the IKernel
|
||||
// interface, I'll just redirect all the message sending functions to Emacs.
|
||||
|
||||
// It looks like widgets send messages through the callbacks of a
|
||||
// KernelFutureHandler so I will have to redirect all received messages that
|
||||
// originated from a request generated by skewer.postJSON back to the
|
||||
// JavaScript environment. Emacs then acts as an intermediary, capturing kernel
|
||||
// messages and re-packaging them to send to the Javascript environment.
|
||||
//
|
||||
// It looks like whenever the kernel receives a message it accesse the correct
|
||||
// future object using this.futures.get and calls handleMsg function of the
|
||||
// future.
|
||||
//
|
||||
// The flow of message with respect to Comm objects is that Comm object send
|
||||
// shell messages, then widgets register callbacks on the future.
|
||||
|
||||
var EmacsJupyter = function(options, port) {
|
||||
var _this = this;
|
||||
|
||||
this.username = options.username || '';
|
||||
// This is the Jupyter session id
|
||||
this.clientId = options.clientId;
|
||||
this.isDisposed = false;
|
||||
// A mapping from comm_id's to promises that resolve to their open Comm
|
||||
// objects.
|
||||
this.commPromises = new Map();
|
||||
// The targetRegistry is a dictionary mapping target names to target
|
||||
// functions that are called whenever a new Comm is requested to be open by
|
||||
// the kernel. The target function gets called with the initial comm_open
|
||||
// message data and a comm handler for the new Comm.
|
||||
this.targetRegistry = {};
|
||||
// A mapping of msg_id's for messages sent to the kernel and their
|
||||
// KernelFutureHandler objects.
|
||||
this.futures = new Map();
|
||||
// The WidgetManager that connects comms to their corresponding widget
|
||||
// models, construct widget views, load widget modules, and get the current
|
||||
// widget state.
|
||||
this.widgetManager = null;
|
||||
this.widgetState = null;
|
||||
// The CommManager that registers the target names and their target
|
||||
// functions handles opening and closing comms for a particular
|
||||
// target name.
|
||||
this.commManager = null;
|
||||
this.messagePromise = new Promise(function (resolve) { resolve(); });
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
// TODO: Send widget state
|
||||
});
|
||||
|
||||
// Localhost
|
||||
this.wsPort = port;
|
||||
this.ws = new WebSocket("ws://127.0.0.1:" + port);
|
||||
this.ws.onopen = function () {
|
||||
// Ensure that Emacs knows which websocket connection corresponds to
|
||||
// each kernel client
|
||||
_this.ws.send(JSON.stringify({
|
||||
header: {
|
||||
msg_type: "connect",
|
||||
session: _this.clientId
|
||||
}
|
||||
}));
|
||||
};
|
||||
this.ws.onmessage = function(event) {
|
||||
if(_this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
var msg = JSON.parse(event.data);
|
||||
_this.messagePromise =
|
||||
_this.messagePromise.then(function () {
|
||||
if(msg.buffers && msg.buffers.length > 0) {
|
||||
for(var i = 0; i < msg.buffers.length; i++) {
|
||||
var bin = atob(msg.buffers[i]);
|
||||
var len = bin.length;
|
||||
var buf = new Uint8Array(len);
|
||||
for(var j = 0; j < len; j++) {
|
||||
buf[j] = bin.charCodeAt(j);
|
||||
}
|
||||
msg.buffers[i] = buf.buffer;
|
||||
}
|
||||
}
|
||||
_this.handleMessage(msg);
|
||||
});
|
||||
};
|
||||
};
|
||||
exports.EmacsJupyter = EmacsJupyter;
|
||||
|
||||
EmacsJupyter.prototype.dispose = function () {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this.isDisposed = true;
|
||||
this.commPromises.forEach(function (promise, key) {
|
||||
promise.then(function (comm) {
|
||||
comm.dispose();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.registerCommTarget = function(targetName, callback) {
|
||||
var _this = this;
|
||||
this.targetRegistry[targetName] = callback;
|
||||
return new disposable.DisposableDelegate(function () {
|
||||
if (!_this.isDisposed) {
|
||||
delete _this.targetRegistry[targetName];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.connectToComm = function (targetName, commId) {
|
||||
var _this = this;
|
||||
var id = commId || coreutils.uuid();
|
||||
if (this.commPromises.has(id)) {
|
||||
return this.commPromises.get(id);
|
||||
}
|
||||
var promise = Promise.resolve(void 0).then(function () {
|
||||
return new CommHandler(targetName, id, _this, function () {
|
||||
_this._unregisterComm(id);
|
||||
});
|
||||
});
|
||||
this.commPromises.set(id, promise);
|
||||
return promise;
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.handleCommOpen = function (msg) {
|
||||
var _this = this;
|
||||
var content = msg.content;
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
var promise = this.loadObject(content.target_name,
|
||||
content.target_module,
|
||||
this.targetRegistry)
|
||||
.then(function (target) {
|
||||
var comm = new CommHandler(content.target_name,
|
||||
content.comm_id,
|
||||
_this, function () {
|
||||
_this._unregisterComm(content.comm_id);
|
||||
});
|
||||
var response;
|
||||
try {
|
||||
response = target(comm, msg);
|
||||
}
|
||||
catch (e) {
|
||||
comm.close();
|
||||
console.error('Exception opening new comm');
|
||||
throw (e);
|
||||
}
|
||||
return Promise.resolve(response).then(function () {
|
||||
if (_this.isDisposed) {
|
||||
return null;
|
||||
}
|
||||
return comm;
|
||||
});
|
||||
});
|
||||
this.commPromises.set(content.comm_id, promise);
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.handleCommClose = function (msg) {
|
||||
var _this = this;
|
||||
var content = msg.content;
|
||||
var promise = this.commPromises.get(content.comm_id);
|
||||
if (!promise) {
|
||||
console.error('Comm not found for comm id ' + content.comm_id);
|
||||
return;
|
||||
}
|
||||
promise.then(function (comm) {
|
||||
if (!comm) {
|
||||
return;
|
||||
}
|
||||
_this._unregisterComm(comm.commId);
|
||||
try {
|
||||
var onClose = comm.onClose;
|
||||
if (onClose) {
|
||||
onClose(msg);
|
||||
}
|
||||
comm.dispose();
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Exception closing comm: ', e, e.stack, msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.handleCommMsg = function (msg) {
|
||||
var promise = this.commPromises.get(msg.content.comm_id);
|
||||
if (!promise) {
|
||||
// We do have a registered comm for this comm id, ignore.
|
||||
return;
|
||||
}
|
||||
promise.then(function (comm) {
|
||||
if (!comm) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var onMsg = comm.onMsg;
|
||||
if (onMsg) {
|
||||
onMsg(msg);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Exception handling comm msg: ', e, e.stack, msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.loadObject = function(name, moduleName, registry) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
// Try loading the view module using require.js
|
||||
if (moduleName) {
|
||||
if (typeof window.require === 'undefined') {
|
||||
throw new Error('requirejs not found');
|
||||
}
|
||||
window.require([moduleName], function (mod) {
|
||||
if (mod[name] === void 0) {
|
||||
var msg = "Object '" + name + "' not found in module '" + moduleName + "'";
|
||||
reject(new Error(msg));
|
||||
}
|
||||
else {
|
||||
resolve(mod[name]);
|
||||
}
|
||||
}, reject);
|
||||
}
|
||||
else {
|
||||
if (registry && registry[name]) {
|
||||
resolve(registry[name]);
|
||||
}
|
||||
else {
|
||||
reject(new Error("Object '" + name + "' not found in registry"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EmacsJupyter.prototype._unregisterComm = function (commId) {
|
||||
this.commPromises.delete(commId);
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.sendShellMessage = function(msg, expectReply, disposeOnDone) {
|
||||
var _this = this;
|
||||
if (expectReply === void 0) { expectReply = false; }
|
||||
if (disposeOnDone === void 0) { disposeOnDone = true; }
|
||||
|
||||
var future = new KernelFutureHandler(function () {
|
||||
var msgId = msg.header.msg_id;
|
||||
_this.futures.delete(msgId);
|
||||
}, msg, expectReply, disposeOnDone, this);
|
||||
|
||||
this.ws.send(JSON.stringify(msg));
|
||||
this.futures.set(msg.header.msg_id, future);
|
||||
return future;
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.requestCommInfo = function(targetName) {
|
||||
var msg = {
|
||||
channel: 'shell',
|
||||
msg_type: 'comm_info_request',
|
||||
// A message ID will be added by Emacs anyway
|
||||
header: {msg_id: ''},
|
||||
content: {target_name: targetName}
|
||||
};
|
||||
var future = this.sendShellMessage(msg, true);
|
||||
return new Promise(function (resolve) {
|
||||
future.onReply = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
EmacsJupyter.prototype.handleMessage = function(msg) {
|
||||
var _this = this;
|
||||
var parentHeader = msg.parent_header;
|
||||
var future = parentHeader && this.futures && this.futures.get(parentHeader.msg_id);
|
||||
if (future) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
future.handleMsg(msg);
|
||||
resolve(msg);
|
||||
} catch(err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return new Promise(function (resolve, reject) {
|
||||
switch(msg.msg_type) {
|
||||
// Special messages not really a Jupyter message
|
||||
case 'display_model':
|
||||
_this.widgetManager.get_model(msg.content.model_id).then(function (model) {
|
||||
_this.widgetManager.display_model(undefined, model);
|
||||
});
|
||||
break;
|
||||
case 'clear_display':
|
||||
var widget = _this.widgetManager.area;
|
||||
while(widget.firstChild) {
|
||||
widget.removeChild(widget.firstChild);
|
||||
}
|
||||
break;
|
||||
// Regular Jupyter messages
|
||||
case 'comm_open':
|
||||
_this.handleCommOpen(msg);
|
||||
// Periodically get the state of the widgetManager, this gets
|
||||
// sent to the browser when its unloaded.
|
||||
// _this.widgetManager.get_state({}).then(function (state) {
|
||||
// _this.widgetState = state;
|
||||
// });
|
||||
break;
|
||||
case 'comm_close':
|
||||
_this.handleCommClose(msg);
|
||||
break;
|
||||
case 'comm_msg':
|
||||
_this.handleCommMsg(msg);
|
||||
break;
|
||||
case 'status':
|
||||
// Comes from the comm info messages
|
||||
break;
|
||||
default:
|
||||
reject(new Error('Unhandled message', msg));
|
||||
};
|
||||
resolve(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
12
lisp/jupyter/js/index.js
Normal file
12
lisp/jupyter/js/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
window.CommManager = require('@jupyter-widgets/base').shims.services.CommManager;
|
||||
window.WidgetManager = require('./manager').WidgetManager;
|
||||
window.EmacsJupyter = require('./emacs-jupyter').EmacsJupyter;
|
||||
require('font-awesome/css/font-awesome.min.css');
|
||||
require('@jupyter-widgets/controls/css/widgets.built.css');
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
|
||||
var widget = document.createElement("div");
|
||||
widget.setAttribute("id", "widget");
|
||||
document.body.appendChild(widget);
|
||||
});
|
||||
82
lisp/jupyter/js/manager.js
Normal file
82
lisp/jupyter/js/manager.js
Normal file
@@ -0,0 +1,82 @@
|
||||
var base = require('@jupyter-widgets/base');
|
||||
var output = require('@jupyter-widgets/output');
|
||||
var controls = require('@jupyter-widgets/controls');
|
||||
var PhosphorWidget = require('@phosphor/widgets').Widget;
|
||||
|
||||
var defineWidgetModules = function () {
|
||||
if(window.define) {
|
||||
window.define('@jupyter-widgets/output', [], function () { return output; });
|
||||
window.define('@jupyter-widgets/base', [], function () { return base; });
|
||||
window.define('@jupyter-widgets/controls', [], function () { return controls; });
|
||||
} else {
|
||||
setTimeout(defineWidgetModules, 50);
|
||||
}
|
||||
};
|
||||
|
||||
// requirejs loading is async so it may not be available on this event
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
defineWidgetModules();
|
||||
});
|
||||
|
||||
var WidgetManager = exports.WidgetManager = function(kernel, area) {
|
||||
base.ManagerBase.call(this);
|
||||
this.kernel = kernel;
|
||||
this.area = area;
|
||||
};
|
||||
WidgetManager.prototype = Object.create(base.ManagerBase.prototype);
|
||||
|
||||
WidgetManager.prototype.loadClass = function(className, moduleName, moduleVersion) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (moduleName === '@jupyter-widgets/controls') {
|
||||
resolve(controls);
|
||||
} else if (moduleName === '@jupyter-widgets/base') {
|
||||
resolve(base);
|
||||
} else if (moduleName === '@jupyter-widgets/output')
|
||||
resolve(output);
|
||||
else {
|
||||
var fallback = function(err) {
|
||||
var failedId = err.requireModules && err.requireModules[0];
|
||||
if (failedId) {
|
||||
console.log('Falling back to unpkg.com for ' + moduleName + '@' + moduleVersion);
|
||||
window.require(['https://unpkg.com/' + moduleName + '@' + moduleVersion + '/dist/index.js'], resolve, reject);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
window.require([moduleName + '.js'], resolve, fallback);
|
||||
}
|
||||
}).then(function(module) {
|
||||
if (module[className]) {
|
||||
return module[className];
|
||||
} else {
|
||||
return Promise.reject('Class ' + className + ' not found in module ' + moduleName + '@' + moduleVersion);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
WidgetManager.prototype.display_view = function(msg, view, options) {
|
||||
var _this = this;
|
||||
return Promise.resolve(view).then(function(view) {
|
||||
PhosphorWidget.attach(view.pWidget, _this.area);
|
||||
view.on('remove', function() {
|
||||
console.log('View removed', view);
|
||||
});
|
||||
view.trigger('displayed');
|
||||
return view;
|
||||
});
|
||||
};
|
||||
|
||||
WidgetManager.prototype._get_comm_info = function() {
|
||||
return this.kernel.requestCommInfo(this.comm_target_name).then(function(reply) {
|
||||
return reply.content.comms;
|
||||
});
|
||||
};
|
||||
|
||||
WidgetManager.prototype._create_comm = function(targetName, commId, data, metadata) {
|
||||
// Construct a comm that already exists
|
||||
var comm = this.kernel.connectToComm(targetName, commId);
|
||||
if(data || metadata) {
|
||||
comm.open(data, metadata);
|
||||
}
|
||||
return Promise.resolve(new base.shims.services.Comm(comm));
|
||||
}
|
||||
33
lisp/jupyter/js/package.json
Normal file
33
lisp/jupyter/js/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "emacs-jupyter",
|
||||
"version": "0.3.0",
|
||||
"description": "Integrate emacs-jupyter with widgets in a browser.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"clean": "rm -rf built",
|
||||
"build": "webpack",
|
||||
"test": "npm run test:default",
|
||||
"test:default": "echo \"No test specified\""
|
||||
},
|
||||
"author": "Nathaniel Nicandro",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"dependencies": {
|
||||
"@jupyter-widgets/base": "^1.2.2",
|
||||
"@jupyter-widgets/controls": "^1.2.1",
|
||||
"@jupyter-widgets/output": "^1.0.10",
|
||||
"codemirror": "^5.9.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"npm": "^6.4.1",
|
||||
"yarn": "^1.12.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "^0.28.4",
|
||||
"file-loader": "^0.11.2",
|
||||
"json-loader": "^0.5.4",
|
||||
"raw-loader": "^0.5.1",
|
||||
"style-loader": "^0.18.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^3.5.5"
|
||||
}
|
||||
}
|
||||
29
lisp/jupyter/js/webpack.config.js
Normal file
29
lisp/jupyter/js/webpack.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
var path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: "./index.js",
|
||||
output: {
|
||||
filename: 'index.built.js',
|
||||
path: path.resolve(__dirname, 'built'),
|
||||
publicPath: 'built/'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@jupyterlab/services/kernel/future': path.resolve(__dirname, 'node_modules/@jupyterlab/services/lib/kernel/future'),
|
||||
'@jupyterlab/services/kernel/comm': path.resolve(__dirname, 'node_modules/@jupyterlab/services/lib/kernel/comm')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.css$/, loader: "style-loader!css-loader" },
|
||||
// jquery-ui loads some images
|
||||
{ test: /\.(jpg|png|gif)$/, use: 'file-loader' },
|
||||
// required to load font-awesome
|
||||
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?mimetype=application/font-woff' },
|
||||
{ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?mimetype=application/font-woff' },
|
||||
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?mimetype=application/octet-stream' },
|
||||
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, use: 'file-loader' },
|
||||
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, use: 'url-loader?mimetype=image/svg+xml' }
|
||||
]
|
||||
},
|
||||
}
|
||||
63
lisp/jupyter/jupyter-R.el
Normal file
63
lisp/jupyter/jupyter-R.el
Normal file
@@ -0,0 +1,63 @@
|
||||
;;; jupyter-R.el --- Jupyter support for R -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Jack Kamm <jackkamm@gmail.com>
|
||||
;; Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Support methods for integration with R.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-repl)
|
||||
(require 'jupyter-org-client)
|
||||
(require 'jupyter-mime)
|
||||
|
||||
(defvar ess-font-lock-keywords)
|
||||
|
||||
(cl-defmethod jupyter-repl-initialize-fontification (&context (jupyter-lang R))
|
||||
(when (featurep 'ess)
|
||||
(setq-local ess-font-lock-keywords 'ess-R-font-lock-keywords))
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-org-result ((_mime (eql :text/html)) content params
|
||||
&context (jupyter-lang R))
|
||||
"If html DATA is an iframe, save it to a separate file and open in browser.
|
||||
Otherwise, parse it as normal."
|
||||
(if (plist-get (plist-get content :metadata) :isolated)
|
||||
(let* ((data (plist-get content :data))
|
||||
(file (or (alist-get :file params)
|
||||
(jupyter-org-image-file-name data ".html"))))
|
||||
(with-temp-file file
|
||||
(insert data))
|
||||
(browse-url-of-file file)
|
||||
(jupyter-org-file-link file))
|
||||
(cl-call-next-method)))
|
||||
|
||||
(cl-defmethod jupyter-insert ((_mime (eql :text/html)) data
|
||||
&context (jupyter-lang R)
|
||||
&optional metadata)
|
||||
(if (plist-get metadata :isolated)
|
||||
(jupyter-browse-url-in-temp-file data)
|
||||
(cl-call-next-method)))
|
||||
|
||||
(provide 'jupyter-R)
|
||||
|
||||
;;; jupyter-R.el ends here
|
||||
793
lisp/jupyter/jupyter-base.el
Normal file
793
lisp/jupyter/jupyter-base.el
Normal file
@@ -0,0 +1,793 @@
|
||||
;;; jupyter-base.el --- Core definitions for Jupyter -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 06 Jan 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; This file holds the core requires, variables, and type definitions necessary
|
||||
;; for jupyter.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(eval-when-compile (require 'subr-x))
|
||||
(require 'cl-lib)
|
||||
(require 'eieio)
|
||||
(require 'eieio-base)
|
||||
(require 'json)
|
||||
|
||||
(declare-function tramp-dissect-file-name "tramp" (name &optional nodefault))
|
||||
(declare-function tramp-file-name-user "tramp")
|
||||
(declare-function tramp-file-name-host "tramp")
|
||||
(declare-function jupyter-message-content "jupyter-messages" (msg))
|
||||
(declare-function jupyter-new-uuid "jupyter-messages")
|
||||
|
||||
(cl-deftype json-plist () '(satisfies json-plist-p))
|
||||
|
||||
;;; Custom variables
|
||||
|
||||
(defcustom jupyter-pop-up-frame nil
|
||||
"Whether or not buffers should be displayed in a new frame by default.
|
||||
Note, this variable is only considered when evaluating code
|
||||
interactively with functions like `jupyter-eval-line-or-region'.
|
||||
|
||||
If equal to nil, frames will never be popped up. When equal to t,
|
||||
pop-up frames instead of windows.
|
||||
|
||||
`jupyter-pop-up-frame' can also be a list of message type
|
||||
keywords for messages which will cause frames to be used. For any
|
||||
message type not in the list, windows will be used instead.
|
||||
Currently only `execute_result', `error', and `stream'
|
||||
messages consider this variable."
|
||||
:group 'jupyter
|
||||
:type '(choice (const :tag "Pop up frames" t)
|
||||
(const :tag "Pop up windows" nil)
|
||||
;; TODO: These are the only ones where `jupyter-pop-up-frame'
|
||||
;; is checked at the moment.
|
||||
(set (const "execute_result")
|
||||
(const "error")
|
||||
(const "stream"))))
|
||||
|
||||
(defcustom jupyter-use-zmq (and (locate-library "zmq") t)
|
||||
"Whether or not ZMQ can be used to communicate with kernels.
|
||||
If ZMQ is not available for use, kernels can only be launched
|
||||
from a backing notebook server."
|
||||
:group 'jupyter
|
||||
:type 'boolean)
|
||||
|
||||
(defconst jupyter-root (file-name-directory load-file-name)
|
||||
"Root directory containing emacs-jupyter.")
|
||||
|
||||
(defconst jupyter-protocol-version "5.3"
|
||||
"The jupyter protocol version that is implemented.")
|
||||
|
||||
(defconst jupyter-message-types
|
||||
(list "execute_result"
|
||||
"execute_request"
|
||||
"execute_reply"
|
||||
"inspect_request"
|
||||
"inspect_reply"
|
||||
"complete_request"
|
||||
"complete_reply"
|
||||
"history_request"
|
||||
"history_reply"
|
||||
"is_complete_request"
|
||||
"is_complete_reply"
|
||||
"comm_info_request"
|
||||
"comm_info_reply"
|
||||
"comm_open"
|
||||
"comm_msg"
|
||||
"comm_close"
|
||||
"kernel_info_request"
|
||||
"kernel_info_reply"
|
||||
"shutdown_request"
|
||||
"shutdown_reply"
|
||||
"interrupt_request"
|
||||
"interrupt_reply"
|
||||
"stream"
|
||||
"display_data"
|
||||
"update_display_data"
|
||||
"execute_input"
|
||||
"error"
|
||||
"status"
|
||||
"clear_output"
|
||||
"input_reply"
|
||||
"input_request")
|
||||
"A list of valid Jupyter message types.")
|
||||
|
||||
(defconst jupyter-mime-types '(:application/vnd.jupyter.widget-view+json
|
||||
:text/html :text/markdown
|
||||
:image/svg+xml :image/jpeg :image/png
|
||||
:text/latex :text/plain)
|
||||
"MIME types handled by Jupyter.")
|
||||
|
||||
(defconst jupyter-nongraphic-mime-types '(:application/vnd.jupyter.widget-view+json
|
||||
:text/html :text/markdown
|
||||
:text/plain)
|
||||
"MIME types that can be used in terminal Emacs.")
|
||||
|
||||
(defvar jupyter--debug nil
|
||||
"When non-nil, some parts of Jupyter will emit debug statements.
|
||||
If the symbol \='message, messages received by a kernel will only
|
||||
be handled by clients when the function
|
||||
`jupyter--debug-replay-requests' is called manually. This allows
|
||||
for stepping through the code with Edebug.")
|
||||
|
||||
(defvar jupyter--debug-request-queue nil)
|
||||
|
||||
(defun jupyter-debug (format-string &rest args)
|
||||
"Display a message when `jupyter--debug' is non-nil.
|
||||
FORMAT-STRING and ARGS have the same meaning as in `message'."
|
||||
(when jupyter--debug
|
||||
(apply #'message (concat "Jupyter: " format-string) args)))
|
||||
|
||||
(defvar jupyter-default-timeout 2.5
|
||||
"The default timeout in seconds for `jupyter-wait-until'.")
|
||||
|
||||
(defvar jupyter-long-timeout 10
|
||||
"A longer timeout than `jupyter-default-timeout' used for some operations.
|
||||
A longer timeout is needed, for example, when retrieving the
|
||||
`jupyter-kernel-info' to allow for the kernel to startup.")
|
||||
|
||||
(defconst jupyter-version "1.0"
|
||||
"Current version of Jupyter.")
|
||||
|
||||
;;; Macros
|
||||
|
||||
(defmacro jupyter-with-timeout (spec &rest wait-forms)
|
||||
"Periodically evaluate WAIT-FORMS until timeout.
|
||||
Or until WAIT-FORMS evaluates to a non-nil value.
|
||||
|
||||
Wait until timeout SECONDS, periodically evaluating WAIT-FORMS
|
||||
until it returns non-nil. If WAIT-FORMS returns non-nil, stop
|
||||
waiting and return its value. Otherwise if timeout SECONDS
|
||||
elapses, evaluate TIMEOUT-FORMS and return its value.
|
||||
|
||||
If PROGRESS is non-nil and evaluates to a string, a progress
|
||||
reporter will be used with PROGRESS as the message while waiting.
|
||||
|
||||
SPEC takes the form (PROGRESS SECONDS TIMEOUT-FORMS...).
|
||||
|
||||
\(fn (PROGRESS SECONDS TIMEOUT-FORMS...) WAIT-FORMS...)"
|
||||
(declare (indent 1) (debug ((form form body) body)))
|
||||
(let ((res (make-symbol "res"))
|
||||
(prog (make-symbol "prog"))
|
||||
(prog-msg (make-symbol "prog-msg"))
|
||||
(timeout (make-symbol "timeout"))
|
||||
(wait-time (make-symbol "wait-time")))
|
||||
`(let* ((,res nil)
|
||||
(,prog-msg ,(pop spec))
|
||||
(,timeout ,(pop spec))
|
||||
(,wait-time (/ ,timeout 10.0))
|
||||
(,prog (and (stringp ,prog-msg)
|
||||
(make-progress-reporter ,prog-msg))))
|
||||
(with-timeout (,timeout ,@spec)
|
||||
(while (not (setq ,res (progn ,@wait-forms)))
|
||||
(accept-process-output nil ,wait-time)
|
||||
(when ,prog (progress-reporter-update ,prog))))
|
||||
(prog1 ,res
|
||||
(when ,prog (progress-reporter-done ,prog))))))
|
||||
|
||||
(defmacro jupyter-with-insertion-bounds (beg end bodyform &rest afterforms)
|
||||
"Bind BEG and END to `point-marker's, evaluate BODYFORM then AFTERFORMS.
|
||||
The END marker will advance if BODYFORM inserts text in the
|
||||
current buffer. Thus after BODYFORM is evaluated, AFTERFORMS will
|
||||
have access to the bounds of the text inserted by BODYFORM in the
|
||||
variables BEG and END. The result of evaluating BODYFORM is
|
||||
returned."
|
||||
(declare (indent 3) (debug (symbolp symbolp form body)))
|
||||
`(let ((,beg (point-marker))
|
||||
(,end (point-marker)))
|
||||
(set-marker-insertion-type ,end t)
|
||||
(unwind-protect
|
||||
(prog1 ,bodyform ,@afterforms)
|
||||
(set-marker ,beg nil)
|
||||
(set-marker ,end nil))))
|
||||
|
||||
(defun jupyter-map-mime-bundle (mime-types content fun)
|
||||
"For each mime-type in MIME-TYPES, call FUN with its data in CONTENT.
|
||||
If the result of evaluating FUN on the data of a mime-type is
|
||||
non-nil, return it. Otherwise, call FUN for the next mime-type.
|
||||
Return nil if FUN was evaluated on all mime-types without a
|
||||
non-nil result. FUN is only called on mime-types that have data
|
||||
in CONTENT.
|
||||
|
||||
CONTENT is a mime bundle, a property list containing a :data key
|
||||
and, optionally, a :metadata key that are themselves property
|
||||
lists with mime-type keywords as keys.
|
||||
|
||||
A call to FUN looks like this
|
||||
|
||||
\(funcall fun MIME-TYPE \='(:data D :metadata M))
|
||||
|
||||
where D will be the data associated with MIME-TYPE in CONTENT and
|
||||
M is any associated metadata."
|
||||
(declare (indent 2))
|
||||
(cl-destructuring-bind (&key data metadata &allow-other-keys)
|
||||
content
|
||||
(catch 'mime-type
|
||||
(mapc
|
||||
(lambda (mime-type)
|
||||
(let ((d (plist-get data mime-type))
|
||||
(m (plist-get metadata mime-type)))
|
||||
(if d
|
||||
(let ((r (funcall fun mime-type `(:data ,d :metadata ,m))))
|
||||
(if r (throw 'mime-type r))))))
|
||||
mime-types)
|
||||
nil)))
|
||||
|
||||
(defun jupyter-mime-value (content mime)
|
||||
"Extract a value from a mime bundle.
|
||||
CONTENT has the same meaning as in `jupyter-map-mime-bundle'.
|
||||
Return the value of MIME in CONTENT. If MIME is not in CONTENT,
|
||||
return nil."
|
||||
(jupyter-map-mime-bundle (list mime)
|
||||
content
|
||||
(lambda (_mime content)
|
||||
(plist-get content :data))))
|
||||
|
||||
|
||||
;;;; Display buffers
|
||||
|
||||
(defvar-local jupyter-display-buffer-marker nil
|
||||
"The marker to store the last output position of an output buffer.
|
||||
See `jupyter-with-display-buffer'.")
|
||||
|
||||
(defvar-local jupyter-display-buffer-request-id nil
|
||||
"The last `jupyter-request' message ID that generated output.")
|
||||
|
||||
(defun jupyter-get-buffer-create (name)
|
||||
"Return a buffer with some special properties.
|
||||
|
||||
- The buffer's name is based on NAME, specifically it will be
|
||||
\"*jupyter-NAME*\"
|
||||
|
||||
- Its `major-mode' will be `special-mode'."
|
||||
(let* ((bname (format "*jupyter-%s*" name))
|
||||
(buffer (get-buffer bname)))
|
||||
(unless buffer
|
||||
(setq buffer (get-buffer-create bname))
|
||||
(with-current-buffer buffer
|
||||
;; For buffers such as the jupyter REPL, showing trailing whitespaces
|
||||
;; may be a nuisance (as evidenced by the Python banner).
|
||||
(setq-local show-trailing-whitespace nil)
|
||||
(unless (eq major-mode 'special-mode)
|
||||
(special-mode))))
|
||||
buffer))
|
||||
|
||||
(defun jupyter--reset-display-buffer-p (arg)
|
||||
"Return non-nil if the current output buffer should be reset.
|
||||
If ARG is a `jupyter-request', reset the buffer if ARG's
|
||||
`jupyter-request-id' is no equal to the
|
||||
`jupyter-buffer-last-request-id'. If ARG is not a
|
||||
`jupyter-request-id', return ARG."
|
||||
(if (jupyter-request-p arg)
|
||||
;; Reset the output buffer is the last request ID does not
|
||||
;; match the current request's ID.
|
||||
(let ((id (jupyter-request-id arg)))
|
||||
(and (not (equal id jupyter-display-buffer-request-id))
|
||||
(setq jupyter-display-buffer-request-id id)
|
||||
t))
|
||||
;; Otherwise reset the output buffer if RESET evaluates to a
|
||||
;; non-nil value
|
||||
arg))
|
||||
|
||||
(defmacro jupyter-with-display-buffer (name reset &rest body)
|
||||
"In a buffer with a name derived from NAME current, evaluate BODY.
|
||||
The buffer's name is obtained by a call to
|
||||
`jupyter-get-buffer-create'.
|
||||
|
||||
A display buffer is similar to a *Help* buffer, but maintains its
|
||||
previous output on subsequent invocations that use the same NAME
|
||||
and BODY is wrapped using `jupyter-with-control-code-handling' so
|
||||
that any insertions into the buffer that contain ANSI escape
|
||||
codes are properly handled.
|
||||
|
||||
Note, before BODY is evaluated, `point' is moved to the end of
|
||||
the most recent output.
|
||||
|
||||
Also note, the `jupyter-current-client' variable in the buffer
|
||||
that BODY is evaluated in is let bound to whatever value it has
|
||||
before making that buffer current.
|
||||
|
||||
RESET is a form or symbol that determines if the buffer should be
|
||||
erased before evaluating BODY. If RESET is nil, no erasing of the
|
||||
buffer is ever performed. If RESET evaluates to a
|
||||
`jupyter-request' object, reset the buffer if the previous
|
||||
request that generated output in the buffer is not the same
|
||||
request. Otherwise if RESET evaluates to any non-nil value, reset
|
||||
the output buffer."
|
||||
(declare (indent 2) (debug (stringp [&or atom form] body)))
|
||||
(let ((buffer (make-symbol "buffer"))
|
||||
(client (make-symbol "client")))
|
||||
`(let ((,client jupyter-current-client)
|
||||
(,buffer (jupyter-get-buffer-create ,name)))
|
||||
(setq other-window-scroll-buffer ,buffer)
|
||||
(with-current-buffer ,buffer
|
||||
(unless jupyter-display-buffer-marker
|
||||
(setq jupyter-display-buffer-marker (point-max-marker))
|
||||
(set-marker-insertion-type jupyter-display-buffer-marker t))
|
||||
(let ((inhibit-read-only t)
|
||||
(jupyter-current-client ,client))
|
||||
(when (jupyter--reset-display-buffer-p ,reset)
|
||||
(erase-buffer)
|
||||
(set-marker jupyter-display-buffer-marker (point))
|
||||
(setq ansi-color-context-region nil))
|
||||
(goto-char jupyter-display-buffer-marker)
|
||||
(jupyter-with-control-code-handling ,@body))))))
|
||||
|
||||
(defun jupyter-display-current-buffer-reuse-window (&optional msg-type alist &rest actions)
|
||||
"Convenience function to call `display-buffer' on the `current-buffer'.
|
||||
If a window showing the current buffer is already available,
|
||||
re-use it.
|
||||
|
||||
If ALIST is non-nil it is used as the ACTION alist of
|
||||
`display-buffer'.
|
||||
|
||||
If MSG-TYPE is specified, it should be one of the keywords in
|
||||
`jupyter-message-types' and is used in setting `pop-up-frames'
|
||||
and `pop-up-windows'. See `jupyter-pop-up-frame'.
|
||||
|
||||
The rest of the arguments are display ACTIONS tried after
|
||||
attempting to re-use a window and before attempting to pop-up a
|
||||
new window or frame."
|
||||
(let* ((jupyter-pop-up-frame (jupyter-pop-up-frame-p msg-type))
|
||||
(pop-up-frames (and jupyter-pop-up-frame 'graphic-only))
|
||||
(pop-up-windows (not jupyter-pop-up-frame))
|
||||
(display-buffer-base-action
|
||||
(cons
|
||||
(append '(display-buffer-reuse-window)
|
||||
(delq nil actions))
|
||||
alist)))
|
||||
(display-buffer (current-buffer))))
|
||||
|
||||
(defun jupyter-pop-up-frame-p (msg-type)
|
||||
"Return non-nil if a frame should be popped up for MSG-TYPE."
|
||||
(or (eq jupyter-pop-up-frame t)
|
||||
(member msg-type jupyter-pop-up-frame)))
|
||||
|
||||
(defun jupyter-display-current-buffer-guess-where (msg-type)
|
||||
"Display the current buffer in a window or frame depending on MSG-TYPE.
|
||||
Call `jupyter-display-current-buffer-reuse-window' passing
|
||||
MSG-TYPE as argument. If MSG-TYPE should be displayed in a window
|
||||
and the current buffer is not already being displayed, display
|
||||
the buffer below the selected window."
|
||||
(jupyter-display-current-buffer-reuse-window
|
||||
msg-type nil (unless (jupyter-pop-up-frame-p msg-type)
|
||||
#'display-buffer-below-selected)))
|
||||
|
||||
;;; Some useful classes
|
||||
|
||||
(defclass jupyter-instance-tracker ()
|
||||
((tracking-symbol :type symbol))
|
||||
:documentation "Similar to `eieio-instance-tracker', but keeping weak references.
|
||||
To access all the objects in TRACKING-SYMBOL, use
|
||||
`jupyter-all-objects'."
|
||||
:abstract t)
|
||||
|
||||
(cl-defmethod initialize-instance ((obj jupyter-instance-tracker) &optional _slots)
|
||||
(cl-call-next-method)
|
||||
(let ((sym (oref obj tracking-symbol)))
|
||||
(unless (hash-table-p (symbol-value sym))
|
||||
(put sym 'jupyter-instance-tracker t)
|
||||
(set sym (make-hash-table :weakness 'key)))
|
||||
(puthash obj t (symbol-value sym))))
|
||||
|
||||
(defun jupyter-all-objects (sym)
|
||||
"Return all tracked objects in tracking SYM.
|
||||
SYM is a symbol used for tracking objects that inherit from the
|
||||
class corresponding to the symbol `jupyter-instance-tracker'."
|
||||
(let ((table (symbol-value sym)))
|
||||
(when (hash-table-p table)
|
||||
(cl-assert (get sym 'jupyter-instance-tracker) t)
|
||||
(hash-table-keys table))))
|
||||
|
||||
(defclass jupyter-finalized-object ()
|
||||
((finalizers :type list :initform nil))
|
||||
:documentation "A list of finalizers."
|
||||
:documentation "A base class for cleaning up resources.
|
||||
Adds the method `jupyter-add-finalizer' which maintains a list of
|
||||
finalizer functions to be called when the object is garbage
|
||||
collected.")
|
||||
|
||||
(cl-defmethod jupyter-add-finalizer ((obj jupyter-finalized-object) finalizer)
|
||||
"Cleanup resources automatically.
|
||||
FINALIZER if a function to be added to a list of finalizers that
|
||||
will be called when OBJ is garbage collected."
|
||||
(cl-check-type finalizer function)
|
||||
(push (make-finalizer finalizer) (oref obj finalizers)))
|
||||
|
||||
;;; Session object definition
|
||||
|
||||
(cl-defstruct (jupyter-session
|
||||
(:constructor nil)
|
||||
(:constructor
|
||||
jupyter-session
|
||||
(&key
|
||||
(conn-info nil)
|
||||
(id (jupyter-new-uuid))
|
||||
(key nil))))
|
||||
"A `jupyter-session' holds the information needed to
|
||||
authenticate messages. A `jupyter-session' contains the following
|
||||
fields:
|
||||
|
||||
- CONN-INFO :: The connection info. property list of the kernel
|
||||
this session is used to sign messages for.
|
||||
|
||||
- ID :: A string of bytes that uniquely identifies this session.
|
||||
|
||||
- KEY :: The key used when signing messages. If KEY is nil,
|
||||
message signing is not performed."
|
||||
(conn-info nil :read-only t)
|
||||
(id nil :read-only t)
|
||||
(key nil :read-only t))
|
||||
|
||||
(cl-defmethod jupyter-session-endpoints ((session jupyter-session))
|
||||
"Return a property list containing the endpoints from SESSION."
|
||||
(cl-destructuring-bind
|
||||
(&key shell_port iopub_port stdin_port hb_port control_port
|
||||
ip transport
|
||||
&allow-other-keys)
|
||||
(jupyter-session-conn-info session)
|
||||
(cl-assert (and transport ip))
|
||||
(let ((addr (lambda (port) (format "%s://%s:%d" transport ip port))))
|
||||
(cl-loop
|
||||
for (channel . port) in `((:hb . ,hb_port)
|
||||
(:stdin . ,stdin_port)
|
||||
(:shell . ,shell_port)
|
||||
(:iopub . ,iopub_port)
|
||||
(:control . ,control_port))
|
||||
do (cl-assert port) and
|
||||
collect channel and collect (funcall addr port)))))
|
||||
|
||||
;;; Request object definition
|
||||
|
||||
(cl-defstruct jupyter-request
|
||||
"Represents a request made to a kernel.
|
||||
Requests sent by a client always return something that can be
|
||||
interpreted as a `jupyter-request'. It holds the state of a
|
||||
request as the kernel and client communicate messages between
|
||||
each other. A client has a request table to keep track of all
|
||||
requests that are not considered idle. The most recent idle
|
||||
request is also kept track of.
|
||||
|
||||
Each request contains: a message ID, a request type, a request
|
||||
message, a time sent, a last message received by the client that
|
||||
sent it, and a list of message types that tell the client to not
|
||||
call the handler methods of those types."
|
||||
(id (jupyter-new-uuid) :read-only t)
|
||||
(type nil :read-only t)
|
||||
(content nil :read-only t)
|
||||
(client nil :read-only nil)
|
||||
(time (current-time) :read-only t)
|
||||
(idle-p nil)
|
||||
(last-message nil)
|
||||
(messages nil)
|
||||
(message-publisher nil)
|
||||
(inhibited-handlers nil))
|
||||
|
||||
(defun jupyter-channel-from-request-type (type)
|
||||
"Return the name of the channel that a request with TYPE is sent on."
|
||||
(pcase type
|
||||
((or "input_reply" "input_request") "stdin")
|
||||
("interrupt_request" "control")
|
||||
(_ "shell")))
|
||||
|
||||
;;; Connecting to a kernel's channels
|
||||
|
||||
(eval-when-compile (require 'tramp))
|
||||
|
||||
(defun jupyter-available-local-ports (n)
|
||||
"Return a list of N ports available on the localhost."
|
||||
(let (servers)
|
||||
(unwind-protect
|
||||
(cl-loop
|
||||
repeat n
|
||||
do (push (make-network-process
|
||||
:name "jupyter-available-local-ports"
|
||||
:server t
|
||||
:host "127.0.0.1"
|
||||
:service t)
|
||||
servers)
|
||||
finally return (mapcar (lambda (p) (cadr (process-contact p))) servers))
|
||||
(mapc #'delete-process servers))))
|
||||
|
||||
(defun jupyter-make-ssh-tunnel (lport rport server remoteip)
|
||||
(or remoteip (setq remoteip "127.0.0.1"))
|
||||
(start-process
|
||||
"jupyter-ssh-tunnel" nil
|
||||
"ssh"
|
||||
;; Run in background
|
||||
"-f"
|
||||
;; Wait until the tunnel is open
|
||||
"-o ExitOnForwardFailure=yes"
|
||||
;; Local forward
|
||||
"-L" (format "127.0.0.1:%d:%s:%d" lport remoteip rport)
|
||||
server
|
||||
;; Close the tunnel if no other connections are made within 60
|
||||
;; seconds
|
||||
"sleep 60"))
|
||||
|
||||
(defun jupyter-read-connection (conn-file)
|
||||
"Return the connection information in CONN-FILE.
|
||||
Return a property list representation of the JSON in CONN-FILE, a
|
||||
Jupyter connection file."
|
||||
(let ((conn-info (jupyter-read-plist conn-file)))
|
||||
;; Also validate the signature scheme here.
|
||||
(cl-destructuring-bind (&key key signature_scheme &allow-other-keys)
|
||||
conn-info
|
||||
(when (and (> (length key) 0)
|
||||
(not (functionp
|
||||
(intern (concat "jupyter-" signature_scheme)))))
|
||||
(error "Unsupported signature scheme: %s" signature_scheme)))
|
||||
conn-info))
|
||||
|
||||
(defun jupyter-tunnel-connection (conn-file &optional server)
|
||||
"Forward local ports to the remote ports in CONN-FILE.
|
||||
CONN-FILE is the path to a Jupyter connection file, SERVER is the
|
||||
host that the kernel connection in CONN-FILE is located. Return a
|
||||
copy of the connection plist in CONN-FILE, but with the ports
|
||||
replaced by the local ports used for the forwarding.
|
||||
|
||||
If CONN-FILE is a `tramp' file name, the SERVER argument will be
|
||||
ignored and the host will be extracted from the information
|
||||
contained in the file name.
|
||||
|
||||
Note only SSH tunnels are currently supported."
|
||||
(catch 'no-tunnels
|
||||
(let ((conn-info (jupyter-read-connection conn-file)))
|
||||
(when (and (file-remote-p conn-file)
|
||||
(functionp 'tramp-dissect-file-name))
|
||||
(pcase-let (((cl-struct tramp-file-name method user host)
|
||||
(tramp-dissect-file-name conn-file)))
|
||||
(pcase method
|
||||
;; TODO: Document this in the README along with the fact that
|
||||
;; connection files can use /ssh: TRAMP files.
|
||||
("docker"
|
||||
;; Assume docker is using the -p argument to publish its exposed
|
||||
;; ports to the localhost. The ports used in the container should
|
||||
;; be the same ports accessible on the local host. For example, if
|
||||
;; the shell port is on 1234 in the container, the published port
|
||||
;; flag should be "-p 1234:1234".
|
||||
(throw 'no-tunnels conn-info))
|
||||
(_
|
||||
(setq server (if user (concat user "@" host)
|
||||
host))))))
|
||||
(let* ((keys '(:hb_port :shell_port :control_port
|
||||
:stdin_port :iopub_port))
|
||||
(lports (jupyter-available-local-ports (length keys))))
|
||||
(cl-loop
|
||||
with remoteip = (plist-get conn-info :ip)
|
||||
for (key maybe-rport) on conn-info by #'cddr
|
||||
collect key and if (memq key keys)
|
||||
collect (let ((lport (pop lports)))
|
||||
(prog1 lport
|
||||
(jupyter-make-ssh-tunnel lport maybe-rport server remoteip)))
|
||||
else collect maybe-rport)))))
|
||||
|
||||
(defun jupyter-connection-file-to-session (conn-file)
|
||||
"Return a `jupyter-session' based on CONN-FILE.
|
||||
CONN-FILE is a Jupyter connection file. If CONN-FILE is a remote
|
||||
file, open local SSH tunnels to the remote ports listed in
|
||||
CONN-FILE. The returned session object will have the remote
|
||||
ports remapped to the local ports."
|
||||
(let ((conn-info (if (file-remote-p conn-file)
|
||||
(jupyter-tunnel-connection conn-file)
|
||||
(jupyter-read-connection conn-file))))
|
||||
(jupyter-session
|
||||
:conn-info conn-info
|
||||
:key (plist-get conn-info :key))))
|
||||
|
||||
;;; Kernel I/O
|
||||
|
||||
(defvar jupyter-io-cache (make-hash-table :weakness 'key))
|
||||
|
||||
(cl-defgeneric jupyter-io (thing)
|
||||
"Return the I/O object of THING.")
|
||||
|
||||
(cl-defmethod jupyter-io :around (thing)
|
||||
"Cache the I/O object of THING in `jupyter-io-cache'."
|
||||
(or (gethash thing jupyter-io-cache)
|
||||
(puthash thing (cl-call-next-method) jupyter-io-cache)))
|
||||
|
||||
;;; Helper functions
|
||||
|
||||
(defun jupyter-canonicalize-language-string (str)
|
||||
"Return STR with \" \" converted to \"-\".
|
||||
The `file-name-nondirectory' of STR will be converted and
|
||||
returned if it looks like a file path."
|
||||
;; The call to `file-name-nondirectory' is here to be more robust when
|
||||
;; running on systems like Guix or Nix. Some builders on those kinds of
|
||||
;; systems will indiscriminately replace "python" with something like
|
||||
;; "/gnu/store/.../bin/python" when building the kernelspecs.
|
||||
(replace-regexp-in-string " " "-" (file-name-nondirectory str)))
|
||||
|
||||
(defvar server-buffer)
|
||||
(defvar jupyter-current-client)
|
||||
(defvar jupyter-server-mode-client-timer nil
|
||||
"Timer used to unset `jupyter-current-client' from `server-buffer'.")
|
||||
|
||||
;; FIXME: This works if we only consider a single send request that will also
|
||||
;; finish within TIMEOUT which is probably 99% of the cases. It doesn't work
|
||||
;; for multiple requests that have been sent using different clients where one
|
||||
;; sets the client in `server-buffer' and, before a file is opened by the
|
||||
;; underlying kernel, another sets the client in `server-buffer'.
|
||||
|
||||
(defun jupyter-server-mode-set-client (client &optional timeout)
|
||||
"Set CLIENT as the `jupyter-current-client' in the `server-buffer'.
|
||||
Kill `jupyter-current-client's local value in `server-buffer'
|
||||
after TIMEOUT seconds, defaulting to `jupyter-long-timeout'.
|
||||
|
||||
If a function causes a buffer to be displayed through
|
||||
emacsclient, e.g. when a function calls an external command that
|
||||
invokes the EDITOR, we don't know when the buffer will be
|
||||
displayed. All we know is that the buffer that will be current
|
||||
before display will be the `server-buffer'. So we temporarily set
|
||||
`jupyter-current-client' in `server-buffer' so that the client
|
||||
gets a chance to be propagated to the displayed buffer, see
|
||||
`jupyter-repl-persistent-mode'.
|
||||
|
||||
For this to work properly you should have something like the
|
||||
following in your Emacs configuration
|
||||
|
||||
(server-mode 1)
|
||||
(setenv \"EDITOR\" \"emacsclient\")
|
||||
|
||||
before starting any Jupyter kernels. The kernel also has to know
|
||||
that it should use EDITOR to open files."
|
||||
(when (bound-and-true-p server-mode)
|
||||
;; After switching to a server buffer, keep the client alive in `server-buffer'
|
||||
;; to account for multiple files being opened by the server.
|
||||
(unless (and (boundp 'server-switch-hook)
|
||||
(memq #'jupyter-server-mode--unset-client-soon
|
||||
server-switch-hook))
|
||||
(add-hook 'server-switch-hook #'jupyter-server-mode--unset-client-soon))
|
||||
(with-current-buffer (get-buffer-create server-buffer)
|
||||
(setq jupyter-current-client client)
|
||||
(jupyter-server-mode--unset-client-soon timeout))))
|
||||
|
||||
(defun jupyter-server-mode-unset-client ()
|
||||
"Set `jupyter-current-client' to nil in `server-buffer'."
|
||||
(when (and (bound-and-true-p server-mode)
|
||||
(get-buffer server-buffer))
|
||||
(with-current-buffer server-buffer
|
||||
(setq jupyter-current-client nil))))
|
||||
|
||||
(defun jupyter-server-mode--unset-client-soon (&optional timeout)
|
||||
(when (timerp jupyter-server-mode-client-timer)
|
||||
(cancel-timer jupyter-server-mode-client-timer))
|
||||
(setq jupyter-server-mode-client-timer
|
||||
(run-at-time (or timeout jupyter-long-timeout)
|
||||
nil #'jupyter-server-mode-unset-client)))
|
||||
|
||||
(defun jupyter-read-plist (file)
|
||||
"Read a JSON encoded FILE as a property list."
|
||||
(let ((json-object-type 'plist))
|
||||
(json-read-file file)))
|
||||
|
||||
(defun jupyter-read-plist-from-string (string)
|
||||
"Read a property list from a JSON encoded STRING."
|
||||
(let ((json-object-type 'plist))
|
||||
(json-read-from-string string)))
|
||||
|
||||
(defun jupyter-normalize-data (plist &optional metadata)
|
||||
"Return a property list (:data DATA :metadata META) from PLIST.
|
||||
DATA is a property list of mimetype data extracted from PLIST.
|
||||
If PLIST is a message plist, DATA will be the value of the :data
|
||||
key in the `jupyter-message-content'. Otherwise, DATA is either
|
||||
the :data key of PLIST or PLIST itself.
|
||||
|
||||
A similar extraction process is performed for the :metadata key
|
||||
of PLIST which will be the META argument in the return value. If
|
||||
no :metadata key can be found, then META will be METADATA."
|
||||
(list :data (or
|
||||
;; Allow for passing message plists
|
||||
(plist-get (jupyter-message-content plist) :data)
|
||||
;; Allow for passing (jupyter-message-content msg)
|
||||
(plist-get plist :data)
|
||||
;; Otherwise assume the plist contains mimetypes
|
||||
plist)
|
||||
:metadata (or (plist-get (jupyter-message-content plist) :metadata)
|
||||
(plist-get plist :metadata)
|
||||
metadata)))
|
||||
|
||||
(defun jupyter-line-count-greater-p (str n)
|
||||
"Return non-nil if STR has more than N lines."
|
||||
(string-match-p
|
||||
(format "^\\(?:[^\n]*\n\\)\\{%d,\\}" (1+ n))
|
||||
str))
|
||||
|
||||
(defun jupyter-format-time-low-res (time)
|
||||
"Return a description string describing TIME.
|
||||
If TIME is nil return \"Never\", otherwise return strings like
|
||||
|
||||
\"1 day ago\", \"an hour ago\", \"in 10 minutes\", ...
|
||||
|
||||
depending on the relative value of TIME from the `current-time'.
|
||||
TIME is assumed to have the same form as the return value of
|
||||
`current-time'."
|
||||
(if (null time) "Never"
|
||||
(let* ((seconds (- (float-time time)
|
||||
(float-time (current-time))))
|
||||
(past (< seconds 0))
|
||||
(seconds (abs seconds))
|
||||
(minutes (floor (/ seconds 60.0)))
|
||||
(hours (floor (/ seconds 3600.0)))
|
||||
(days (floor (/ seconds 86400.0))))
|
||||
(cond
|
||||
((< seconds 60)
|
||||
(if (or past
|
||||
;; Account for discrepancies between time resolution
|
||||
(< seconds 0.1))
|
||||
"a few seconds ago"
|
||||
"in a few seconds"))
|
||||
((not (zerop days))
|
||||
(format "%s%d day%s%s"
|
||||
(if past "" "in ")
|
||||
days
|
||||
(if (= days 1) "" "s")
|
||||
(if past " ago" "")))
|
||||
((not (zerop hours))
|
||||
(if (= hours 1)
|
||||
(if past "an hour ago"
|
||||
"in one hour")
|
||||
(format "%s%d hours%s"
|
||||
(if past "" "in ")
|
||||
hours
|
||||
(if hours " ago" ""))))
|
||||
((not (zerop minutes))
|
||||
(if (= minutes 1)
|
||||
(if past "a minute ago"
|
||||
"in one minute")
|
||||
(format "%s%d minutes%s"
|
||||
(if past "" "in ")
|
||||
minutes
|
||||
(if past " ago" ""))))))))
|
||||
|
||||
;;; Simple weak references
|
||||
;; Thanks to Chris Wellon https://nullprogram.com/blog/2014/01/27/
|
||||
|
||||
(defun jupyter-weak-ref (object)
|
||||
"Return a weak reference for OBJECT."
|
||||
(let ((ref (make-hash-table :weakness 'value :size 1)))
|
||||
(prog1 ref
|
||||
(puthash t object ref))))
|
||||
|
||||
(defsubst jupyter-weak-ref-resolve (ref)
|
||||
"Resolve a weak REF.
|
||||
Return nil if the underlying object has been garbage collected,
|
||||
otherwise return the underlying object."
|
||||
(gethash t ref))
|
||||
|
||||
;;; Errors
|
||||
|
||||
(defun jupyter-error-if-not-client-class-p (class &optional check-class)
|
||||
"Signal a wrong-type-argument error if CLASS is not a client class.
|
||||
If CHECK-CLASS is provided check CLASS against it. CHECK-CLASS
|
||||
defaults to `jupyter-kernel-client'."
|
||||
(or check-class (setq check-class 'jupyter-kernel-client))
|
||||
(cl-assert (class-p check-class))
|
||||
(unless (child-of-class-p class check-class)
|
||||
(signal 'wrong-type-argument
|
||||
(list (list 'subclass check-class) class))))
|
||||
|
||||
(provide 'jupyter-base)
|
||||
|
||||
;;; jupyter-base.el ends here
|
||||
45
lisp/jupyter/jupyter-c++.el
Normal file
45
lisp/jupyter/jupyter-c++.el
Normal file
@@ -0,0 +1,45 @@
|
||||
;;; jupyter-c++.el --- Jupyter support for C++ -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 12 April 2019
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Support methods for integration with C++.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-repl)
|
||||
|
||||
(cl-defmethod jupyter-repl-initialize-fontification (&context (jupyter-lang c++))
|
||||
"Copy buffer local variables used for fontification to the REPL buffer."
|
||||
(cl-loop
|
||||
with c-vars = (jupyter-with-repl-lang-buffer
|
||||
(cl-loop
|
||||
for var-val in (buffer-local-variables)
|
||||
if (string-prefix-p "c-" (symbol-name (car var-val)))
|
||||
collect var-val))
|
||||
for (var . val) in c-vars
|
||||
do (set (make-local-variable var) val))
|
||||
(cl-call-next-method))
|
||||
|
||||
(provide 'jupyter-c++)
|
||||
|
||||
;;; jupyter-c++.el ends here
|
||||
187
lisp/jupyter/jupyter-channel-ioloop.el
Normal file
187
lisp/jupyter/jupyter-channel-ioloop.el
Normal file
@@ -0,0 +1,187 @@
|
||||
;;; jupyter-channel-ioloop.el --- Abstract class to communicate with a jupyter-channel in a subprocess -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 27 Jun 2019
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Define a `jupyter-ioloop' that can be sent events to start, stop, or send a
|
||||
;; message on a set of `jupyter-channel' objects. For example to start a
|
||||
;; `jupyter-channel' in the subprocess environment you would do something like
|
||||
;;
|
||||
;; (jupyter-send ioloop 'start-channel TYPE ENDPOINT)
|
||||
;;
|
||||
;; where TYPE and ENDPOINT have the same meaning as in `jupyter-channel'.
|
||||
;;
|
||||
;; Note by default, no channels are available in the subprocess environment.
|
||||
;; You initialize channels by setting the `jupyter-channel-ioloop-channels'
|
||||
;; variable in the subprocess environment, e.g. using
|
||||
;; `jupyter-ioloop-add-setup', before starting the `jupyter-ioloop'.
|
||||
;;
|
||||
;; When you call `jupyter-ioloop-start' a `jupyter-session' object needs to
|
||||
;; passed as the second argument with whatever object you would like to receive
|
||||
;; events as the third. The `jupyter-session-id' will be used as the value of
|
||||
;; the :identity key in the call to `jupyter-start' when starting a
|
||||
;; channel.
|
||||
;;
|
||||
;; Each event sent to the subprocess will send back a corresponding
|
||||
;; confirmation event, the three events that can be sent and their
|
||||
;; corresponding confirmation events are:
|
||||
;;
|
||||
;; (start-channel TYPE ENDPOINT) -> (start-channel TYPE)
|
||||
;; (stop-channel TYPE) -> (stop-channel TYPE)
|
||||
;; (send TYPE MSG-TYPE MSG MSG-ID) -> (sent MSG-ID)
|
||||
;;
|
||||
;; For the send event, the MSG-TYPE, MSG, and MSG-ID have the same meaning as
|
||||
;; the TYPE, MSG, and MSG-ID arguments of the `jupyter-send' method of a
|
||||
;; `jupyter-channel'.
|
||||
;;
|
||||
;; Ex.
|
||||
;;
|
||||
;; (let ((ioloop (jupyter-channel-ioloop))
|
||||
;; (session (jupyter-session :id ...)))
|
||||
;; (jupyter-start-ioloop ioloop session ...)
|
||||
;; ...
|
||||
;; (jupyter-send ioloop 'start-channel ...)
|
||||
;; ...)
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-ioloop)
|
||||
|
||||
(defvar jupyter-channel-ioloop-session nil
|
||||
"The `jupyter-session' used when initializing Jupyter channels.")
|
||||
|
||||
(defvar jupyter-channel-ioloop-channels nil
|
||||
"A list of synchronous channels in an ioloop controlling Jupyter channels.")
|
||||
|
||||
(jupyter-ioloop-add-arg-type jupyter-channel
|
||||
(lambda (arg)
|
||||
`(or (object-assoc ,arg :type jupyter-channel-ioloop-channels)
|
||||
(error "Channel not alive (%s)" ,arg))))
|
||||
|
||||
(defclass jupyter-channel-ioloop (jupyter-ioloop)
|
||||
()
|
||||
:abstract t)
|
||||
|
||||
(cl-defmethod initialize-instance ((ioloop jupyter-channel-ioloop) &optional _slots)
|
||||
(cl-call-next-method)
|
||||
(jupyter-ioloop-add-setup ioloop
|
||||
(require 'jupyter-channel-ioloop))
|
||||
(jupyter-channel-ioloop-add-start-channel-event ioloop)
|
||||
(jupyter-channel-ioloop-add-stop-channel-event ioloop)
|
||||
(jupyter-channel-ioloop-add-send-event ioloop)
|
||||
(jupyter-ioloop-add-teardown ioloop
|
||||
(mapc #'jupyter-stop jupyter-channel-ioloop-channels)))
|
||||
|
||||
(defun jupyter-channel-ioloop-set-session (ioloop session)
|
||||
"In the IOLOOP, set SESSION as the `jupyter-channel-ioloop-session'.
|
||||
Add a form to IOLOOP's setup that sets the variable
|
||||
`jupyter-channel-ioloop-session' to a `jupyter-session' based on
|
||||
SESSION's id and key. Remove any top level form in the setup that
|
||||
sets `jupyter-channel-ioloop-session' via `setq' before doing so."
|
||||
(cl-callf (lambda (setup)
|
||||
(cons `(setq jupyter-channel-ioloop-session
|
||||
(jupyter-session
|
||||
:id ,(jupyter-session-id session)
|
||||
:key ,(jupyter-session-key session)))
|
||||
(cl-remove-if
|
||||
(lambda (f) (and (eq (car f) 'setq)
|
||||
(eq (cadr f) 'jupyter-channel-ioloop-session)))
|
||||
setup)))
|
||||
(oref ioloop setup)))
|
||||
|
||||
;;; Channel events
|
||||
|
||||
(defun jupyter-channel-ioloop-add-start-channel-event (ioloop)
|
||||
"Add a start-channel event handler to IOLOOP.
|
||||
The event fires when the IOLOOP receives a list with the form
|
||||
|
||||
(start-channel CHANNEL-TYPE ENDPOINT)
|
||||
|
||||
and shall stop any existing channel with CHANNEL-TYPE and start a
|
||||
new channel with CHANNEL-TYPE connected to ENDPOINT. The
|
||||
underlying socket IDENTITY is derived from
|
||||
`jupyter-channel-ioloop-session' in the IOLOOP environment. The
|
||||
channel will be added to the variable
|
||||
`jupyter-channel-ioloop-channels' in the IOLOOP environment.
|
||||
|
||||
Note, before sending this event to IOLOOP, the corresponding
|
||||
channel needs to be available in the
|
||||
`jupyer-channel-ioloop-channels' variable. You can initialize
|
||||
this variable in the setup form of IOLOOP.
|
||||
|
||||
A list with the form
|
||||
|
||||
(start-channel CHANNEL-TYPE)
|
||||
|
||||
is returned to the parent process."
|
||||
(jupyter-ioloop-add-event
|
||||
ioloop start-channel ((channel jupyter-channel) endpoint)
|
||||
;; Stop the channel if it is already alive
|
||||
(when (jupyter-alive-p channel)
|
||||
(jupyter-stop channel))
|
||||
;; Start the channel
|
||||
(oset channel endpoint endpoint)
|
||||
(let ((identity (jupyter-session-id jupyter-channel-ioloop-session)))
|
||||
(jupyter-start channel :identity identity))
|
||||
(list 'start-channel (oref channel type))))
|
||||
|
||||
(defun jupyter-channel-ioloop-add-stop-channel-event (ioloop)
|
||||
"Add a stop-channel event handler to IOLOOP.
|
||||
The event fires when the IOLOOP receives a list with the form
|
||||
|
||||
(stop-channel CHANNEL-TYPE)
|
||||
|
||||
If a channel with CHANNEL-TYPE exists and is alive, it is stopped.
|
||||
|
||||
A list with the form
|
||||
|
||||
(stop-channel CHANNEL-TYPE)
|
||||
|
||||
is returned to the parent process."
|
||||
(jupyter-ioloop-add-event ioloop stop-channel (type)
|
||||
(let ((channel (object-assoc type :type jupyter-channel-ioloop-channels)))
|
||||
(when (and channel (jupyter-alive-p channel))
|
||||
(jupyter-stop channel))
|
||||
(list 'stop-channel type))))
|
||||
|
||||
(defun jupyter-channel-ioloop-add-send-event (ioloop)
|
||||
"Add a send event handler to IOLOOP.
|
||||
The event fires when the IOLOOP receives a list of the form
|
||||
|
||||
(send CHANNEL-TYPE MSG-TYPE MSG MSG-ID)
|
||||
|
||||
and calls (jupyter-send CHANNEL MSG-TYPE MSG MSG-ID) using the
|
||||
channel corresponding to CHANNEL-TYPE in the IOLOOP environment.
|
||||
|
||||
A list of the form
|
||||
|
||||
(sent CHANNEL-TYPE MSG-ID)
|
||||
|
||||
is returned to the parent process."
|
||||
(jupyter-ioloop-add-event
|
||||
ioloop send ((channel jupyter-channel) msg-type msg msg-id)
|
||||
(list 'sent (oref channel type)
|
||||
(jupyter-send channel msg-type msg msg-id))))
|
||||
|
||||
(provide 'jupyter-channel-ioloop)
|
||||
|
||||
;;; jupyter-channel-ioloop.el ends here
|
||||
73
lisp/jupyter/jupyter-channel.el
Normal file
73
lisp/jupyter/jupyter-channel.el
Normal file
@@ -0,0 +1,73 @@
|
||||
;;; jupyter-channel.el --- Jupyter channel interface -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 27 Jun 2019
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Defines the `jupyter-channel' interface.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'eieio)
|
||||
|
||||
(defclass jupyter-channel ()
|
||||
((type
|
||||
:type keyword
|
||||
:initarg :type
|
||||
:documentation "The type of this channel.")
|
||||
(session
|
||||
:type jupyter-session
|
||||
:initarg :session
|
||||
:documentation "The session object used to sign and send/receive messages.")
|
||||
(endpoint
|
||||
:type string
|
||||
:initarg :endpoint
|
||||
:documentation "The endpoint this channel is connected to.
|
||||
Typical endpoints look like \"tcp://127.0.0.1:5555\"."))
|
||||
:abstract t)
|
||||
|
||||
(cl-defmethod jupyter-start ((_channel jupyter-channel) &key _identity)
|
||||
"Start a Jupyter CHANNEL using IDENTITY as the routing ID.
|
||||
If CHANNEL is already alive, do nothing."
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-stop ((_channel jupyter-channel))
|
||||
"Stop a Jupyter CHANNEL.
|
||||
If CHANNEL is already stopped, do nothing."
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-alive-p ((_channel jupyter-channel))
|
||||
"Return non-nil if a CHANNEL is alive."
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-send (_channel _type _message &optional _msg-id)
|
||||
"On CHANNEL send MESSAGE which has message TYPE and optionally a MSG-ID."
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-recv (_channel &optional _dont-wait)
|
||||
"Receive a message on CHANNEL.
|
||||
If DONT-WAIT is non-nil, return nil immediately if there is no
|
||||
message available to receive."
|
||||
(cl-call-next-method))
|
||||
|
||||
(provide 'jupyter-channel)
|
||||
|
||||
;;; jupyter-channel.el ends here
|
||||
1812
lisp/jupyter/jupyter-client.el
Normal file
1812
lisp/jupyter/jupyter-client.el
Normal file
File diff suppressed because it is too large
Load Diff
192
lisp/jupyter/jupyter-env.el
Normal file
192
lisp/jupyter/jupyter-env.el
Normal file
@@ -0,0 +1,192 @@
|
||||
;;; jupyter-env.el --- Query the jupyter shell command for information -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 27 Jun 2019
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Custom variables and functions related to calling the jupyter shell command
|
||||
;; and its sub-commands for information.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-base)
|
||||
(eval-when-compile (require 'subr-x))
|
||||
|
||||
(defvar jupyter-runtime-directory nil
|
||||
"The Jupyter runtime directory.
|
||||
When a new kernel is started through `jupyter-start-kernel', this
|
||||
directory is where kernel connection files are written to.
|
||||
|
||||
This variable should not be used. To obtain the runtime directory
|
||||
call the function `jupyter-runtime-directory'.")
|
||||
|
||||
(defcustom jupyter-executable "jupyter"
|
||||
"The `jupyter` command executable."
|
||||
:type 'string
|
||||
:group 'jupyter)
|
||||
|
||||
(defun jupyter-command (&rest args)
|
||||
"Run a Jupyter shell command synchronously, return its output.
|
||||
The shell command run is
|
||||
|
||||
jupyter ARGS...
|
||||
|
||||
If the command fails or the jupyter shell command doesn't exist,
|
||||
return nil."
|
||||
(let ((stderr-file (make-temp-file "jupyter"))
|
||||
(stdout (get-buffer-create " *jupyter-command-stdout*")))
|
||||
(unwind-protect
|
||||
(let* ((status (apply #'process-file
|
||||
jupyter-executable
|
||||
nil
|
||||
(list stdout stderr-file)
|
||||
nil
|
||||
args))
|
||||
(buffer (find-file-noselect stderr-file)))
|
||||
(unwind-protect
|
||||
(with-current-buffer buffer
|
||||
(unless (eq (point-min) (point-max))
|
||||
(message "jupyter-command: Content written to stderr stream")
|
||||
(while (not (eq (point) (point-max)))
|
||||
(message " %s" (buffer-substring (line-beginning-position)
|
||||
(line-end-position)))
|
||||
(forward-line))))
|
||||
(kill-buffer buffer))
|
||||
(when (zerop status)
|
||||
(with-current-buffer stdout
|
||||
(string-trim-right (buffer-string)))))
|
||||
(delete-file stderr-file)
|
||||
(kill-buffer stdout))))
|
||||
|
||||
(defun jupyter-runtime-directory ()
|
||||
"Return the runtime directory used by Jupyter.
|
||||
Create the directory if necessary. If `default-directory' is a
|
||||
remote directory, return the runtime directory on that remote.
|
||||
|
||||
As a side effect, the variable `jupyter-runtime-directory' is set
|
||||
to the local runtime directory if it is nil."
|
||||
(unless jupyter-runtime-directory
|
||||
(setq jupyter-runtime-directory
|
||||
(let ((default-directory (expand-file-name "~" user-emacs-directory)))
|
||||
(file-name-as-directory (jupyter-command "--runtime-dir")))))
|
||||
(let ((dir (if (file-remote-p default-directory)
|
||||
(jupyter-command "--runtime-dir")
|
||||
jupyter-runtime-directory)))
|
||||
(unless dir
|
||||
(error "Can't obtain runtime directory from jupyter shell command"))
|
||||
(setq dir (concat (file-remote-p default-directory) dir))
|
||||
(make-directory dir 'parents)
|
||||
(file-name-as-directory dir)))
|
||||
|
||||
(defun jupyter-locate-python ()
|
||||
"Return the path to the python executable in use by Jupyter.
|
||||
If the `default-directory' is a remote directory, search on that
|
||||
remote. Raise an error if the executable could not be found.
|
||||
|
||||
The paths examined are the data paths of \"jupyter --paths\" in
|
||||
the order specified.
|
||||
|
||||
This function always returns the `file-local-name' of the path."
|
||||
(let* ((remote (file-remote-p default-directory))
|
||||
(paths (mapcar (lambda (x) (concat remote x))
|
||||
(or (plist-get
|
||||
(jupyter-read-plist-from-string
|
||||
(jupyter-command "--paths" "--json"))
|
||||
:data)
|
||||
(error "Can't get search paths"))))
|
||||
(path nil))
|
||||
(cl-loop
|
||||
with programs = '("bin/python3" "bin/python"
|
||||
;; Need to also check Windows since paths can be
|
||||
;; pointing to local or remote files.
|
||||
"python3.exe" "python.exe")
|
||||
with pred = (lambda (dir)
|
||||
(cl-loop
|
||||
for program in programs
|
||||
for spath = (expand-file-name program dir)
|
||||
thereis (setq path (and (file-exists-p spath) spath))))
|
||||
for path in paths
|
||||
thereis (locate-dominating-file path pred)
|
||||
finally (error "No `python' found in search paths"))
|
||||
(file-local-name path)))
|
||||
|
||||
(defun jupyter-write-connection-file (session)
|
||||
"Write a connection file based on SESSION to `jupyter-runtime-directory'.
|
||||
Return the path to the connection file."
|
||||
(cl-check-type session jupyter-session)
|
||||
(let* ((temporary-file-directory (jupyter-runtime-directory))
|
||||
(json-encoding-pretty-print t)
|
||||
(file (make-temp-file "emacs-kernel-" nil ".json")))
|
||||
(prog1 file
|
||||
(with-temp-file file
|
||||
(insert (json-encode-plist
|
||||
(jupyter-session-conn-info session)))))))
|
||||
|
||||
(defun jupyter-session-with-random-ports ()
|
||||
"Return a `jupyter-session' with random channel ports.
|
||||
The session can be used to write a connection file, see
|
||||
`jupyter-write-connection-file'."
|
||||
;; The actual work of making the connection file is left up to the
|
||||
;; `jupyter kernel` shell command. This is done to support
|
||||
;; launching remote kernels via TRAMP. The Jupyter suite of shell
|
||||
;; commands probably exist on the remote system, so we rely on them
|
||||
;; to figure out a set of open ports on the remote.
|
||||
(with-temp-buffer
|
||||
;; NOTE: On Windows, apparently the "jupyter kernel" command uses something
|
||||
;; like an exec shell command to start the process which launches the kernel,
|
||||
;; but exec like commands on Windows start a new process instead of replacing
|
||||
;; the current one which results in the process we start here exiting after
|
||||
;; the new process is launched. We call python directly to avoid this.
|
||||
(let ((process (start-file-process
|
||||
"jupyter-session-with-random-ports" (current-buffer)
|
||||
(jupyter-locate-python) "-c"
|
||||
"from jupyter_client.kernelapp import main; main()")))
|
||||
(set-process-query-on-exit-flag process nil)
|
||||
(jupyter-with-timeout
|
||||
(nil jupyter-long-timeout
|
||||
(error "`jupyter kernel` failed to show connection file path"))
|
||||
(and (process-live-p process)
|
||||
(goto-char (point-min))
|
||||
(re-search-forward "Connection file: \\(.+\\)\n" nil t)))
|
||||
(let* ((conn-file (concat
|
||||
(save-match-data
|
||||
(file-remote-p default-directory))
|
||||
(match-string 1)))
|
||||
(conn-info (jupyter-read-connection conn-file)))
|
||||
;; Tell the `jupyter kernel` process to shutdown itself and
|
||||
;; the launched kernel.
|
||||
(interrupt-process process)
|
||||
;; Wait until the connection file is cleaned up before
|
||||
;; forgetting about the process completely.
|
||||
(jupyter-with-timeout
|
||||
(nil jupyter-default-timeout
|
||||
(delete-file conn-file))
|
||||
(file-exists-p conn-file))
|
||||
(delete-process process)
|
||||
(let ((new-key (jupyter-new-uuid)))
|
||||
(plist-put conn-info :key new-key)
|
||||
(jupyter-session
|
||||
:conn-info conn-info
|
||||
:key new-key))))))
|
||||
|
||||
(provide 'jupyter-env)
|
||||
|
||||
;;; jupyter-env.el ends here
|
||||
502
lisp/jupyter/jupyter-ioloop.el
Normal file
502
lisp/jupyter/jupyter-ioloop.el
Normal file
@@ -0,0 +1,502 @@
|
||||
;;; jupyter-ioloop.el --- Jupyter channel subprocess -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 03 Nov 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; An ioloop encapsulates a subprocess that communicates with its parent
|
||||
;; process in a pre-defined way. The parent process sends events (lists with a
|
||||
;; head element tagging the type of event and the rest of the elements being
|
||||
;; the arguments), via a call to the `jupyter-send' method of a
|
||||
;; `jupyter-ioloop'. The ioloop subprocess then handles the event in its
|
||||
;; environment. You add an event that can be handled in the ioloop environment
|
||||
;; by calling `jupyter-ioloop-add-event' before calling `jupyter-ioloop-start'.
|
||||
;;
|
||||
;; When one of the events added through `jupyter-ioloop-add-event'
|
||||
;; returns something other than nil, it is sent back to the parent
|
||||
;; process and the handler function passed to `jupyter-ioloop-start'
|
||||
;; is called.
|
||||
;;
|
||||
;; An example that will echo back what was sent to the ioloop as a message in
|
||||
;; the parent process:
|
||||
;;
|
||||
;; (let ((ioloop (jupyter-ioloop))
|
||||
;; (jupyter-ioloop-add-event ioloop echo (data)
|
||||
;; "Return DATA back to the parent process."
|
||||
;; (list 'echo data))
|
||||
;; (jupyter-ioloop-start ioloop (lambda (event) (message "%s" (cadr event))))
|
||||
;; (jupyter-send ioloop 'echo "Message")
|
||||
;; (jupyter-ioloop-stop ioloop))
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-base)
|
||||
(require 'zmq)
|
||||
(eval-when-compile (require 'subr-x))
|
||||
|
||||
(defvar jupyter-ioloop-poller nil
|
||||
"The polling object being used to poll for events in an ioloop.")
|
||||
|
||||
(defvar jupyter-ioloop-stdin nil
|
||||
"A file descriptor or ZMQ socket used to receive events in an ioloop.")
|
||||
|
||||
(defvar jupyter-ioloop-nsockets 1
|
||||
"The number of sockets being polled by `jupyter-ioloop-poller'.")
|
||||
|
||||
(defvar jupyter-ioloop-pre-hook nil
|
||||
"A hook called at the start of every polling loop.
|
||||
The hook is called with no arguments.")
|
||||
|
||||
(defvar jupyter-ioloop-post-hook nil
|
||||
"A hook called at the end of every polling loop.
|
||||
The hook is called with a single argument, the list of polling
|
||||
events that occurred for this iteration or nil. The polling
|
||||
events have the same value as the return value of
|
||||
`zmq-poller-wait-all'.")
|
||||
|
||||
(defvar jupyter-ioloop-timers nil)
|
||||
|
||||
(defvar jupyter-ioloop-timeout 200
|
||||
"Maximum time (in ms) to wait for polling events on `jupyter-ioloop-poller'.")
|
||||
|
||||
(defvar jupyter-ioloop--argument-types nil
|
||||
"Argument types added via `jupyter-ioloop-add-arg-type'.")
|
||||
|
||||
(defun jupyter-ioloop-environment-p ()
|
||||
"Return non-nil if this Emacs instance is an IOLoop subprocess."
|
||||
(and noninteractive jupyter-ioloop-stdin jupyter-ioloop-poller))
|
||||
|
||||
(defclass jupyter-ioloop (jupyter-finalized-object)
|
||||
((process :type (or null process) :initform nil)
|
||||
(callbacks :type list :initform nil)
|
||||
(events :type list :initform nil)
|
||||
(setup :type list :initform nil)
|
||||
(teardown :type list :initform nil))
|
||||
:documentation "An interface for sending asynchronous messages via a subprocess.
|
||||
|
||||
An ioloop starts an Emacs subprocess setup to send events back
|
||||
and forth between the parent Emacs process and the ioloop
|
||||
asynchronously. The ioloop subprocess is essentially a polling
|
||||
loop that polls its stdin and any sockets that may have been
|
||||
created in the ioloop environment and performs pre-defined
|
||||
actions when stdin sends an event. The structure of the
|
||||
subprocess is the following
|
||||
|
||||
\(progn
|
||||
(let ((jupyter-ioloop-poller (zmq-poller)))
|
||||
<jupyter-ioloop-setup>
|
||||
<send start event to parent>
|
||||
(condition-case nil
|
||||
(while t
|
||||
(run-hook 'jupyter-ioloop-pre-hook)
|
||||
<poll for stdin/socket events>
|
||||
(run-hook 'jupyter-ioloop-post-hook))
|
||||
(quit
|
||||
<jupyter-ioloop-teardown>
|
||||
<send quit event to parent>))))
|
||||
|
||||
<jupyter-ioloop-setup> is replaced by the form in the setup slot
|
||||
of an ioloop and can be conveniently added to using
|
||||
`jupyter-ioloop-add-setup'.
|
||||
|
||||
<jupyter-ioloop-teardown> is replaced with the teardown slot and
|
||||
can be added to using `jupyter-ioloop-add-teardown'.
|
||||
|
||||
<poll for stdin/socket events> is replaced by code that will
|
||||
listen for stdin/socket events using `jupyter-ioloop-poller'.
|
||||
|
||||
You add events to be handled by the subprocess using
|
||||
`jupyter-ioloop-add-event', the return value of any event added
|
||||
is what is sent to the parent Emacs process and what will
|
||||
eventually be the sole argument to the handler function passed to
|
||||
`jupyter-ioloop-start'. To suppress the subprocess from sending
|
||||
anything back to the parent, ensure nil is returned by the form
|
||||
created by `jupyter-ioloop-add-event'.
|
||||
|
||||
See `jupyter-channel-ioloop' for an example of its usage.")
|
||||
|
||||
(cl-defmethod initialize-instance ((ioloop jupyter-ioloop) &optional _slots)
|
||||
(cl-call-next-method)
|
||||
(jupyter-add-finalizer ioloop
|
||||
(lambda ()
|
||||
(with-slots (process) ioloop
|
||||
(when (process-live-p process)
|
||||
(delete-process process))))))
|
||||
|
||||
(defun jupyter-ioloop-wait-until (ioloop event cb &optional timeout progress-msg)
|
||||
"Wait until EVENT occurs on IOLOOP.
|
||||
If EVENT occurs, call CB and return its value if non-nil. CB is
|
||||
called with a single argument, an event list whose first element
|
||||
is EVENT. If CB returns nil, continue waiting until EVENT occurs
|
||||
again or until TIMEOUT seconds elapses, TIMEOUT defaults to
|
||||
`jupyter-default-timeout'. If TIMEOUT is reached, return nil.
|
||||
|
||||
If PROGRESS-MSG is non-nil, a progress reporter will be displayed
|
||||
while waiting using PROGRESS-MSG as the message."
|
||||
(declare (indent 2))
|
||||
(cl-check-type ioloop jupyter-ioloop)
|
||||
(jupyter-with-timeout
|
||||
(progress-msg (or timeout jupyter-default-timeout))
|
||||
(let ((e (jupyter-ioloop-last-event ioloop)))
|
||||
(when (eq (car-safe e) event) (funcall cb e)))))
|
||||
|
||||
(defun jupyter-ioloop-last-event (ioloop)
|
||||
"Return the last event received on IOLOOP."
|
||||
(cl-check-type ioloop jupyter-ioloop)
|
||||
(and (oref ioloop process)
|
||||
(process-get (oref ioloop process) :last-event)))
|
||||
|
||||
(defmacro jupyter-ioloop-add-setup (ioloop &rest body)
|
||||
"Set IOLOOP's `jupyter-ioloop-setup' slot to BODY.
|
||||
BODY is the code that will be evaluated before the IOLOOP sends a
|
||||
start event to the parent process."
|
||||
(declare (indent 1))
|
||||
`(setf (oref ,ioloop setup)
|
||||
(append (oref ,ioloop setup)
|
||||
(quote ,body))))
|
||||
|
||||
(defmacro jupyter-ioloop-add-teardown (ioloop &rest body)
|
||||
"Set IOLOOP's `jupyter-ioloop-teardown' slot to BODY.
|
||||
BODY is the code that will be evaluated just before the IOLOOP
|
||||
sends a quit event to the parent process."
|
||||
(declare (indent 1))
|
||||
`(setf (oref ,ioloop teardown)
|
||||
(append (oref ,ioloop teardown)
|
||||
(quote ,body))))
|
||||
|
||||
(defmacro jupyter-ioloop-add-arg-type (tag fun)
|
||||
"Add a new argument type for arguments in `jupyter-ioloop-add-event'.
|
||||
If an argument has the form (arg TAG), where TAG is a symbol, in
|
||||
the ARGS argument of `jupyter-ioloop-add-event', replace it with
|
||||
the result of evaluating the form returned by FUN on arg in the
|
||||
IOLOOP environment.
|
||||
|
||||
For example suppose we define an argument type, jupyter-channel:
|
||||
|
||||
(jupyter-ioloop-add-arg-type jupyter-channel
|
||||
(lambda (arg)
|
||||
`(or (object-assoc ,arg :type jupyter-channel-ioloop-channels)
|
||||
(error \"Channel not alive (%s)\" ,arg))))
|
||||
|
||||
and define an event like
|
||||
|
||||
(jupyter-ioloop-add-event ioloop stop-channel ((channel jupyter-channel))
|
||||
(jupyter-stop channel))
|
||||
|
||||
Finally after adding other events and starting the ioloop we send
|
||||
an event like
|
||||
|
||||
(jupyter-send ioloop \='stop-channel :shell)
|
||||
|
||||
Then before the stop-channel event defined by
|
||||
`jupyter-ioloop-add-event' is called in the IOLOOP environment,
|
||||
the value for the channel argument passed by the `jupyter-send'
|
||||
call is replaced by the form returned by the function specified
|
||||
in the `jupyter-ioloop-add-arg-type' call."
|
||||
(declare (indent 1))
|
||||
`(progn
|
||||
(setf (alist-get ',tag jupyter-ioloop--argument-types nil 'remove) nil)
|
||||
;; NOTE: FUN is quoted to ensure lexical closures aren't created
|
||||
(push (cons ',tag ,(list '\` fun)) jupyter-ioloop--argument-types)))
|
||||
|
||||
(defun jupyter-ioloop--replace-args (args)
|
||||
"Convert special arguments in ARGS.
|
||||
Map over ARGS, converting its elements into
|
||||
|
||||
,arg or ,(app (lambda (x) BODY) arg)
|
||||
|
||||
for use in a `pcase' form. The latter form occurs when one of
|
||||
ARGS is of the form (arg TAG) where TAG is one of the keys in
|
||||
`jupyter-ioloop--argument-types'. BODY will be replaced with the
|
||||
result of calling the function associated with TAG in
|
||||
`jupyter-ioloop--argument-types'.
|
||||
|
||||
Return the list of converted arguments."
|
||||
(mapcar (lambda (arg)
|
||||
(pcase arg
|
||||
(`(,val ,tag)
|
||||
(let ((form (alist-get tag jupyter-ioloop--argument-types)))
|
||||
(list '\, (list 'app `(lambda (x) ,(funcall form 'x)) val))))
|
||||
(_ (list '\, arg))))
|
||||
args))
|
||||
|
||||
(defmacro jupyter-ioloop-add-event (ioloop event args &optional doc &rest body)
|
||||
"For IOLOOP, add an EVENT handler.
|
||||
ARGS is a list of arguments that are bound when EVENT occurs. DOC
|
||||
is an optional documentation string describing what BODY, the
|
||||
expression which will be evaluated when EVENT occurs, does. If
|
||||
BODY evaluates to any non-nil value, it will be sent to the
|
||||
parent Emacs process. A nil value for BODY means don't send
|
||||
anything.
|
||||
|
||||
Some arguments are treated specially:
|
||||
|
||||
If one of ARGS is a list (<sym> tag) where <sym> is any symbol,
|
||||
then the parent process that sends EVENT to IOLOOP is expected to
|
||||
send a value that will be bound to <sym> and be handled by an
|
||||
argument handler associated with tag before BODY is evaluated in
|
||||
the IOLOOP process, see `jupyter-ioloop-add-arg-type'."
|
||||
(declare (indent 3) (doc-string 4) (debug t))
|
||||
(unless (stringp doc)
|
||||
(when doc
|
||||
(setq body (cons doc body))))
|
||||
`(setf (oref ,ioloop events)
|
||||
(cons (list (quote ,event) (quote ,args) (quote ,body))
|
||||
(cl-remove-if (lambda (x) (eq (car x) (quote ,event)))
|
||||
(oref ,ioloop events)))))
|
||||
|
||||
(defun jupyter-ioloop--event-dispatcher (ioloop exp)
|
||||
"For IOLOOP return a form suitable for matching against EXP.
|
||||
That is, return an expression which will cause an event to be
|
||||
fired if EXP matches any event types handled by IOLOOP.
|
||||
|
||||
TODO: Explain these
|
||||
By default this adds the events quit, callback, and timer."
|
||||
(let ((user-events
|
||||
(cl-loop
|
||||
for (event args body) in (oref ioloop events)
|
||||
for cond = (list '\` (cl-list*
|
||||
event (jupyter-ioloop--replace-args args)))
|
||||
if (memq event '(quit callback timer))
|
||||
do (error "Event can't be one of quit, callback, or, timer")
|
||||
;; cond = `(event ,arg1 ,arg2 ...)
|
||||
else collect `(,cond ,@body))))
|
||||
`(let* ((cmd ,exp)
|
||||
(res (pcase cmd
|
||||
,@user-events
|
||||
;; Default events
|
||||
(`(timer ,id ,period ,cb)
|
||||
;; Ensure we don't send anything back to the parent process
|
||||
(prog1 nil
|
||||
(let ((timer (run-at-time 0.0 period (byte-compile cb))))
|
||||
(puthash id timer jupyter-ioloop-timers))))
|
||||
(`(callback ,cb)
|
||||
;; Ensure we don't send anything back to the parent process
|
||||
(prog1 nil
|
||||
(setq jupyter-ioloop-timeout 0)
|
||||
(add-hook 'jupyter-ioloop-pre-hook (byte-compile cb) 'append)))
|
||||
('(quit) (signal 'quit nil))
|
||||
(_ (error "Unhandled command %s" cmd)))))
|
||||
;; Can only send lists at the moment
|
||||
(when (and res (listp res)) (zmq-prin1 res)))))
|
||||
|
||||
(cl-defgeneric jupyter-ioloop-add-callback ()
|
||||
(declare (indent 1)))
|
||||
|
||||
(cl-defmethod jupyter-ioloop-add-callback ((ioloop jupyter-ioloop) cb)
|
||||
"In IOLOOP, add CB to be run in the IOLOOP environment.
|
||||
CB is run at the start of every polling loop. Callbacks are
|
||||
called in the order they are added.
|
||||
|
||||
WARNING: A function added as a callback should be quoted to avoid
|
||||
sending closures to the IOLOOP. An example:
|
||||
|
||||
(jupyter-ioloop-add-callback ioloop
|
||||
`(lambda () (zmq-prin1 \='foo \"bar\")))"
|
||||
(cl-assert (functionp cb))
|
||||
(cl-callf append (oref ioloop callbacks) (list cb))
|
||||
(when (process-live-p (oref ioloop process))
|
||||
(jupyter-send ioloop 'callback (macroexpand-all cb))))
|
||||
|
||||
(defun jupyter-ioloop-poller-add (socket events)
|
||||
"Add SOCKET to be polled using the `jupyter-ioloop-poller'.
|
||||
EVENTS are the polling events that should be listened for on
|
||||
SOCKET. If `jupyter-ioloop-poller' is not a `zmq-poller' object
|
||||
do nothing."
|
||||
(when (zmq-poller-p jupyter-ioloop-poller)
|
||||
(zmq-poller-add jupyter-ioloop-poller socket events)
|
||||
(cl-incf jupyter-ioloop-nsockets)))
|
||||
|
||||
(defun jupyter-ioloop-poller-remove (socket)
|
||||
"Remove SOCKET from the `jupyter-ioloop-poller'.
|
||||
If `jupyter-ioloop-poller' is not a `zmq-poller' object do
|
||||
nothing."
|
||||
(when (zmq-poller-p jupyter-ioloop-poller)
|
||||
(zmq-poller-remove jupyter-ioloop-poller socket)
|
||||
(cl-decf jupyter-ioloop-nsockets)))
|
||||
|
||||
(defun jupyter-ioloop--body (ioloop on-stdin)
|
||||
`(let (events)
|
||||
(condition-case nil
|
||||
(progn
|
||||
,@(oref ioloop setup)
|
||||
;; Initialize any callbacks that were added before the ioloop was
|
||||
;; started
|
||||
(setq jupyter-ioloop-pre-hook
|
||||
(mapcar (lambda (f)
|
||||
(when (symbolp f)
|
||||
(setq f (symbol-function f)))
|
||||
(unless (byte-code-function-p f)
|
||||
(byte-compile f)))
|
||||
(append jupyter-ioloop-pre-hook
|
||||
(quote ,(mapcar #'macroexpand-all
|
||||
(oref ioloop callbacks))))))
|
||||
;; Notify the parent process we are ready to do something
|
||||
(zmq-prin1 '(start))
|
||||
(let ((on-stdin (byte-compile (lambda () ,on-stdin))))
|
||||
(while t
|
||||
(run-hooks 'jupyter-ioloop-pre-hook)
|
||||
(setq events
|
||||
(condition-case nil
|
||||
(zmq-poller-wait-all
|
||||
jupyter-ioloop-poller
|
||||
jupyter-ioloop-nsockets
|
||||
jupyter-ioloop-timeout)
|
||||
((zmq-EAGAIN zmq-EINTR zmq-ETIMEDOUT) nil)))
|
||||
(let ((stdin-event (zmq-assoc jupyter-ioloop-stdin events)))
|
||||
(when stdin-event
|
||||
(setq events (delq stdin-event events))
|
||||
(funcall on-stdin)))
|
||||
(run-hook-with-args 'jupyter-ioloop-post-hook events))))
|
||||
(quit
|
||||
,@(oref ioloop teardown)
|
||||
(zmq-prin1 '(quit))))))
|
||||
|
||||
(defun jupyter-ioloop--function (ioloop port)
|
||||
"Return the function that does the work of IOLOOP.
|
||||
The returned function is suitable to send to a ZMQ subprocess for
|
||||
evaluation using `zmq-start-process'.
|
||||
|
||||
If PORT is non-nil the returned function will create a ZMQ PULL
|
||||
socket to receive events from the parent process on the PORT of
|
||||
the local host, otherwise events are expected to be received on
|
||||
STDIN. This is useful on Windows systems which don't allow
|
||||
polling the STDIN file handle."
|
||||
`(lambda (ctx)
|
||||
(push ,(file-name-directory (locate-library "jupyter-base")) load-path)
|
||||
(require 'jupyter-ioloop)
|
||||
(setq jupyter-ioloop-poller (zmq-poller))
|
||||
(setq jupyter-ioloop-stdin
|
||||
,(if port
|
||||
`(let ((sock (zmq-socket ctx zmq-PAIR)))
|
||||
(prog1 sock
|
||||
(zmq-connect sock (format "tcp://127.0.0.1:%s" ,port))))
|
||||
0))
|
||||
(zmq-poller-add jupyter-ioloop-poller jupyter-ioloop-stdin zmq-POLLIN)
|
||||
,(jupyter-ioloop--body
|
||||
ioloop (jupyter-ioloop--event-dispatcher
|
||||
ioloop (if port '(read (zmq-recv jupyter-ioloop-stdin))
|
||||
'(zmq-subprocess-read))))))
|
||||
|
||||
(defun jupyter-ioloop-alive-p (ioloop)
|
||||
"Return non-nil if IOLOOP is ready to receive/send events."
|
||||
(cl-check-type ioloop jupyter-ioloop)
|
||||
(with-slots (process) ioloop
|
||||
(and (process-live-p process) (process-get process :start))))
|
||||
|
||||
(defun jupyter-ioloop--make-filter (ioloop handler)
|
||||
(lambda (event)
|
||||
(let ((process (oref ioloop process)))
|
||||
(process-put process :last-event event)
|
||||
(cond
|
||||
((eq (car-safe event) 'start)
|
||||
(process-put process :start t))
|
||||
((eq (car-safe event) 'quit)
|
||||
(process-put process :quit t))
|
||||
(t
|
||||
(funcall handler event))))))
|
||||
|
||||
(cl-defmethod jupyter-ioloop-start ((ioloop jupyter-ioloop)
|
||||
handler
|
||||
&key buffer)
|
||||
"Start an IOLOOP.
|
||||
HANDLER is a function of one argument and will be passed an event
|
||||
received by the subprocess that IOLOOP represents, an event is
|
||||
just a list.
|
||||
|
||||
If IOLOOP was previously running, it is stopped first.
|
||||
|
||||
If BUFFER is non-nil it should be a buffer that will be used as
|
||||
the IOLOOP subprocess buffer, see `zmq-start-process'."
|
||||
(jupyter-ioloop-stop ioloop)
|
||||
(let (stdin port)
|
||||
;; NOTE: A socket is used to read input from the parent process to avoid
|
||||
;; the stdin buffering done when using `read-from-minibuffer' in the
|
||||
;; subprocess. When `noninteractive', `read-from-minibuffer' uses
|
||||
;; `getc_unlocked' internally and `getc_unlocked' reads from the stdin FILE
|
||||
;; object as opposed to reading directly from STDIN_FILENO. The problem is
|
||||
;; that FILE objects are buffered streams which means that every message
|
||||
;; the parent process sends does not necessarily correspond to a POLLIN
|
||||
;; event on STDIN_FILENO in the subprocess. Since we only call
|
||||
;; `read-from-minibuffer' when there is a POLLIN event on STDIN_FILENO
|
||||
;; there is the potential that a message is waiting to be handled in the
|
||||
;; buffer used by stdin which will only get handled if we send more
|
||||
;; messages to the subprocess thereby creating more POLLIN events.
|
||||
(when (or t (memq system-type '(windows-nt ms-dos cygwin)))
|
||||
(setq stdin (zmq-socket (zmq-current-context) zmq-PAIR))
|
||||
(setq port (zmq-bind-to-random-port stdin "tcp://127.0.0.1")))
|
||||
(let ((process (zmq-start-process
|
||||
(jupyter-ioloop--function ioloop (when stdin port))
|
||||
:filter (jupyter-ioloop--make-filter ioloop handler)
|
||||
:buffer buffer)))
|
||||
(oset ioloop process process)
|
||||
(when stdin
|
||||
(process-put process :stdin stdin))
|
||||
(jupyter-ioloop-wait-until ioloop 'start #'identity))))
|
||||
|
||||
(cl-defmethod jupyter-ioloop-stop ((ioloop jupyter-ioloop))
|
||||
"Stop IOLOOP.
|
||||
Send a quit event to IOLOOP, wait until it actually quits before
|
||||
returning."
|
||||
(with-slots (process) ioloop
|
||||
(when (process-live-p process)
|
||||
(jupyter-send ioloop 'quit)
|
||||
(unless (jupyter-ioloop-wait-until ioloop 'quit #'identity)
|
||||
(delete-process process))
|
||||
(when-let* ((stdin (process-get process :stdin))
|
||||
(socket-p (zmq-socket-p stdin)))
|
||||
(zmq-unbind stdin (zmq-get-option stdin zmq-LAST-ENDPOINT))))))
|
||||
|
||||
(defvar jupyter-ioloop--send-buffer nil)
|
||||
|
||||
(defun jupyter-ioloop--dump-message (plist)
|
||||
(with-current-buffer
|
||||
(if (buffer-live-p jupyter-ioloop--send-buffer)
|
||||
jupyter-ioloop--send-buffer
|
||||
(setq jupyter-ioloop--send-buffer
|
||||
(get-buffer-create " *jupyter-ioloop-send*")))
|
||||
(erase-buffer)
|
||||
(let (print-level print-length)
|
||||
(prin1 plist (current-buffer)))
|
||||
(buffer-string)))
|
||||
|
||||
(cl-defmethod jupyter-send ((ioloop jupyter-ioloop) &rest args)
|
||||
"Using IOLOOP, send ARGS to its process.
|
||||
|
||||
All arguments passed to this function are sent as a list to the
|
||||
process unchanged. This means that all arguments should be
|
||||
serializable."
|
||||
(with-slots (process) ioloop
|
||||
(cl-assert (process-live-p process))
|
||||
(let ((stdin (process-get process :stdin)))
|
||||
(if stdin
|
||||
(let ((msg (jupyter-ioloop--dump-message args)) sent)
|
||||
(while (not sent)
|
||||
(condition-case nil
|
||||
(progn
|
||||
(zmq-send stdin (encode-coding-string msg 'utf-8) zmq-DONTWAIT)
|
||||
(setq sent t))
|
||||
(zmq-EAGAIN (accept-process-output nil 0)))))
|
||||
(zmq-subprocess-send process args)))))
|
||||
|
||||
(provide 'jupyter-ioloop)
|
||||
|
||||
;;; jupyter-ioloop.el ends here
|
||||
54
lisp/jupyter/jupyter-javascript.el
Normal file
54
lisp/jupyter/jupyter-javascript.el
Normal file
@@ -0,0 +1,54 @@
|
||||
;;; jupyter-javascript.el --- Jupyter support for Javascript -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 23 Oct 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Support methods for integration with Javascript.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-repl)
|
||||
|
||||
(declare-function js2-parse "ext:js2-mode")
|
||||
(declare-function js2-mode-apply-deferred-properties "ext:js2-mode")
|
||||
|
||||
(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))
|
||||
|
||||
(provide 'jupyter-javascript)
|
||||
|
||||
;;; jupyter-javascript.el ends here
|
||||
245
lisp/jupyter/jupyter-julia.el
Normal file
245
lisp/jupyter/jupyter-julia.el
Normal file
@@ -0,0 +1,245 @@
|
||||
;;; jupyter-julia.el --- Jupyter support for Julia -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 23 Oct 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Support methods for integration with Julia.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(eval-when-compile (require 'subr-x))
|
||||
(require 'jupyter-repl)
|
||||
|
||||
(declare-function julia-latexsub-or-indent "ext:julia-mode" (arg))
|
||||
|
||||
(cl-defmethod jupyter-indent-line (&context (major-mode julia-mode))
|
||||
"Call `julia-latexsub-or-indent'."
|
||||
(call-interactively #'julia-latexsub-or-indent))
|
||||
|
||||
(cl-defmethod jupyter-load-file-code (file &context (jupyter-lang julia))
|
||||
(format "include(\"%s\");" file))
|
||||
|
||||
;;; Completion
|
||||
|
||||
(cl-defmethod jupyter-completion-prefix (&context (jupyter-lang julia))
|
||||
(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)))
|
||||
(t
|
||||
(let ((prefix (cl-call-next-method "\\\\\\|\\.\\|::?" 2)))
|
||||
(prog1 prefix
|
||||
(when (consp prefix)
|
||||
(let ((beg (- (point) (length (car prefix)))))
|
||||
(cond
|
||||
;; Include the \ in the prefix so it gets replaced if a canidate is
|
||||
;; selected.
|
||||
((eq (char-before beg) ?\\)
|
||||
(setcar prefix (concat "\\" (car prefix))))
|
||||
;; Also include : to complete symbols when used as dictionary keys
|
||||
((and (eq (char-before beg) ?:)
|
||||
(not (eq (char-before (1- beg)) ?:))
|
||||
;; Except for when it is part of range expressions like 1:len
|
||||
(not (memq (char-syntax (char-before (1- beg))) '(?w ?_))))
|
||||
(setcar prefix (concat ":" (car prefix))))))))))))
|
||||
|
||||
(cl-defmethod jupyter-completion-post-completion (candidate
|
||||
&context (jupyter-lang julia))
|
||||
"Insert the unicode representation of a LaTeX completion."
|
||||
(if (eq (aref candidate 0) ?\\)
|
||||
(when (get-text-property 0 'annot candidate)
|
||||
(search-backward candidate)
|
||||
(delete-region (point) (match-end 0))
|
||||
;; Alternatively use `julia-latexsub-or-indent', but I have found
|
||||
;; problems with that.
|
||||
(insert (string-trim (get-text-property 0 'annot candidate))))
|
||||
(cl-call-next-method)))
|
||||
|
||||
;;; `markdown-mode'
|
||||
|
||||
(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\".
|
||||
If URL is \"@ref <section>\" then open a browser to the Julia
|
||||
manual for <section>. Otherwise follow the link normally."
|
||||
(if (string-prefix-p "@ref" url)
|
||||
(if (string= url "@ref")
|
||||
;; Links have the form `fun`
|
||||
(let ((fun (substring link-text 1 -1)))
|
||||
(if (not (eq major-mode 'jupyter-repl-mode))
|
||||
(jupyter-inspect fun (1- (length fun)))
|
||||
(goto-char (point-max))
|
||||
(jupyter-repl-replace-cell-code (concat "?" fun))
|
||||
(jupyter-repl-ret)))
|
||||
(let* ((ref (split-string url))
|
||||
(section (cadr ref)))
|
||||
(browse-url
|
||||
(format "https://docs.julialang.org/en/latest/manual/%s/" section))))
|
||||
(cl-call-next-method)))
|
||||
|
||||
;;; `jupyter-repl-after-change'
|
||||
|
||||
(defvar ansi-color-names-vector)
|
||||
|
||||
(defun jupyter-julia-add-prompt (prompt color)
|
||||
"Display PROMPT at the beginning of the cell using COLOR as the foreground.
|
||||
Make the character after `point' invisible."
|
||||
(add-text-properties (point) (1+ (point)) '(invisible t rear-nonsticky t))
|
||||
(let ((ov (make-overlay (point) (1+ (point)) nil t))
|
||||
(md (propertize prompt
|
||||
'fontified t
|
||||
'font-lock-face `((:foreground ,color)))))
|
||||
(overlay-put ov 'after-string (propertize " " 'display md))
|
||||
(overlay-put ov 'evaporate t)))
|
||||
|
||||
(defun jupyter-julia-pkg-prompt ()
|
||||
"Return the Pkg prompt.
|
||||
If the Pkg prompt can't be retrieved from the kernel, return
|
||||
nil."
|
||||
(let ((prompt-code "import Pkg; Pkg.REPLMode.promptf()"))
|
||||
(jupyter-run-with-client jupyter-current-client
|
||||
(jupyter-mlet* ((msg
|
||||
(jupyter-reply
|
||||
(jupyter-execute-request
|
||||
:code ""
|
||||
:silent t
|
||||
:user-expressions (list :prompt prompt-code)))))
|
||||
(cl-destructuring-bind (&key prompt &allow-other-keys)
|
||||
(jupyter-message-get msg :user_expressions)
|
||||
(cl-destructuring-bind (&key status data &allow-other-keys)
|
||||
prompt
|
||||
(jupyter-return
|
||||
(when (equal status "ok")
|
||||
(plist-get data :text/plain)))))))))
|
||||
|
||||
(cl-defmethod jupyter-repl-after-change ((_type (eql insert)) beg _end
|
||||
&context (jupyter-lang julia))
|
||||
"Change the REPL prompt when a REPL mode is entered."
|
||||
(when (= beg (jupyter-repl-cell-code-beginning-position))
|
||||
(save-excursion
|
||||
(goto-char beg)
|
||||
(when (and (bound-and-true-p blink-paren-function)
|
||||
(eq (char-syntax (char-after)) ?\)))
|
||||
;; Spoof `last-command-event' so that a "No matching paren" message
|
||||
;; doesn't happen.
|
||||
(setq last-command-event ?\[))
|
||||
(cl-case (char-after)
|
||||
(?\]
|
||||
(when-let* ((pkg-prompt (jupyter-julia-pkg-prompt)))
|
||||
(jupyter-julia-add-prompt
|
||||
(substring pkg-prompt 1 (1- (length pkg-prompt)))
|
||||
(aref ansi-color-names-vector 5)))) ; magenta
|
||||
(?\;
|
||||
(jupyter-julia-add-prompt
|
||||
"shell> " (aref ansi-color-names-vector 1))) ; red
|
||||
(?\?
|
||||
(jupyter-julia-add-prompt
|
||||
"help?> " (aref ansi-color-names-vector 3)))))) ; yellow
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-repl-after-change ((_type (eql delete)) beg _len
|
||||
&context (jupyter-lang julia))
|
||||
"Reset the prompt if needed."
|
||||
(when (= beg (jupyter-repl-cell-code-beginning-position))
|
||||
(jupyter-repl-cell-reset-prompt)))
|
||||
|
||||
;;; REPL font lock
|
||||
|
||||
(defun jupyter-julia--propertize-repl-mode-char (beg end)
|
||||
(jupyter-repl-map-cells beg end
|
||||
(lambda ()
|
||||
;; Handle Julia package prompt so `syntax-ppss' works properly.
|
||||
(when (and (eq (char-syntax (char-after (point-min))) ?\))
|
||||
(= (point-min)
|
||||
(save-restriction
|
||||
(widen)
|
||||
;; Looks at the position before the narrowed cell-code
|
||||
;; which is why the widen is needed here.
|
||||
(jupyter-repl-cell-code-beginning-position))))
|
||||
(put-text-property
|
||||
(point-min) (1+ (point-min)) 'syntax-table '(1 . ?.))))
|
||||
#'ignore))
|
||||
|
||||
;;; `jupyter-repl-after-init'
|
||||
|
||||
(defun jupyter-julia--setup-hooks (client)
|
||||
(jupyter-run-with-client client
|
||||
(jupyter-sent
|
||||
(jupyter-execute-request
|
||||
:handlers nil
|
||||
:store-history nil
|
||||
:silent t
|
||||
;; This is mainly for supporting the :dir header argument in
|
||||
;; `org-mode' source blocks.
|
||||
:code "\
|
||||
if !isdefined(Main, :__JUPY_saved_dir)
|
||||
Core.eval(Main, :(__JUPY_saved_dir = Ref(\"\")))
|
||||
let popdir = () -> begin
|
||||
if !isempty(Main.__JUPY_saved_dir[])
|
||||
cd(Main.__JUPY_saved_dir[])
|
||||
Main.__JUPY_saved_dir[] = \"\"
|
||||
end
|
||||
end
|
||||
IJulia.push_posterror_hook(popdir)
|
||||
IJulia.push_postexecute_hook(popdir)
|
||||
end
|
||||
end"))))
|
||||
|
||||
(cl-defmethod jupyter-repl-after-init (&context (jupyter-lang julia))
|
||||
(if syntax-propertize-function
|
||||
(add-function
|
||||
:after (local 'syntax-propertize-function)
|
||||
#'jupyter-julia--propertize-repl-mode-char)
|
||||
(setq-local syntax-propertize-function #'jupyter-julia--propertize-repl-mode-char))
|
||||
(jupyter-julia--setup-hooks jupyter-current-client)
|
||||
;; Setup hooks after restart as well
|
||||
(jupyter-add-hook jupyter-current-client 'jupyter-iopub-message-hook
|
||||
(lambda (client msg)
|
||||
(when (jupyter-message-status-starting-p msg)
|
||||
(jupyter-julia--setup-hooks client)))))
|
||||
|
||||
;;; `jupyter-org'
|
||||
|
||||
(cl-defmethod jupyter-org-error-location (&context (jupyter-lang julia))
|
||||
(when (and (re-search-forward "^Stacktrace:" nil t)
|
||||
(re-search-forward "top-level scope" nil t)
|
||||
(re-search-forward "In\\[[0-9]+\\]:\\([0-9]+\\)" nil t))
|
||||
(string-to-number (match-string 1))))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-transform-code (code changelist &context (jupyter-lang julia))
|
||||
(when (plist-get changelist :dir)
|
||||
(setq code
|
||||
;; Stay on one line so that tracebacks will report the right line
|
||||
;; numbers
|
||||
(format "Main.__JUPY_saved_dir[] = pwd(); cd(\"%s\"); %s"
|
||||
(plist-get changelist :dir) code)))
|
||||
code)
|
||||
|
||||
(provide 'jupyter-julia)
|
||||
|
||||
;;; jupyter-julia.el ends here
|
||||
416
lisp/jupyter/jupyter-kernel-process.el
Normal file
416
lisp/jupyter/jupyter-kernel-process.el
Normal file
@@ -0,0 +1,416 @@
|
||||
;;; jupyter-kernel-process.el --- Jupyter kernels as Emacs processes -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2020-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 25 Apr 2020
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Jupyter kernels as Emacs processes.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-kernel)
|
||||
(require 'jupyter-monads)
|
||||
|
||||
(defgroup jupyter-kernel-process nil
|
||||
"Jupyter kernels as Emacs processes"
|
||||
:group 'jupyter)
|
||||
|
||||
(declare-function jupyter-ioloop-start "jupyter-ioloop")
|
||||
(declare-function jupyter-ioloop-stop "jupyter-ioloop")
|
||||
(declare-function jupyter-send "jupyter-ioloop")
|
||||
(declare-function jupyter-ioloop-alive-p "jupyter-ioloop")
|
||||
(declare-function jupyter-channel-ioloop-set-session "jupyter-channel-ioloop")
|
||||
(declare-function ansi-color-apply "ansi-color")
|
||||
(declare-function jupyter-hb-pause "jupyter-zmq-channel")
|
||||
|
||||
(defvar jupyter--kernel-processes '()
|
||||
"The list of kernel processes launched.
|
||||
Elements look like (PROCESS CONN-FILE) where PROCESS is a kernel
|
||||
process and CONN-FILE the associated connection file.
|
||||
|
||||
Cleaning up the list removes elements whose PROCESS is no longer
|
||||
live. When removing an element, CONN-FILE will be deleted and
|
||||
PROCESS's buffer killed.
|
||||
|
||||
The list is periodically cleaned up when a new process is
|
||||
launched.
|
||||
|
||||
Also, just before Emacs exits any connection files that still
|
||||
exist are deleted.")
|
||||
|
||||
;;; Kernel definition
|
||||
|
||||
(cl-defstruct (jupyter-kernel-process
|
||||
(:include jupyter-kernel))
|
||||
connect-p)
|
||||
|
||||
(cl-defmethod jupyter-process ((kernel jupyter-kernel-process))
|
||||
"Return the process of KERNEL.
|
||||
Return nil if KERNEL does not have an associated process."
|
||||
(car (cl-find-if (lambda (x) (and (processp (car x))
|
||||
(eq (process-get (car x) :kernel) kernel)))
|
||||
jupyter--kernel-processes)))
|
||||
|
||||
(cl-defmethod jupyter-alive-p ((kernel jupyter-kernel-process))
|
||||
(let ((process (jupyter-process kernel)))
|
||||
(and (process-live-p process)
|
||||
(cl-call-next-method))))
|
||||
|
||||
(defun jupyter-kernel-process (&rest args)
|
||||
"Return a `jupyter-kernel-process' initialized with ARGS."
|
||||
(apply #'make-jupyter-kernel-process args))
|
||||
|
||||
(cl-defmethod jupyter-kernel :extra "process" (&rest args)
|
||||
"Return a kernel as an Emacs process.
|
||||
If ARGS contains a :spec key with a value being a
|
||||
`jupyter-kernelspec', a `jupyter-kernel-process' initialized from
|
||||
it will be returned. The value can also be a string, in which
|
||||
case it is considered the name of a kernelspec to use.
|
||||
|
||||
If ARGS contains a :conn-info key, a `jupyter-kernel-process'
|
||||
with a session initialized from its value, either the name of a
|
||||
connection file to read or a connection property list itself (see
|
||||
`jupyter-read-connection'), will be returned. The remaining ARGS
|
||||
will be used to initialize the returned kernel.
|
||||
|
||||
Call the next method if ARGS does not contain a :spec or
|
||||
:conn-info key."
|
||||
(if (plist-get args :server) (cl-call-next-method)
|
||||
(let ((spec (plist-get args :spec))
|
||||
(conn-info (plist-get args :conn-info)))
|
||||
(cond
|
||||
((and spec (not conn-info))
|
||||
(when (stringp spec)
|
||||
(plist-put args :spec
|
||||
(or (jupyter-guess-kernelspec spec)
|
||||
(error "No kernelspec matching name (%s)" spec))))
|
||||
(cl-check-type (plist-get args :spec) jupyter-kernelspec)
|
||||
(apply #'jupyter-kernel-process args))
|
||||
(conn-info
|
||||
(apply #'jupyter-kernel-process
|
||||
:session (if (stringp conn-info)
|
||||
(jupyter-connection-file-to-session conn-info)
|
||||
conn-info)
|
||||
(cl-loop
|
||||
for (k v) on args by #'cddr
|
||||
unless (eq k :conn-info) collect k and collect v)))
|
||||
(t
|
||||
(cl-call-next-method))))))
|
||||
|
||||
;;; Client connection
|
||||
|
||||
(cl-defmethod jupyter-zmq-io ((kernel jupyter-kernel-process))
|
||||
(unless (jupyter-kernel-process-connect-p kernel)
|
||||
(jupyter-launch kernel))
|
||||
(let ((channels '(:shell :iopub :stdin :control))
|
||||
session ch-group hb kernel-io ioloop shutdown)
|
||||
(cl-macrolet ((continue-after
|
||||
(cond on-timeout)
|
||||
`(jupyter-with-timeout
|
||||
(nil jupyter-default-timeout ,on-timeout)
|
||||
,cond)))
|
||||
(cl-labels ((set-session
|
||||
()
|
||||
(or (setq session (jupyter-kernel-session kernel))
|
||||
(error "A session is needed to connect to a kernel's I/O")))
|
||||
(set-ch-group
|
||||
()
|
||||
(let ((endpoints (jupyter-session-endpoints (set-session))))
|
||||
(setq ch-group
|
||||
(cl-loop
|
||||
for ch in channels
|
||||
collect ch
|
||||
collect (list :endpoint (plist-get endpoints ch)
|
||||
:alive-p nil)))))
|
||||
(ch-put
|
||||
(ch prop value)
|
||||
(plist-put (plist-get ch-group ch) prop value))
|
||||
(ch-get
|
||||
(ch prop)
|
||||
(plist-get (plist-get ch-group ch) prop))
|
||||
(ch-alive-p
|
||||
(ch)
|
||||
(and ioloop (jupyter-ioloop-alive-p ioloop)
|
||||
(ch-get ch :alive-p)))
|
||||
(ch-start
|
||||
(ch)
|
||||
(unless (ch-alive-p ch)
|
||||
(jupyter-send ioloop 'start-channel ch
|
||||
(ch-get ch :endpoint))
|
||||
(continue-after
|
||||
(ch-alive-p ch)
|
||||
(error "Channel failed to start: %s" ch))))
|
||||
(ch-stop
|
||||
(ch)
|
||||
(when (ch-alive-p ch)
|
||||
(jupyter-send ioloop 'stop-channel ch)
|
||||
(continue-after
|
||||
(not (ch-alive-p ch))
|
||||
(error "Channel failed to stop: %s" ch))))
|
||||
(start
|
||||
()
|
||||
(unless ioloop
|
||||
(require 'jupyter-zmq-channel-ioloop)
|
||||
(setq ioloop (make-instance 'jupyter-zmq-channel-ioloop))
|
||||
(jupyter-channel-ioloop-set-session ioloop session))
|
||||
(unless (jupyter-ioloop-alive-p ioloop)
|
||||
(jupyter-ioloop-start
|
||||
ioloop
|
||||
(lambda (event)
|
||||
(pcase (car event)
|
||||
((and 'start-channel (let ch (cadr event)))
|
||||
(ch-put ch :alive-p t))
|
||||
((and 'stop-channel (let ch (cadr event)))
|
||||
(ch-put ch :alive-p nil))
|
||||
;; TODO: Get rid of this
|
||||
('sent nil)
|
||||
(_
|
||||
(jupyter-run-with-io kernel-io
|
||||
(jupyter-publish event))))))
|
||||
(condition-case err
|
||||
(cl-loop
|
||||
for ch in channels
|
||||
do (ch-start ch))
|
||||
(error
|
||||
(jupyter-ioloop-stop ioloop)
|
||||
(signal (car err) (cdr err)))))
|
||||
ioloop)
|
||||
(stop
|
||||
()
|
||||
(when hb
|
||||
(jupyter-hb-pause hb)
|
||||
(setq hb nil))
|
||||
(when ioloop
|
||||
(when (jupyter-ioloop-alive-p ioloop)
|
||||
(jupyter-ioloop-stop ioloop))
|
||||
(setq ioloop nil))))
|
||||
(set-ch-group)
|
||||
(setq kernel-io
|
||||
;; TODO: (jupyter-publisher :name "Session I/O" :fn ...)
|
||||
;;
|
||||
;; so that on error in a subscriber, the name can be
|
||||
;; displayed to know where to look. This requires a
|
||||
;; `jupyter-publisher' struct type.
|
||||
(jupyter-publisher
|
||||
(lambda (content)
|
||||
(if shutdown
|
||||
(error "Kernel I/O no longer available: %s"
|
||||
(cl-prin1-to-string session))
|
||||
(pcase (car content)
|
||||
;; ('message channel idents . msg)
|
||||
('message
|
||||
(pop content)
|
||||
;; Set the channel key of the message property list
|
||||
(plist-put
|
||||
(cddr content) :channel
|
||||
(substring (symbol-name (car content)) 1))
|
||||
(jupyter-content (cddr content)))
|
||||
('send
|
||||
;; Set the channel argument to a keyword so its
|
||||
;; recognized by the ioloop
|
||||
(setq content
|
||||
(cons (car content)
|
||||
(cons (intern (concat ":" (cadr content)))
|
||||
(cddr content))))
|
||||
(apply #'jupyter-send (start) content))
|
||||
('hb
|
||||
(unless hb
|
||||
(setq hb
|
||||
(let ((endpoints (set-session)))
|
||||
(make-instance
|
||||
'jupyter-hb-channel
|
||||
:session session
|
||||
:endpoint (plist-get endpoints :hb)))))
|
||||
(jupyter-run-with-io (cadr content)
|
||||
(jupyter-publish hb)))
|
||||
(_ (error "Unhandled I/O: %s" content)))))))
|
||||
(list kernel-io
|
||||
(jupyter-subscriber
|
||||
(lambda (action)
|
||||
(pcase action
|
||||
('interrupt
|
||||
(jupyter-interrupt kernel))
|
||||
('shutdown
|
||||
(jupyter-shutdown kernel)
|
||||
(stop)
|
||||
(setq shutdown t))
|
||||
('restart
|
||||
(setq shutdown nil)
|
||||
(jupyter-restart kernel)
|
||||
(stop)
|
||||
(set-ch-group)
|
||||
(start))
|
||||
(`(action ,fn)
|
||||
(funcall fn kernel))))))))))
|
||||
|
||||
(cl-defmethod jupyter-io ((kernel jupyter-kernel-process))
|
||||
"Return an I/O connection to KERNEL's session."
|
||||
(jupyter-zmq-io kernel))
|
||||
|
||||
;;; Kernel management
|
||||
|
||||
(defun jupyter--gc-kernel-processes ()
|
||||
(setq jupyter--kernel-processes
|
||||
(cl-loop for (p conn-file) in jupyter--kernel-processes
|
||||
if (process-live-p p) collect (list p conn-file)
|
||||
else do (delete-process p)
|
||||
(when (file-exists-p conn-file)
|
||||
(delete-file conn-file))
|
||||
and when (buffer-live-p (process-buffer p))
|
||||
do (kill-buffer (process-buffer p)))))
|
||||
|
||||
(defun jupyter-delete-connection-files ()
|
||||
"Delete all connection files created by Emacs."
|
||||
;; Ensure Emacs can be killed on error
|
||||
(ignore-errors
|
||||
(cl-loop for (_ conn-file) in jupyter--kernel-processes
|
||||
do (when (file-exists-p conn-file)
|
||||
(delete-file conn-file)))))
|
||||
|
||||
(add-hook 'kill-emacs-hook #'jupyter-delete-connection-files)
|
||||
|
||||
(defun jupyter--start-kernel-process (name kernelspec conn-file)
|
||||
(let* ((process-name (format "jupyter-kernel-%s" name))
|
||||
(buffer-name (format " *jupyter-kernel[%s]*" name))
|
||||
(process-environment
|
||||
(append (jupyter-process-environment kernelspec)
|
||||
process-environment))
|
||||
(args (jupyter-kernel-argv kernelspec conn-file))
|
||||
(atime (nth 4 (file-attributes conn-file)))
|
||||
(process (apply #'start-file-process process-name
|
||||
(generate-new-buffer buffer-name)
|
||||
(car args) (cdr args))))
|
||||
(set-process-query-on-exit-flag process jupyter--debug)
|
||||
;; Wait until the connection file has been read before returning.
|
||||
;; This is to give the kernel a chance to setup before sending it
|
||||
;; messages.
|
||||
;;
|
||||
;; TODO: Replace with a check of the heartbeat channel.
|
||||
(jupyter-with-timeout
|
||||
((format "Starting %s kernel process..." name)
|
||||
jupyter-long-timeout
|
||||
(unless (process-live-p process)
|
||||
(error "Kernel process exited:\n%s"
|
||||
(with-current-buffer (process-buffer process)
|
||||
(ansi-color-apply (buffer-string))))))
|
||||
;; Windows systems may not have good time resolution when retrieving
|
||||
;; the last access time of a file so we don't bother with checking that
|
||||
;; the kernel has read the connection file and leave it to the
|
||||
;; downstream initialization to ensure that we can communicate with a
|
||||
;; kernel.
|
||||
(or (memq system-type '(ms-dos windows-nt cygwin))
|
||||
(let ((attribs (file-attributes conn-file)))
|
||||
;; `file-attributes' can potentially return nil, in this case
|
||||
;; just assume it has read the connection file so that we can
|
||||
;; know for sure it is not connected if it fails to respond to
|
||||
;; any messages we send it.
|
||||
(or (null attribs)
|
||||
(not (equal atime (nth 4 attribs)))))))
|
||||
(jupyter--gc-kernel-processes)
|
||||
(push (list process conn-file) jupyter--kernel-processes)
|
||||
process))
|
||||
|
||||
(cl-defmethod jupyter-launch :before ((kernel jupyter-kernel-process))
|
||||
"Ensure KERNEL has a non-nil SESSION slot.
|
||||
A `jupyter-session' with random port numbers for the channels and
|
||||
a newly generated message signing key will be set as the value of
|
||||
KERNEL's SESSION slot if it is nil."
|
||||
(pcase-let (((cl-struct jupyter-kernel-process session) kernel))
|
||||
(unless session
|
||||
(setf (jupyter-kernel-session kernel) (jupyter-session-with-random-ports))
|
||||
;; This is here for stability when running the tests. Sometimes
|
||||
;; the kernel ports are not set up fast enough due to the hack
|
||||
;; done in `jupyter-session-with-random-ports'. The effect
|
||||
;; seems to be messages that are sent but never received by the
|
||||
;; kernel.
|
||||
(sit-for 0.2))))
|
||||
|
||||
(cl-defmethod jupyter-launch ((kernel jupyter-kernel-process))
|
||||
"Start KERNEL's process.
|
||||
Do nothing if KERNEL's process is already live.
|
||||
|
||||
The process arguments are constructed from KERNEL's SPEC. The
|
||||
connection file passed as argument to the process is first
|
||||
written to file, its contents are generated from KERNEL's SESSION
|
||||
slot.
|
||||
|
||||
See also https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs"
|
||||
(let ((process (jupyter-process kernel)))
|
||||
(unless (process-live-p process)
|
||||
(pcase-let (((cl-struct jupyter-kernel-process spec session) kernel))
|
||||
(let ((conn-file (jupyter-write-connection-file session)))
|
||||
(setq process (jupyter--start-kernel-process
|
||||
(jupyter-kernel-name kernel) spec
|
||||
conn-file))
|
||||
;; Make local tunnels to the remote ports when connecting to
|
||||
;; remote kernels. Update the session object to reflect
|
||||
;; these changes.
|
||||
(when (file-remote-p conn-file)
|
||||
(setf (jupyter-kernel-session kernel)
|
||||
(let ((conn-info (jupyter-tunnel-connection conn-file)))
|
||||
(jupyter-session
|
||||
:conn-info conn-info
|
||||
:key (plist-get conn-info :key)))))))
|
||||
(setf (process-get process :kernel) kernel)
|
||||
(setf (process-sentinel process)
|
||||
(lambda (process _)
|
||||
(pcase (process-status process)
|
||||
('signal
|
||||
(let ((kernel (process-get process :kernel)))
|
||||
(when kernel
|
||||
(warn "Kernel died unexpectedly")
|
||||
(jupyter-shutdown kernel)))))))))
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-shutdown ((kernel jupyter-kernel-process))
|
||||
"Shutdown KERNEL by killing its process unconditionally."
|
||||
(let ((process (jupyter-process kernel)))
|
||||
(when process
|
||||
(setf (process-get process :kernel) nil)
|
||||
(delete-process process))
|
||||
(cl-call-next-method)))
|
||||
|
||||
(cl-defmethod jupyter-restart ((_kernel jupyter-kernel-process))
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-interrupt ((kernel jupyter-kernel-process))
|
||||
"Interrupt KERNEL's process.
|
||||
The process can be interrupted when the interrupt mode of
|
||||
KERNEL's spec. is \"signal\" or not specified.
|
||||
|
||||
See also https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs"
|
||||
(pcase-let* ((process (jupyter-process kernel))
|
||||
((cl-struct jupyter-kernel-process spec) kernel)
|
||||
((cl-struct jupyter-kernelspec plist) spec)
|
||||
(imode (plist-get plist :interrupt_mode)))
|
||||
(cond
|
||||
((or (null imode) (string= imode "signal"))
|
||||
(when (process-live-p process)
|
||||
(interrupt-process process t)))
|
||||
((string= imode "message")
|
||||
(error "Send an interrupt_request using a client"))
|
||||
(t (cl-call-next-method)))))
|
||||
|
||||
(provide 'jupyter-kernel-process)
|
||||
|
||||
;;; jupyter-kernel-process.el ends here
|
||||
|
||||
|
||||
130
lisp/jupyter/jupyter-kernel.el
Normal file
130
lisp/jupyter/jupyter-kernel.el
Normal file
@@ -0,0 +1,130 @@
|
||||
;;; jupyter-kernel.el --- Kernels -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2020-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 21 Apr 2020
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Working with Jupyter kernels. This file contains the functions
|
||||
;; used to control the lifetime of a kernel and how clients can
|
||||
;; connect to launched kernels.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-base)
|
||||
(require 'jupyter-monads)
|
||||
(require 'jupyter-kernelspec)
|
||||
|
||||
(defgroup jupyter-kernel nil
|
||||
"Kernels"
|
||||
:group 'jupyter)
|
||||
|
||||
;;; Kernel definition
|
||||
|
||||
(cl-defstruct jupyter-kernel
|
||||
"A Jupyter kernel."
|
||||
(spec (make-jupyter-kernelspec)
|
||||
:type jupyter-kernelspec
|
||||
:documentation "The kernelspec of this kernel.")
|
||||
;; FIXME: Remove this slot, used by `jupyter-widget-client'.
|
||||
(session nil :type jupyter-session))
|
||||
|
||||
(cl-defmethod jupyter-alive-p ((kernel jupyter-kernel))
|
||||
"Return non-nil if KERNEL has been launched."
|
||||
(and (jupyter-kernel-session kernel) t))
|
||||
|
||||
(cl-defmethod cl-print-object ((kernel jupyter-kernel) stream)
|
||||
(princ (format "#<jupyter-kernel %s%s>"
|
||||
(jupyter-kernelspec-name (jupyter-kernel-spec kernel))
|
||||
(if (jupyter-alive-p kernel)
|
||||
(concat " " (truncate-string-to-width
|
||||
(jupyter-session-id (jupyter-kernel-session kernel))
|
||||
9 nil nil "…"))
|
||||
""))
|
||||
stream))
|
||||
|
||||
(cl-defgeneric jupyter-kernel (&rest args)
|
||||
"Return a kernel constructed from ARGS.
|
||||
|
||||
This method can be extended with extra primary methods for the
|
||||
purposes of handling different forms of ARGS."
|
||||
(let ((server (plist-get args :server))
|
||||
(conn-info (plist-get args :conn-info))
|
||||
(spec (plist-get args :spec)))
|
||||
(cond
|
||||
(server
|
||||
(require 'jupyter-server-kernel)
|
||||
(apply #'jupyter-kernel args))
|
||||
((or conn-info spec)
|
||||
(require 'jupyter-kernel-process)
|
||||
(apply #'jupyter-kernel args))
|
||||
(t (cl-call-next-method)))))
|
||||
|
||||
;;; Kernel management
|
||||
|
||||
(defun jupyter-kernel-name (kernel)
|
||||
(jupyter-kernelspec-name
|
||||
(jupyter-kernel-spec kernel)))
|
||||
|
||||
(cl-defmethod jupyter-launch ((kernel jupyter-kernel))
|
||||
"Launch KERNEL."
|
||||
(cl-assert (jupyter-alive-p kernel)))
|
||||
|
||||
(cl-defmethod jupyter-launch :before ((kernel jupyter-kernel))
|
||||
"Notify that the kernel launched."
|
||||
(message "Launching %s kernel..." (jupyter-kernel-name kernel)))
|
||||
|
||||
(cl-defmethod jupyter-launch :after ((kernel jupyter-kernel))
|
||||
"Notify that the kernel launched."
|
||||
(message "Launching %s kernel...done" (jupyter-kernel-name kernel)))
|
||||
|
||||
(cl-defmethod jupyter-shutdown ((kernel jupyter-kernel))
|
||||
"Shutdown KERNEL.
|
||||
Once a kernel has been shutdown it has no more connected clients
|
||||
and the process it represents no longer exists.
|
||||
|
||||
The default implementation of this method disconnects all
|
||||
connected clients of KERNEL and sets KERNEL's session slot to
|
||||
nil."
|
||||
(setf (jupyter-kernel-session kernel) nil))
|
||||
|
||||
(cl-defmethod jupyter-shutdown :before ((kernel jupyter-kernel))
|
||||
"Notify that the kernel will be shutdown."
|
||||
(message "%s kernel shutdown..." (jupyter-kernel-name kernel)))
|
||||
|
||||
(cl-defmethod jupyter-shutdown :after ((kernel jupyter-kernel))
|
||||
"Notify that the kernel launched."
|
||||
(message "%s kernel shutdown...done" (jupyter-kernel-name kernel)))
|
||||
|
||||
(cl-defmethod jupyter-restart ((kernel jupyter-kernel))
|
||||
"Restart KERNEL.
|
||||
|
||||
The default implementation shuts down and then re-launches
|
||||
KERNEL."
|
||||
(jupyter-shutdown kernel)
|
||||
(jupyter-launch kernel))
|
||||
|
||||
(cl-defmethod jupyter-interrupt ((_kernel jupyter-kernel))
|
||||
"Interrupt KERNEL."
|
||||
(ignore))
|
||||
|
||||
(provide 'jupyter-kernel)
|
||||
|
||||
;;; jupyter-kernel.el ends here
|
||||
273
lisp/jupyter/jupyter-kernelspec.el
Normal file
273
lisp/jupyter/jupyter-kernelspec.el
Normal file
@@ -0,0 +1,273 @@
|
||||
;;; jupyter-kernelspec.el --- Jupyter kernelspecs -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 17 Jan 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Functions to work with kernelspecs found by the shell command
|
||||
;;
|
||||
;; jupyter kernelspec list
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'json)
|
||||
(require 'jupyter-env)
|
||||
|
||||
(defgroup jupyter-kernelspec nil
|
||||
"Jupyter kernelspecs"
|
||||
:group 'jupyter)
|
||||
|
||||
(declare-function jupyter-read-plist "jupyter-base" (file))
|
||||
(declare-function jupyter-read-plist-from-string "jupyter-base" (file))
|
||||
|
||||
(cl-defstruct jupyter-kernelspec
|
||||
(name "python"
|
||||
:type string
|
||||
:documentation "The name of the kernelspec."
|
||||
:read-only t)
|
||||
(plist nil
|
||||
:type list
|
||||
:documentation "The kernelspec as a property list."
|
||||
:read-only t)
|
||||
(resource-directory nil
|
||||
:type (or null string)
|
||||
:documentation "The resource directory."
|
||||
:read-only t))
|
||||
|
||||
(defvar jupyter--kernelspecs (make-hash-table :test #'equal :size 5)
|
||||
"A hash table mapping hosts to the kernelspecs available on them.
|
||||
The top level hash-table maps hosts to nested hash-tables keyed
|
||||
on virtual environment path or nil for a system-wide Jupyter
|
||||
install: hosts[hash-table] -> venv[hash-table] -> kernelspecs.")
|
||||
|
||||
(defun jupyter-kernelspecs-ensure-cache (host)
|
||||
"Return, creating if necessary, the hash-table for HOST."
|
||||
(let ((cache (gethash host jupyter--kernelspecs)))
|
||||
(if cache cache
|
||||
(puthash host (make-hash-table :test #'equal :size 5)
|
||||
jupyter--kernelspecs))))
|
||||
|
||||
(defun jupyter-kernelspecs-cache-put (host kernelspecs)
|
||||
"Cache KERNELSPECS available on HOST.
|
||||
This takes into account any currently active virtual
|
||||
environment."
|
||||
(let ((venv (getenv "VIRTUAL_ENV")))
|
||||
(let ((cache (jupyter-kernelspecs-ensure-cache host)))
|
||||
(puthash venv kernelspecs cache))))
|
||||
|
||||
(defun jupyter-kernelspecs-cache-get (host)
|
||||
"Retrieve cached KERNELSPECS available on HOST.
|
||||
This takes into account any currently active virtual
|
||||
environment."
|
||||
(let ((venv (getenv "VIRTUAL_ENV")))
|
||||
(let ((cache (jupyter-kernelspecs-ensure-cache host)))
|
||||
(gethash venv cache))))
|
||||
|
||||
(defun jupyter-available-kernelspecs (&optional refresh)
|
||||
"Return the available kernelspecs.
|
||||
Return a list of `jupyter-kernelspec's available on the host
|
||||
associated with the `default-directory'. If `default-directory'
|
||||
is a remote file name, return the list of available kernelspecs
|
||||
on the remote system. The kernelspecs on the local system are
|
||||
returned otherwise (taking into account any currently active
|
||||
virtual environment).
|
||||
|
||||
On any system, the list is formed by parsing the output of the
|
||||
shell command
|
||||
|
||||
jupyter kernelspec list --json
|
||||
|
||||
By default the available kernelspecs are cached. To force an
|
||||
update of the cached kernelspecs, give a non-nil value to
|
||||
REFRESH."
|
||||
(let* ((host (or (file-remote-p default-directory) "local"))
|
||||
(kernelspecs
|
||||
(or (and (not refresh) (jupyter-kernelspecs-cache-get host))
|
||||
(let ((specs
|
||||
(plist-get
|
||||
(let ((json (or (jupyter-command "kernelspec" "list"
|
||||
"--json" "--log-level" "ERROR")
|
||||
(error "\
|
||||
Can't obtain kernelspecs from jupyter shell command"))))
|
||||
(condition-case nil
|
||||
(jupyter-read-plist-from-string json)
|
||||
(error
|
||||
(error "\
|
||||
Jupyter kernelspecs couldn't be parsed from
|
||||
|
||||
jupyter kernelspec list --json
|
||||
|
||||
To investiagate further, run that command in a shell and examine
|
||||
why it isn't returning valid JSON."))))
|
||||
:kernelspecs)))
|
||||
(jupyter-kernelspecs-cache-put
|
||||
host
|
||||
(sort
|
||||
(cl-loop
|
||||
for (kname spec) on specs by #'cddr
|
||||
for name = (substring (symbol-name kname) 1)
|
||||
for dir = (plist-get spec :resource_dir)
|
||||
collect (make-jupyter-kernelspec
|
||||
:name name
|
||||
:resource-directory (concat
|
||||
(unless (string= host "local") host)
|
||||
dir)
|
||||
:plist (plist-get spec :spec)))
|
||||
(lambda (x y)
|
||||
(string< (jupyter-kernelspec-name x)
|
||||
(jupyter-kernelspec-name y)))))))))
|
||||
kernelspecs))
|
||||
|
||||
(cl-defgeneric jupyter-kernelspecs (host &optional refresh)
|
||||
"Return a list of kernelspecs on HOST.
|
||||
If REFRESH is non-nil, then refresh the list of cached
|
||||
kernelspecs first. Otherwise a cached version of the kernelspecs
|
||||
may be returned.")
|
||||
|
||||
(cl-defmethod jupyter-kernelspecs ((host string) &optional refresh)
|
||||
(let ((default-directory host))
|
||||
(jupyter-available-kernelspecs refresh)))
|
||||
|
||||
(cl-defmethod jupyter-do-refresh-kernelspecs ()
|
||||
(jupyter-kernelspecs default-directory 'refresh))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-refresh-kernelspecs ()
|
||||
"Refresh the list of available kernelspecs.
|
||||
Execute this command if the kernelspecs seen by Emacs is out of
|
||||
sync with those specified on your system or notebook server."
|
||||
(interactive)
|
||||
(message "Refreshing kernelspecs...")
|
||||
(jupyter-do-refresh-kernelspecs)
|
||||
(message "Refreshing kernelspecs...done"))
|
||||
|
||||
(defun jupyter-get-kernelspec (name &optional specs refresh)
|
||||
"Get the kernelspec for a kernel named NAME.
|
||||
If no kernelspec is found, return nil. Otherwise return the
|
||||
kernelspec for the kernel named NAME.
|
||||
|
||||
If SPECS is provided, it is a list of kernelspecs that will be
|
||||
searched. Otherwise the kernelspecs associated with the
|
||||
`default-directory' are used.
|
||||
|
||||
Optional argument REFRESH has the same meaning as in
|
||||
`jupyter-kernelspecs'."
|
||||
(cl-loop
|
||||
for kernelspec in (or specs (jupyter-kernelspecs default-directory refresh))
|
||||
thereis (when (string= (jupyter-kernelspec-name kernelspec) name)
|
||||
kernelspec)))
|
||||
|
||||
(defun jupyter-find-kernelspecs (re &optional specs refresh)
|
||||
"Find all specs of kernels that have names matching RE.
|
||||
RE is a regular expression use to match the name of a kernel.
|
||||
Return a list of `jupyter-kernelspec' objects.
|
||||
|
||||
If SPECS is non-nil search SPECS, otherwise search the
|
||||
kernelspecs associated with the `default-directory'.
|
||||
|
||||
Optional argument REFRESH has the same meaning as in
|
||||
`jupyter-kernelspecs'."
|
||||
(cl-remove-if-not
|
||||
(lambda (kernelspec)
|
||||
(string-match-p re (jupyter-kernelspec-name kernelspec)))
|
||||
(or specs (jupyter-kernelspecs default-directory refresh))))
|
||||
|
||||
(defun jupyter-guess-kernelspec (name &optional specs refresh)
|
||||
"Return the first kernelspec starting with NAME.
|
||||
Raise an error if no kernelspec could be found.
|
||||
|
||||
SPECS and REFRESH have the same meaning as in
|
||||
`jupyter-find-kernelspecs'."
|
||||
(or (car (jupyter-find-kernelspecs (format "^%s" name) specs refresh))
|
||||
(error "No valid kernelspec for kernel name (%s)" name)))
|
||||
|
||||
(defun jupyter-completing-read-kernelspec (&optional specs refresh)
|
||||
"Use `completing-read' to select a kernel and return its kernelspec.
|
||||
|
||||
SPECS is a list of kernelspecs that will be used for completion,
|
||||
if it is nil the kernelspecs associated with the
|
||||
`default-directory' will be used.
|
||||
|
||||
Optional argument REFRESH has the same meaning as in
|
||||
`jupyter-kernelspecs'."
|
||||
(let* ((specs (or specs (jupyter-kernelspecs default-directory refresh)))
|
||||
(display-names (if (null specs) (error "No kernelspecs available")
|
||||
(mapcar (lambda (k)
|
||||
(plist-get
|
||||
(jupyter-kernelspec-plist k)
|
||||
:display_name))
|
||||
specs)))
|
||||
(name (completing-read "kernel: " display-names nil t)))
|
||||
(when (equal name "")
|
||||
(error "No kernelspec selected"))
|
||||
(nth (- (length display-names)
|
||||
(length (member name display-names)))
|
||||
specs)))
|
||||
|
||||
(defun jupyter-expand-environment-variables (var)
|
||||
"Return VAR with all environment variables expanded.
|
||||
VAR is a string, if VAR contains a sequence of characters like
|
||||
${ENV_VAR}, substitute it with the value of ENV_VAR in
|
||||
`process-environment'."
|
||||
(let ((expanded "")
|
||||
(start 0))
|
||||
(while (string-match "\\${\\([^}]+\\)}" var start)
|
||||
(cl-callf concat expanded
|
||||
(substring var start (match-beginning 0))
|
||||
(getenv (match-string 1 var)))
|
||||
(setq start (match-end 0)))
|
||||
(cl-callf concat expanded (substring var start))))
|
||||
|
||||
(defun jupyter-process-environment (kernelspec)
|
||||
"Return a list of environment variables contained in KERNELSPEC.
|
||||
The list of environment variables have the same form as the
|
||||
entries in `process-environment'.
|
||||
|
||||
The environment variables returned are constructed from those in
|
||||
the :env key of KERNELSPEC's property list."
|
||||
(cl-loop
|
||||
with env = (plist-get (jupyter-kernelspec-plist kernelspec) :env)
|
||||
for (k v) on env by #'cddr
|
||||
collect (format "%s=%s" (cl-subseq (symbol-name k) 1)
|
||||
(jupyter-expand-environment-variables v))))
|
||||
|
||||
(defun jupyter-kernel-argv (kernelspec conn-file)
|
||||
"Return a list of process arguments contained in KERNELSPEC.
|
||||
The process arguments are the ones that should be passed to
|
||||
kernel processes launched using KERNELSPEC.
|
||||
|
||||
CONN-FILE is the file name of a connection file, containing the
|
||||
IP address and ports (among other things), a
|
||||
launched kernel should connect to."
|
||||
(cl-loop
|
||||
with argv = (plist-get (jupyter-kernelspec-plist kernelspec) :argv)
|
||||
for arg in (append argv nil)
|
||||
if (equal arg "{connection_file}")
|
||||
collect (file-local-name conn-file)
|
||||
else if (equal arg "{resource_dir}")
|
||||
collect (file-local-name
|
||||
(jupyter-kernelspec-resource-directory
|
||||
kernelspec))
|
||||
else collect arg))
|
||||
|
||||
(provide 'jupyter-kernelspec)
|
||||
|
||||
;;; jupyter-kernelspec.el ends here
|
||||
678
lisp/jupyter/jupyter-messages.el
Normal file
678
lisp/jupyter/jupyter-messages.el
Normal file
@@ -0,0 +1,678 @@
|
||||
;;; jupyter-messages.el --- Jupyter messages -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 08 Jan 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Routines to sign, encode, decode, send, and receive Jupyter messages.
|
||||
;; Messages are represented as property lists, the contents of a message should
|
||||
;; never be accessed directly since decoding of a message's contents is done on
|
||||
;; demand. You access the message contents through `jupyter-message-content',
|
||||
;; `jupyter-message-header', `jupyter-message-metadata', etc.
|
||||
;;
|
||||
;; There are convenience macros: `jupyter-with-message-content' and
|
||||
;; `jupyter-with-message-data'.
|
||||
;;
|
||||
;; There are many convenience functions: `jupyter-message-data',
|
||||
;; `jupyter-message-get', `jupyter-message-type',
|
||||
;; `jupyter-message-status-idle-p', etc.
|
||||
;;
|
||||
;; See the "Convenience functions and macros" section.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(eval-when-compile (require 'subr-x))
|
||||
(require 'jupyter-base)
|
||||
(require 'hmac-def)
|
||||
(require 'parse-time)
|
||||
(require 'json)
|
||||
|
||||
(declare-function jupyter-request "jupyter-monads" (type &rest content))
|
||||
(declare-function jupyter-verify-inhibited-handlers "jupyter-client")
|
||||
|
||||
(defgroup jupyter-messages nil
|
||||
"Jupyter messages"
|
||||
:group 'jupyter)
|
||||
|
||||
(defconst jupyter-message-delimiter "<IDS|MSG>"
|
||||
"The message delimiter required in the jupyter messaging protocol.")
|
||||
|
||||
(defconst jupyter--false :json-false
|
||||
"The symbol used to disambiguate nil from boolean false.")
|
||||
|
||||
(defconst jupyter--empty-dict (make-hash-table :size 1)
|
||||
"An empty hash table to disambiguate nil during encoding.
|
||||
Message parts that are nil, but should be encoded into an empty
|
||||
dictionary are set to this value so that they are encoded as
|
||||
dictionaries.")
|
||||
|
||||
;;; UUID
|
||||
|
||||
(defun jupyter-new-uuid ()
|
||||
"Return a version 4 UUID."
|
||||
(format "%04x%04x-%04x-%04x-%04x-%06x%06x"
|
||||
(cl-random 65536)
|
||||
(cl-random 65536)
|
||||
(cl-random 65536)
|
||||
;; https://tools.ietf.org/html/rfc4122
|
||||
(let ((r (cl-random 65536)))
|
||||
(if (= (byteorder) ?l)
|
||||
;; ?l = little-endian
|
||||
(logior (logand r 4095) 16384)
|
||||
;; big-endian
|
||||
(logior (logand r 65295) 64)))
|
||||
(let ((r (cl-random 65536)))
|
||||
(if (= (byteorder) ?l)
|
||||
(logior (logand r 49151) 32768)
|
||||
(logior (logand r 65471) 128)))
|
||||
(cl-random 16777216)
|
||||
(cl-random 16777216)))
|
||||
|
||||
;;; Signing messages
|
||||
|
||||
(defun jupyter-sha256 (object)
|
||||
"Return the SHA256 hash of OBJECT."
|
||||
(secure-hash 'sha256 object nil nil t))
|
||||
|
||||
(define-hmac-function jupyter-hmac-sha256 jupyter-sha256 64 32)
|
||||
|
||||
(cl-defun jupyter-sign-message (session parts &optional (signer #'jupyter-hmac-sha256))
|
||||
"Use SESSION to sign message PARTS.
|
||||
Return the signature of PARTS. PARTS should be in the order of a
|
||||
valid Jupyter message, see `jupyter-decode-message'. SIGNER is
|
||||
the message signing function and should take two arguments, the
|
||||
text to sign and the key used for signing. The default value
|
||||
signs messages using `jupyter-hmac-sha256'."
|
||||
(if (> (length (jupyter-session-key session)) 0)
|
||||
(cl-loop
|
||||
;; NOTE: Encoding to a unibyte representation due to an "Attempt to
|
||||
;; change byte length of a string" error.
|
||||
with key = (encode-coding-string
|
||||
(jupyter-session-key session) 'utf-8 t)
|
||||
with parts = (encode-coding-string
|
||||
(cl-loop for part in parts concat part)
|
||||
'utf-8 t)
|
||||
for byte across (funcall signer parts key)
|
||||
concat (format "%02x" byte))
|
||||
""))
|
||||
|
||||
(defun jupyter--split-identities (parts)
|
||||
"Extract the identities from a list of message PARTS.
|
||||
Return a cons cell (IDENTS . REST-PARTS)."
|
||||
(or (cl-loop
|
||||
for (part . rest-parts) on parts by #'cdr
|
||||
if (equal part jupyter-message-delimiter)
|
||||
return (cons idents rest-parts)
|
||||
else collect part into idents)
|
||||
(error "Message delimiter not in message list")))
|
||||
|
||||
(defun jupyter--message-header (session msg-type msg-id)
|
||||
"Return a message header.
|
||||
The `:session' key of the header will have its value set to
|
||||
SESSION's ID, and its `:msg_type' will be set to MSG-TYPE. MSG-ID
|
||||
will be set to the value of the `:msg_id' key. The other fields
|
||||
of the returned plist are `:version', `:username', and `:date'.
|
||||
They are all set to appropriate default values."
|
||||
(list
|
||||
:msg_id msg-id
|
||||
:msg_type msg-type
|
||||
:version jupyter-protocol-version
|
||||
:username user-login-name
|
||||
:session (jupyter-session-id session)
|
||||
:date (format-time-string "%FT%T.%6N%z" (current-time))))
|
||||
|
||||
;;; Encode/decoding messages
|
||||
|
||||
(defun jupyter--encode (part)
|
||||
"Encode PART into a JSON string.
|
||||
Take into account `jupyter-message-type' keywords by replacing
|
||||
them with their appropriate message type strings according to the
|
||||
Jupyter messaging spec. After encoding into a JSON
|
||||
representation, return the UTF-8 encoded string.
|
||||
|
||||
If PART is a string, return the UTF-8 encoded string without
|
||||
encoding into JSON first.
|
||||
|
||||
If PART is a list whose first element is the symbol,
|
||||
`message-part', then return the second element of the list if it
|
||||
is non-nil. If it is nil, then set the list's second element to
|
||||
the result of calling `jupyter--encode' on the third element and
|
||||
return the result."
|
||||
(let ((original (if (fboundp 'json--print)
|
||||
#'json--print
|
||||
#'json-encode)))
|
||||
(cl-letf (((symbol-function original)
|
||||
(apply-partially #'jupyter--json-encode
|
||||
(symbol-function original))))
|
||||
(encode-coding-string
|
||||
(cond
|
||||
((stringp part) part)
|
||||
(t (json-encode part)))
|
||||
'utf-8 t))))
|
||||
|
||||
(defun jupyter--json-encode (original object)
|
||||
(let (msg-type)
|
||||
(cond
|
||||
((eq (car-safe object) 'message-part)
|
||||
(cl-destructuring-bind (_ encoded-rep decoded-rep) object
|
||||
(or encoded-rep (setf (nth 1 object)
|
||||
(jupyter--json-encode original decoded-rep)))))
|
||||
((and (keywordp object)
|
||||
(setf msg-type (plist-get jupyter-message-types object)))
|
||||
(json-encode msg-type))
|
||||
((and (listp object)
|
||||
(= (length object) 4)
|
||||
(cl-every #'integerp object))
|
||||
(jupyter-encode-time object))
|
||||
(t (funcall original object)))))
|
||||
|
||||
(defun jupyter--decode (part)
|
||||
"Decode a message PART.
|
||||
|
||||
If PART is a list whose first element is the symbol,
|
||||
`message-part', then return the third element of the list if it
|
||||
is non-nil. If it is nil, then set the list's third element to
|
||||
the result of calling `jupyter--decode' on the second element and
|
||||
return the result.
|
||||
|
||||
Otherwise, if PART is a string decode it using UTF-8 encoding and
|
||||
read it as a JSON string. If it is not valid JSON, return the
|
||||
decoded string."
|
||||
(if (eq (car-safe part) 'message-part)
|
||||
(cl-destructuring-bind (_ encoded-rep decoded-rep) part
|
||||
(or decoded-rep (setf (nth 2 part) (jupyter--decode encoded-rep))))
|
||||
(let* ((json-object-type 'plist)
|
||||
(str (decode-coding-string part 'utf-8)))
|
||||
(condition-case nil
|
||||
(json-read-from-string str)
|
||||
;; If it can't be read as JSON, assume its just a regular
|
||||
;; string
|
||||
(json-unknown-keyword str)))))
|
||||
|
||||
(defun jupyter-decode-time (str)
|
||||
"Decode an ISO 8601 time STR into a time object.
|
||||
The returned object has the same form as the object returned by
|
||||
`current-time'."
|
||||
(unless (string-match-p "T[^.,Z+-]+" str)
|
||||
(setq str (concat str "T00:00:00")))
|
||||
(save-match-data
|
||||
(string-match "T[^.,Z+-]+\\([.,]\\([0-9]+\\)\\)" str)
|
||||
(let ((fraction (match-string 2 str)))
|
||||
(when fraction
|
||||
(setq str (replace-match "" nil nil str 1)))
|
||||
(nconc (parse-iso8601-time-string str)
|
||||
(if fraction
|
||||
(let* ((plen (- 6 (length fraction)))
|
||||
(pad (and (> plen 0) (expt 10 plen))))
|
||||
(list (if pad (* pad (string-to-number fraction))
|
||||
(string-to-number (substring fraction 0 6)))
|
||||
0))
|
||||
(list 0 0))))))
|
||||
|
||||
(defun jupyter-encode-time (time)
|
||||
"Encode TIME into an ISO 8601 time string."
|
||||
(format-time-string "%FT%T.%6N" time t))
|
||||
|
||||
(cl-defun jupyter-encode-raw-message (session
|
||||
type
|
||||
&rest plist
|
||||
&key
|
||||
content
|
||||
(msg-id (jupyter-new-uuid))
|
||||
parent-header
|
||||
metadata
|
||||
buffers
|
||||
&allow-other-keys)
|
||||
"Encode a message into a JSON string.
|
||||
Similar to `jupyter-encode-message', but returns the JSON encoded
|
||||
string instead of a list of the encoded parts.
|
||||
|
||||
PLIST is an extra property list added to the top level of the
|
||||
message before encoding."
|
||||
(declare (indent 2))
|
||||
(cl-check-type session jupyter-session)
|
||||
(cl-check-type metadata json-plist)
|
||||
(cl-check-type content json-plist)
|
||||
(cl-check-type parent-header json-plist)
|
||||
(cl-check-type buffers list)
|
||||
(or content (setq content jupyter--empty-dict))
|
||||
(or parent-header (setq parent-header jupyter--empty-dict))
|
||||
(or metadata (setq metadata jupyter--empty-dict))
|
||||
(or buffers (setq buffers []))
|
||||
(let (fplist)
|
||||
(while plist
|
||||
(cond
|
||||
((memq (car plist)
|
||||
'(:content :parent-header :metadata :buffers :msg-id))
|
||||
(pop plist)
|
||||
(pop plist))
|
||||
(t
|
||||
(push (prog1 (pop plist)
|
||||
(push (pop plist) fplist))
|
||||
fplist))))
|
||||
(jupyter--encode
|
||||
(cl-list*
|
||||
:parent_header parent-header
|
||||
:header (jupyter--message-header session type msg-id)
|
||||
:content content
|
||||
:metadata metadata
|
||||
:buffers buffers
|
||||
fplist))))
|
||||
|
||||
(cl-defun jupyter-encode-message (session
|
||||
type
|
||||
&key idents
|
||||
content
|
||||
(msg-id (jupyter-new-uuid))
|
||||
parent-header
|
||||
metadata
|
||||
buffers
|
||||
(signer #'jupyter-hmac-sha256))
|
||||
(declare (indent 2))
|
||||
(cl-check-type session jupyter-session)
|
||||
(cl-check-type metadata json-plist)
|
||||
(cl-check-type content json-plist)
|
||||
(cl-check-type parent-header json-plist)
|
||||
(cl-check-type buffers list)
|
||||
(or content (setq content jupyter--empty-dict))
|
||||
(or parent-header (setq parent-header jupyter--empty-dict))
|
||||
(or metadata (setq metadata jupyter--empty-dict))
|
||||
(and (stringp idents) (setq idents (list idents)))
|
||||
|
||||
(let ((parts (mapcar #'jupyter--encode
|
||||
(list (jupyter--message-header session type msg-id)
|
||||
parent-header
|
||||
metadata
|
||||
content))))
|
||||
(nconc (cl-list* msg-id idents)
|
||||
(cl-list* jupyter-message-delimiter
|
||||
(jupyter-sign-message session parts signer)
|
||||
parts)
|
||||
buffers)))
|
||||
|
||||
(cl-defun jupyter-decode-message (session parts &key (signer #'jupyter-hmac-sha256))
|
||||
"Use SESSION to decode message PARTS.
|
||||
PARTS should be a list of message parts in the order of a valid
|
||||
Jupyter message, i.e. a list of the form
|
||||
|
||||
(signature header parent-header metadata content buffers...)
|
||||
|
||||
If SESSION supports signing messages, then the signature
|
||||
resulting from the signing of (cdr PARTS) using SESSION should be
|
||||
equal to SIGNATURE. An error is thrown if it is not.
|
||||
|
||||
If SIGNER is non-nil it should be a function used to sign the
|
||||
message. Otherwise the default signing function is used, see
|
||||
`jupyter-sign-message'.
|
||||
|
||||
The returned plist has elements of the form
|
||||
|
||||
(message-part JSON PLIST)
|
||||
|
||||
for the keys `:header', `:parent-header', `:metadata', and
|
||||
`:content'. JSON is the JSON encoded string of the message part.
|
||||
For `:header' and `:parent-header', PLIST will be the decoded
|
||||
message PLIST for the part. The other message parts are decoded
|
||||
into property lists on demand, i.e. after a call to
|
||||
`jupyter-message-metadata' or `jupyter-message-content' PLIST
|
||||
will be decoded message part.
|
||||
|
||||
The binary buffers are left unchanged and will be the value of
|
||||
the `:buffers' key in the returned plist. Also, the message ID
|
||||
and type are available in the top level of the plist as `:msg_id'
|
||||
and `:msg_type'."
|
||||
(when (< (length parts) 5)
|
||||
(error "Malformed message. Minimum length of parts is 5"))
|
||||
(when (jupyter-session-key session)
|
||||
(let ((signature (car parts)))
|
||||
(when (= (length signature) 0)
|
||||
(error "Unsigned message"))
|
||||
;; TODO: digest_history
|
||||
;; https://github.com/jupyter/jupyter_client/blob/7a0278af7c1652ac32356d6f00ae29d24d78e61c/jupyter_client/session.py#L915
|
||||
(unless (string= (jupyter-sign-message session (cdr parts) signer) signature)
|
||||
(error "Invalid signature (%s) for parts %S" signature (cdr parts)))))
|
||||
(cl-destructuring-bind
|
||||
(header parent-header metadata content &rest buffers)
|
||||
(cdr parts)
|
||||
(let ((dheader (jupyter--decode header)))
|
||||
(list
|
||||
:header (list 'message-part header dheader)
|
||||
:msg_id (plist-get dheader :msg_id)
|
||||
:msg_type (plist-get dheader :msg_type)
|
||||
;; Also decode the parent header here since it is used quite often in
|
||||
;; the parent Emacs process
|
||||
:parent_header (list 'message-part parent-header
|
||||
(jupyter--decode parent-header))
|
||||
:metadata (list 'message-part metadata nil)
|
||||
:content (list 'message-part content nil)
|
||||
:buffers buffers))))
|
||||
|
||||
(defvar jupyter-inhibit-handlers)
|
||||
|
||||
(defmacro jupyter-with-client-handlers (handlers &rest body)
|
||||
"Evaluate BODY with `jupyter-inhibit-handlers' bound according to HANDLERS.
|
||||
HANDLERS has the inverted meaning of `jupyter-inhibit-handlers'."
|
||||
(declare (indent 1) (debug (form body)))
|
||||
(let ((h (make-symbol "handlers")))
|
||||
`(let* ((,h ,handlers)
|
||||
(jupyter-inhibit-handlers
|
||||
(pcase ,h
|
||||
('t nil)
|
||||
('nil t)
|
||||
(`(not . ,els) els)
|
||||
(_ (cons 'not ,h)))))
|
||||
(jupyter-verify-inhibited-handlers)
|
||||
,@body)))
|
||||
|
||||
;;; Control messages
|
||||
|
||||
(cl-defun jupyter-interrupt-request (&key (handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(jupyter-request "interrupt_request")))
|
||||
|
||||
;;; stdin messages
|
||||
|
||||
(cl-defun jupyter-input-reply (&key value (handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(cl-check-type value string)
|
||||
(jupyter-request "input_reply"
|
||||
:value value)))
|
||||
|
||||
;;; shell messages
|
||||
|
||||
(cl-defun jupyter-kernel-info-request (&key (handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(jupyter-request "kernel_info_request")))
|
||||
|
||||
(cl-defun jupyter-execute-request (&key code
|
||||
(silent nil)
|
||||
(store-history t)
|
||||
(user-expressions nil)
|
||||
(allow-stdin t)
|
||||
(stop-on-error nil)
|
||||
(handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(cl-check-type code string)
|
||||
(cl-check-type user-expressions json-plist)
|
||||
(jupyter-request "execute_request"
|
||||
:code code :silent (if silent t jupyter--false)
|
||||
:store_history (if store-history t jupyter--false)
|
||||
:user_expressions (or user-expressions jupyter--empty-dict)
|
||||
:allow_stdin (if allow-stdin t jupyter--false)
|
||||
:stop_on_error (if stop-on-error t jupyter--false))))
|
||||
|
||||
(cl-defun jupyter-inspect-request (&key code (pos 0) (detail 0)
|
||||
(handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(setq detail (or detail 0))
|
||||
(unless (member detail '(0 1))
|
||||
(error "Detail can only be 0 or 1 (%s)" detail))
|
||||
(when (markerp pos)
|
||||
(setq pos (marker-position pos)))
|
||||
(cl-check-type code string)
|
||||
(cl-check-type pos integer)
|
||||
(jupyter-request "inspect_request"
|
||||
:code code :cursor_pos pos :detail_level detail)))
|
||||
|
||||
(cl-defun jupyter-complete-request (&key code (pos 0) (handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(when (markerp pos)
|
||||
(setq pos (marker-position pos)))
|
||||
(cl-check-type code string)
|
||||
(cl-check-type pos integer)
|
||||
(jupyter-request "complete_request"
|
||||
:code code :cursor_pos pos)))
|
||||
|
||||
(cl-defun jupyter-history-request (&key
|
||||
output
|
||||
raw
|
||||
(hist-access-type "tail")
|
||||
session
|
||||
start
|
||||
stop
|
||||
(n 10)
|
||||
pattern
|
||||
unique
|
||||
(handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(unless (member hist-access-type '("range" "tail" "search"))
|
||||
(error "History access type can only be one of (range, tail, search)"))
|
||||
(apply #'jupyter-request "history_request"
|
||||
(append
|
||||
(list :output (if output t jupyter--false) :raw (if raw t jupyter--false)
|
||||
:hist_access_type hist-access-type)
|
||||
(cond
|
||||
((equal hist-access-type "range")
|
||||
(cl-check-type session integer)
|
||||
(cl-check-type start integer)
|
||||
(cl-check-type stop integer)
|
||||
(list :session session :start start :stop stop))
|
||||
((equal hist-access-type "tail")
|
||||
(cl-check-type n integer)
|
||||
(list :n n))
|
||||
((equal hist-access-type "search")
|
||||
(cl-check-type pattern string)
|
||||
(cl-check-type n integer)
|
||||
(list :pattern pattern :unique (if unique t jupyter--false) :n n)))))))
|
||||
|
||||
(cl-defun jupyter-is-complete-request (&key code (handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(cl-check-type code string)
|
||||
(jupyter-request "is_complete_request"
|
||||
:code code)))
|
||||
|
||||
(cl-defun jupyter-comm-info-request (&key (target-name "")
|
||||
(handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(cl-check-type target-name string)
|
||||
(jupyter-request "comm_info_request"
|
||||
:target_name target-name)))
|
||||
|
||||
(cl-defun jupyter-comm-open (&key id target-name data
|
||||
(handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(cl-check-type id string)
|
||||
(cl-check-type target-name string)
|
||||
(cl-check-type data json-plist)
|
||||
(jupyter-request "comm_open"
|
||||
:comm_id id :target_name target-name :data data)))
|
||||
|
||||
(cl-defun jupyter-comm-msg (&key id data (handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(cl-check-type id string)
|
||||
(cl-check-type data json-plist)
|
||||
(jupyter-request "comm_msg"
|
||||
:comm_id id :data data)))
|
||||
|
||||
(cl-defun jupyter-comm-close (&key id data (handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(cl-check-type id string)
|
||||
(cl-check-type data json-plist)
|
||||
(jupyter-request "comm_close"
|
||||
:comm_id id :data data)))
|
||||
|
||||
(cl-defun jupyter-shutdown-request (&key restart (handlers t))
|
||||
(jupyter-with-client-handlers handlers
|
||||
(jupyter-request "shutdown_request"
|
||||
:restart (if restart t jupyter--false))))
|
||||
|
||||
;;; Convenience functions and macros
|
||||
|
||||
(defmacro jupyter-with-message-content (msg keys &rest body)
|
||||
"For MSG, bind the corresponding KEYS of its contents then evaluate BODY.
|
||||
KEYS is a list of key names found in the
|
||||
`jupyter-message-content' of MSG. The values are bound to their
|
||||
key names while evaluating BODY.
|
||||
|
||||
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)))
|
||||
(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.
|
||||
VARLIST has a similar form to the VARLIST of a `let' binding
|
||||
except the `cadr' of each binding is a mimetype that will be
|
||||
passed to `jupyter-message-data'.
|
||||
|
||||
So to bind the :text/plain mimetype of MSG to a variable, res,
|
||||
you would do
|
||||
|
||||
(jupyter-with-message-data msg ((res text/plain))
|
||||
BODY)"
|
||||
(declare (indent 2) (debug (form (&rest (symbolp symbolp)) body)))
|
||||
(let* ((m (make-symbol "msg"))
|
||||
(vars
|
||||
(mapcar (lambda (el)
|
||||
(list (car el)
|
||||
`(jupyter-message-data
|
||||
,m ',(if (keywordp (cadr el)) (cadr el)
|
||||
(intern (concat ":" (symbol-name (cadr el))))))))
|
||||
varlist)))
|
||||
(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.
|
||||
If the value of KEY is a list whose first element is the symbol
|
||||
`message-part', then if the the third element of the list is nil
|
||||
set it to the result of calling `jupyter--decode' on the second
|
||||
element. If the third element is non-nil, return it. Otherwise
|
||||
return the value of KEY in MSG."
|
||||
`(let ((part (plist-get ,msg ,key)))
|
||||
(if (and (consp part) (eq (car part) 'message-part))
|
||||
(or (nth 2 part) (jupyter--decode part))
|
||||
part)))
|
||||
|
||||
(defun jupyter-message-header (msg)
|
||||
"Get the header of MSG."
|
||||
(jupyter--decode-message-part :header msg))
|
||||
|
||||
(defun jupyter-message-parent-header (msg)
|
||||
"Get the parent header of MSG."
|
||||
(jupyter--decode-message-part :parent_header msg))
|
||||
|
||||
(defun jupyter-message-metadata (msg)
|
||||
"Get the metadata key of MSG."
|
||||
(jupyter--decode-message-part :metadata msg))
|
||||
|
||||
(defun jupyter-message-content (msg)
|
||||
"Get the MSG contents."
|
||||
(jupyter--decode-message-part :content msg))
|
||||
|
||||
(defsubst jupyter-message-id (msg)
|
||||
"Get the ID of MSG."
|
||||
(or (plist-get msg :msg_id)
|
||||
(plist-get (jupyter-message-header msg) :msg_id)))
|
||||
|
||||
(defsubst jupyter-message-parent-id (msg)
|
||||
"Get the parent ID of MSG."
|
||||
(jupyter-message-id (jupyter-message-parent-header msg)))
|
||||
|
||||
(defsubst jupyter-message-type (msg)
|
||||
"Get the type of MSG."
|
||||
(or (plist-get msg :msg_type)
|
||||
(plist-get (jupyter-message-header msg) :msg_type)))
|
||||
|
||||
(defsubst jupyter-message-session (msg)
|
||||
"Get the session ID of MSG."
|
||||
(plist-get (jupyter-message-header msg) :session))
|
||||
|
||||
(defsubst jupyter-message-parent-type (msg)
|
||||
"Get the type of MSG's parent message."
|
||||
(jupyter-message-type (jupyter-message-parent-header msg)))
|
||||
|
||||
(defun jupyter-message-time (msg)
|
||||
"Get the MSG time.
|
||||
The returned time has the same form as returned by
|
||||
`current-time'."
|
||||
(let* ((header (jupyter-message-header msg))
|
||||
(date (plist-member header :data)))
|
||||
(when (stringp (car date))
|
||||
(setcar date (jupyter-decode-time (car date))))
|
||||
(car date)))
|
||||
|
||||
(defsubst jupyter-message-get (msg key)
|
||||
"Get the value in MSG's `jupyter-message-content' that corresponds to KEY."
|
||||
(plist-get (jupyter-message-content msg) key))
|
||||
|
||||
(defsubst jupyter-message-data (msg mimetype)
|
||||
"Get the message data for a specific mimetype.
|
||||
MSG should be a message with a `:data' field in its contents.
|
||||
MIMETYPE is should be a standard media mimetype
|
||||
keyword (`:text/plain', `:image/png', ...). If the messages data
|
||||
has a key corresponding to MIMETYPE, return the value. Otherwise
|
||||
return nil."
|
||||
(plist-get (jupyter-message-get msg :data) mimetype))
|
||||
|
||||
(defsubst jupyter-message-status-idle-p (msg)
|
||||
"Determine if MSG is a status: idle message."
|
||||
(and (string= (jupyter-message-type msg) "status")
|
||||
(string= (jupyter-message-get msg :execution_state) "idle")))
|
||||
|
||||
(defun jupyter-message-status-starting-p (msg)
|
||||
"Determine if MSG is a status: starting message."
|
||||
(and (string= (jupyter-message-type msg) "status")
|
||||
(string= (jupyter-message-get msg :execution_state) "starting")))
|
||||
|
||||
(provide 'jupyter-messages)
|
||||
|
||||
;;; jupyter-messages.el ends here
|
||||
671
lisp/jupyter/jupyter-mime.el
Normal file
671
lisp/jupyter/jupyter-mime.el
Normal file
@@ -0,0 +1,671 @@
|
||||
;;; jupyter-mime.el --- Insert mime types -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 09 Nov 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Routines for working with MIME types.
|
||||
;; Also adds the following methods which may be extended:
|
||||
;;
|
||||
;; - jupyter-markdown-follow-link
|
||||
;; - jupyter-insert
|
||||
;;
|
||||
;; For working with display IDs, currently rudimentary
|
||||
;;
|
||||
;; - jupyter-current-display
|
||||
;; - jupyter-beginning-of-display
|
||||
;; - jupyter-end-of-display
|
||||
;; - jupyter-next-display-with-id
|
||||
;; - jupyter-delete-current-display
|
||||
;; - jupyter-update-display
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-base)
|
||||
(require 'shr)
|
||||
(require 'ansi-color)
|
||||
|
||||
(declare-function jupyter-message-content "jupyter-messages" (msg))
|
||||
(declare-function org-format-latex "org" (prefix &optional beg end dir overlays msg forbuffer processing-type))
|
||||
(declare-function markdown-link-at-pos "ext:markdown-mode" (pos))
|
||||
(declare-function markdown-follow-link-at-point "ext:markdown-mode")
|
||||
|
||||
;;; User variables
|
||||
|
||||
(defcustom jupyter-image-max-width 0
|
||||
"Maximum width of images in REPL.
|
||||
Wider images are resized. Special value 0 means no limit."
|
||||
:type 'integer
|
||||
:group 'jupyter-repl)
|
||||
|
||||
;;; Implementation
|
||||
|
||||
(defvar-local jupyter-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-update-display'.")
|
||||
|
||||
;;; Macros
|
||||
|
||||
;; Taken from `eshell-handle-control-codes'
|
||||
(defun jupyter-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-with-control-code-handling (&rest body)
|
||||
"Handle control codes in any produced output generated by evaluating BODY.
|
||||
After BODY is evaluated, call `jupyter-handle-control-codes'
|
||||
on the region inserted by BODY."
|
||||
(let ((beg (make-symbol "beg"))
|
||||
(end (make-symbol "end")))
|
||||
`(jupyter-with-insertion-bounds
|
||||
,beg ,end (progn ,@body)
|
||||
;; Handle continuation from previous messages
|
||||
(when (eq (char-before ,beg) ?\r)
|
||||
(move-marker ,beg (1- ,beg)))
|
||||
(jupyter-handle-control-codes ,beg ,end))))
|
||||
|
||||
;;; Fontificiation routines
|
||||
|
||||
(defun jupyter-fontify-buffer-name (mode)
|
||||
"Return the buffer name for fontifying MODE."
|
||||
(format " *jupyter-fontify[%s]*" mode))
|
||||
|
||||
(defun jupyter-fontify-buffer (mode)
|
||||
"Return the buffer used to fontify text for MODE.
|
||||
Retrieve the buffer for MODE from `jupyter-fontify-buffers'.
|
||||
If no buffer for MODE exists, create a new one."
|
||||
(let ((buf (get-buffer-create (jupyter-fontify-buffer-name mode))))
|
||||
(with-current-buffer buf
|
||||
(unless (eq major-mode mode)
|
||||
(delay-mode-hooks (funcall mode))))
|
||||
buf))
|
||||
|
||||
(defun jupyter-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'."
|
||||
(let ((next beg) val)
|
||||
(while (/= beg end)
|
||||
(setq val (get-text-property beg 'face object)
|
||||
next (next-single-property-change beg 'face object end))
|
||||
(remove-text-properties beg next '(face) object)
|
||||
(put-text-property beg next 'font-lock-face (or val 'default) object)
|
||||
(setq beg next))))
|
||||
|
||||
(defun jupyter-add-font-lock-properties (start end &optional object use-face)
|
||||
"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 and the face text property is swapped for
|
||||
font-lock-face.
|
||||
|
||||
If USE-FACE is non-nil, do not replace the face text property
|
||||
with font-lock-face."
|
||||
(unless use-face
|
||||
(jupyter-fixup-font-lock-properties start end object))
|
||||
(add-text-properties start end '(fontified t font-lock-fontified t) object))
|
||||
|
||||
(defun jupyter-fontify-according-to-mode (mode str &optional use-face)
|
||||
"Fontify a string according to MODE.
|
||||
Return the fontified string. In addition to fontifying STR, if
|
||||
MODE has a non-default `fill-forward-paragraph-function', STR
|
||||
will be filled using `fill-region'.
|
||||
|
||||
If USE-FACE is non-nil, do not replace the face text property
|
||||
with font-lock-face in the returned string."
|
||||
(with-current-buffer (jupyter-fontify-buffer mode)
|
||||
(erase-buffer)
|
||||
(insert str)
|
||||
(font-lock-ensure)
|
||||
(jupyter-add-font-lock-properties (point-min) (point-max) nil use-face)
|
||||
(when (not (memq fill-forward-paragraph-function
|
||||
'(forward-paragraph)))
|
||||
(fill-region (point-min) (point-max) t 'nosqueeze))
|
||||
(buffer-string)))
|
||||
|
||||
(defun jupyter-fontify-region-according-to-mode (mode beg end)
|
||||
"Fontify a region according to MODE.
|
||||
Fontify the region between BEG and END in the current buffer
|
||||
according to MODE. This works by creating a new indirect buffer,
|
||||
enabling MODE in the new buffer, ensuring the region is font
|
||||
locked, adding required text properties, and finally re-enabling
|
||||
the `major-mode' that was current before the call to this
|
||||
function."
|
||||
(let ((restore-mode major-mode))
|
||||
(with-current-buffer
|
||||
(make-indirect-buffer
|
||||
(current-buffer) (generate-new-buffer-name
|
||||
(jupyter-fontify-buffer-name mode)))
|
||||
(unwind-protect
|
||||
(save-restriction
|
||||
(narrow-to-region beg end)
|
||||
(delay-mode-hooks (funcall mode))
|
||||
(font-lock-ensure)
|
||||
(jupyter-fixup-font-lock-properties beg end))
|
||||
(kill-buffer)))
|
||||
(funcall restore-mode)))
|
||||
|
||||
;;; Special handling of ANSI sequences
|
||||
|
||||
(defun jupyter-ansi-color-apply-on-region (begin end &optional face-prop)
|
||||
"`ansi-color-apply-on-region' with Jupyter specific modifications.
|
||||
In particular, does not delete escape sequences between BEGIN and
|
||||
END from the buffer. Instead, an invisible text property with a
|
||||
value of t is added to render the escape sequences invisible.
|
||||
Also, the `ansi-color-apply-face-function' is hard-coded to a
|
||||
custom function that prepends to the face property of the text
|
||||
and also sets the FACE-PROP to the prepended face, if FACE-PROP
|
||||
is nil it defaults to `font-lock-face'.
|
||||
|
||||
For convenience, a jupyter-invisible property is also added with
|
||||
a value of t. This is mainly for modes like `org-mode' which
|
||||
strip invisible properties during fontification. In such cases,
|
||||
the jupyter-invisible property can act as an alias to the
|
||||
invisible property by adding it to `char-property-alias-alist'."
|
||||
(cl-letf (((symbol-function #'delete-region)
|
||||
(lambda (beg end)
|
||||
(add-text-properties beg end '(invisible t jupyter-invisible t))))
|
||||
(ansi-color-apply-face-function
|
||||
(lambda (beg end face)
|
||||
(when face
|
||||
(setq face (list face))
|
||||
(font-lock-prepend-text-property beg end 'face face)
|
||||
(put-text-property beg end (or face-prop 'font-lock-face) face)))))
|
||||
(ansi-color-apply-on-region begin end)))
|
||||
|
||||
;;; `jupyter-insert' method
|
||||
|
||||
(cl-defgeneric jupyter-insert (_mime _data &optional _metadata)
|
||||
"Insert MIME data in the current buffer.
|
||||
Additions to this method should insert DATA assuming it has a
|
||||
mime type of MIME. If METADATA is non-nil, it will be a property
|
||||
list containing extra properties for inserting DATA such as
|
||||
:width and :height for image mime types.
|
||||
|
||||
If MIME is considered handled, but does not insert anything in
|
||||
the current buffer, return a non-nil value to indicate that MIME
|
||||
has been handled."
|
||||
(ignore))
|
||||
|
||||
(cl-defmethod jupyter-insert ((plist cons) &optional metadata)
|
||||
"Insert the content contained in PLIST.
|
||||
PLIST should be a property list that contains the key :data and
|
||||
optionally the key :metadata. The value of :data shall be another
|
||||
property list that contains MIME types as keys and their
|
||||
representations as values. Alternatively, PLIST can be a full
|
||||
message property list or be a property list that itself contains
|
||||
mimetypes.
|
||||
|
||||
For each MIME type in `jupyter-mime-types' call
|
||||
|
||||
(jupyter-insert MIME (plist-get data MIME) (plist-get metadata MIME))
|
||||
|
||||
until one of the invocations inserts text into the current
|
||||
buffer (tested by comparisons with `buffer-modified-tick') or
|
||||
returns a non-nil value. When either of these cases occur, return
|
||||
MIME.
|
||||
|
||||
Note on non-graphic displays, `jupyter-nongraphic-mime-types' is
|
||||
used instead of `jupyter-mime-types'.
|
||||
|
||||
When no valid mimetype is present, a warning is shown and nil is
|
||||
returned."
|
||||
(cl-assert plist json-plist)
|
||||
(let ((content (jupyter-normalize-data plist metadata)))
|
||||
(cond
|
||||
((let ((tick (buffer-modified-tick)))
|
||||
(jupyter-map-mime-bundle (if (display-graphic-p) jupyter-mime-types
|
||||
jupyter-nongraphic-mime-types)
|
||||
content
|
||||
(lambda (mime content)
|
||||
(and (or (jupyter-insert
|
||||
mime (plist-get content :data)
|
||||
(plist-get content :metadata))
|
||||
(/= tick (buffer-modified-tick)))
|
||||
mime)))))
|
||||
(t
|
||||
(prog1 nil
|
||||
(let ((warning
|
||||
(format "No valid mimetype found: %s"
|
||||
(cl-loop for (k _v) on (plist-get content :data)
|
||||
by #'cddr collect k))))
|
||||
(display-warning 'jupyter warning)))))))
|
||||
|
||||
;;; HTML
|
||||
|
||||
(defun jupyter--shr-put-image (spec alt &optional flags)
|
||||
"Identical to `shr-put-image', but ensure :ascent is 50.
|
||||
SPEC, ALT and FLAGS have the same meaning as in `shr-put-image'.
|
||||
The :ascent of an image is set to 50 so that the image center
|
||||
aligns on the current line."
|
||||
(let ((image (shr-put-image spec alt flags)))
|
||||
(prog1 image
|
||||
(when image
|
||||
;; Ensure we use an ascent of 50 so that the image center aligns with
|
||||
;; the output prompt of a REPL buffer.
|
||||
(setf (image-property image :ascent) 50)
|
||||
(force-window-update)))))
|
||||
|
||||
(defun jupyter-browse-url-in-temp-file (data)
|
||||
"Insert DATA into a temp file and call `browse-url-of-file' on it."
|
||||
(let* ((secs (time-to-seconds))
|
||||
;; Allow showing the same DATA, but only after a 10s period. This is
|
||||
;; so that the same data doesn't get displayed multiple times very
|
||||
;; quickly. See #121.
|
||||
(secs (- secs (cl-rem secs 10)))
|
||||
(hash (sha1 (concat data (format-time-string "%H%M%S" secs))))
|
||||
(file (expand-file-name
|
||||
(concat "emacs-jupyter-" hash ".html")
|
||||
temporary-file-directory)))
|
||||
(unless (file-exists-p file)
|
||||
(with-temp-file file (insert data))
|
||||
(browse-url-of-file file)
|
||||
;; Give the external browser time to open the tmp file before deleting it
|
||||
;; based on mm-display-external
|
||||
(run-at-time
|
||||
60 nil
|
||||
(lambda ()
|
||||
(ignore-errors (delete-file file)))))))
|
||||
|
||||
(defun jupyter--delete-script-tags (beg end)
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(narrow-to-region beg end)
|
||||
(goto-char beg)
|
||||
(while (re-search-forward "<script[^>]*>" nil t)
|
||||
(delete-region
|
||||
(match-beginning 0)
|
||||
(if (re-search-forward "</script>" nil t)
|
||||
(point)
|
||||
(point-max)))))))
|
||||
|
||||
(defun jupyter-insert-html (html)
|
||||
"Parse and insert the HTML string using `shr'."
|
||||
(jupyter-with-insertion-bounds
|
||||
beg end (insert html)
|
||||
;; TODO: We can't really do much about javascript so
|
||||
;; delete those regions instead of trying to parse
|
||||
;; them. Maybe just re-direct to a browser like with
|
||||
;; widgets?
|
||||
;; NOTE: Parsing takes a very long time when the text
|
||||
;; is > ~500000 characters.
|
||||
(jupyter--delete-script-tags beg end)
|
||||
(let ((shr-put-image-function #'jupyter--shr-put-image)
|
||||
;; Avoid issues with proportional fonts. Sometimes not all of the
|
||||
;; text is rendered using proportional fonts. See #52.
|
||||
(shr-use-fonts nil))
|
||||
(if (save-excursion
|
||||
(goto-char beg)
|
||||
(looking-at "<\\?xml"))
|
||||
;; Be strict about syntax when the html returned explicitly asks to
|
||||
;; be parsed as xml. `libxml-parse-html-region' converts camel cased
|
||||
;; tags/attributes such as viewBox to viewbox in the dom since html
|
||||
;; is case insensitive. See #4.
|
||||
(cl-letf (((symbol-function #'libxml-parse-html-region)
|
||||
#'libxml-parse-xml-region))
|
||||
(shr-render-region beg end))
|
||||
(shr-render-region beg end)))
|
||||
(jupyter-add-font-lock-properties beg end)))
|
||||
|
||||
;;; Markdown
|
||||
|
||||
(defvar markdown-hide-markup)
|
||||
(defvar markdown-enable-math)
|
||||
(defvar markdown-hide-urls)
|
||||
(defvar markdown-fontify-code-blocks-natively)
|
||||
(defvar markdown-mode-mouse-map)
|
||||
|
||||
(defvar jupyter-markdown-mouse-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map [return] 'jupyter-markdown-follow-link-at-point)
|
||||
(define-key map [follow-link] 'mouse-face)
|
||||
(define-key map [mouse-2] 'jupyter-markdown-follow-link-at-point)
|
||||
map)
|
||||
"Keymap when `point' is over a markdown link in the REPL buffer.")
|
||||
|
||||
(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))
|
||||
|
||||
(defun jupyter-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-insert-markdown (text)
|
||||
"Insert TEXT, fontifying it using `markdown-mode' first."
|
||||
(let ((beg (point)))
|
||||
(insert
|
||||
(let ((markdown-hide-markup t)
|
||||
(markdown-hide-urls t)
|
||||
(markdown-enable-math t)
|
||||
(markdown-fontify-code-blocks-natively t))
|
||||
(jupyter-fontify-according-to-mode 'markdown-mode text)))
|
||||
;; Update keymaps
|
||||
(let ((end (point)) next)
|
||||
(setq beg (next-single-property-change beg 'keymap nil end))
|
||||
(while (/= beg end)
|
||||
(setq next (next-single-property-change beg 'keymap nil end))
|
||||
(when (eq (get-text-property beg 'keymap) markdown-mode-mouse-map)
|
||||
(put-text-property beg next 'keymap jupyter-markdown-mouse-map))
|
||||
(setq beg next)))))
|
||||
|
||||
;;; LaTeX
|
||||
|
||||
(defvar org-format-latex-options)
|
||||
(defvar org-preview-latex-image-directory)
|
||||
(defvar org-babel-jupyter-resource-directory)
|
||||
(defvar org-preview-latex-default-process)
|
||||
|
||||
(defun jupyter-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)
|
||||
;; This is added to in `org-babel-jupyter-initiate-session-by-key'
|
||||
(kill-buffer-hook nil)
|
||||
(org-format-latex-options
|
||||
`(:foreground
|
||||
default
|
||||
:background default :scale 2.0
|
||||
:matchers ,(plist-get org-format-latex-options :matchers))))
|
||||
(jupyter-with-insertion-bounds
|
||||
beg end (insert tex)
|
||||
;; FIXME: Best way to cleanup these files? Just delete them by reading
|
||||
;; the image data and using that for the image instead?
|
||||
(org-format-latex
|
||||
"ltximg" beg end org-babel-jupyter-resource-directory
|
||||
'overlays nil 'forbuffer
|
||||
;; Use the default method for creating image files
|
||||
org-preview-latex-default-process)
|
||||
;; Avoid deleting the image overlays due to text property changes
|
||||
(dolist (o (overlays-in beg end))
|
||||
(when (eq (overlay-get o 'org-overlay-type)
|
||||
'org-latex-overlay)
|
||||
(overlay-put o 'modification-hooks nil)))
|
||||
(overlay-recenter end)
|
||||
(goto-char end))))
|
||||
|
||||
;;; Images
|
||||
|
||||
(defun jupyter-insert-image (data type &optional metadata)
|
||||
"Insert image DATA as TYPE in the current buffer.
|
||||
TYPE has the same meaning as in `create-image'. METADATA is a
|
||||
plist containing :width and :height keys that will be used as the
|
||||
width and height of the image."
|
||||
(cl-destructuring-bind (&key width height needs_background &allow-other-keys)
|
||||
metadata
|
||||
(let ((img (create-image
|
||||
data type 'data :width width :height height
|
||||
:max-width (when (> jupyter-image-max-width 0)
|
||||
jupyter-image-max-width)
|
||||
:mask (when needs_background
|
||||
'(heuristic t)))))
|
||||
(insert-image img))))
|
||||
|
||||
;;; Plain text
|
||||
|
||||
(defun jupyter-insert-ansi-coded-text (text)
|
||||
"Insert TEXT, converting ANSI color codes to font lock faces."
|
||||
(jupyter-with-insertion-bounds
|
||||
beg end (insert (ansi-color-apply text))
|
||||
(jupyter-fixup-font-lock-properties beg end)))
|
||||
|
||||
;;; `jupyter-insert' method additions
|
||||
|
||||
(cl-defmethod jupyter-insert ((_mime (eql :text/html)) data
|
||||
&optional _metadata)
|
||||
(if (not (functionp 'libxml-parse-html-region))
|
||||
(cl-call-next-method)
|
||||
(jupyter-insert-html data)
|
||||
(insert "\n")))
|
||||
|
||||
(cl-defmethod jupyter-insert ((_mime (eql :text/markdown)) data
|
||||
&context ((require 'markdown-mode nil t)
|
||||
(eql markdown-mode))
|
||||
&optional _metadata)
|
||||
(jupyter-insert-markdown data))
|
||||
|
||||
(cl-defmethod jupyter-insert ((_mime (eql :text/latex)) data
|
||||
&context ((require 'org nil t)
|
||||
(eql org))
|
||||
&optional _metadata)
|
||||
(jupyter-insert-latex data)
|
||||
(insert "\n"))
|
||||
|
||||
(cl-defmethod jupyter-insert ((_mime (eql :image/svg+xml)) data
|
||||
&context ((and (image-type-available-p 'svg) t)
|
||||
(eql t))
|
||||
&optional metadata)
|
||||
(jupyter-insert-image data 'svg metadata)
|
||||
(insert "\n"))
|
||||
|
||||
(cl-defmethod jupyter-insert ((_mime (eql :image/jpeg)) data
|
||||
&context ((and (image-type-available-p 'jpeg) t)
|
||||
(eql t))
|
||||
&optional metadata)
|
||||
(jupyter-insert-image (base64-decode-string data) 'jpeg metadata)
|
||||
(insert "\n"))
|
||||
|
||||
(cl-defmethod jupyter-insert ((_mime (eql :image/png)) data
|
||||
&context ((and (image-type-available-p 'png) t)
|
||||
(eql t))
|
||||
&optional metadata)
|
||||
(jupyter-insert-image (base64-decode-string data) 'png metadata)
|
||||
(insert "\n"))
|
||||
|
||||
(cl-defmethod jupyter-insert ((_mime (eql :text/plain)) data
|
||||
&optional _metadata)
|
||||
;; Reset the context so that it doesn't leak into DATA if DATA has
|
||||
;; no ANSI sequences.
|
||||
(setq ansi-color-context nil)
|
||||
(jupyter-insert-ansi-coded-text data)
|
||||
(insert "\n"))
|
||||
|
||||
;;; Insert with display IDs
|
||||
|
||||
(cl-defmethod jupyter-insert :before ((_display-id string) &rest _ignore)
|
||||
"Initialize `juptyer-display-ids'"
|
||||
;; FIXME: Set the local display ID hash table for the current buffer, or
|
||||
;; should display IDs be global? Then we would have to associate marker
|
||||
;; positions as well in this table.
|
||||
(unless jupyter-display-ids
|
||||
(setq jupyter-display-ids (make-hash-table
|
||||
:test #'equal
|
||||
:weakness 'value))))
|
||||
|
||||
(cl-defmethod jupyter-insert ((display-id string) data &optional metadata)
|
||||
"Associate DISPLAY-ID with DATA when inserting DATA.
|
||||
DATA and METADATA have the same meaning as in
|
||||
`jupyter-insert'.
|
||||
|
||||
The default implementation adds a jupyter-display text property
|
||||
to any inserted text and a jupyter-display-begin property to the
|
||||
first character.
|
||||
|
||||
Currently there is no support for associating a DISPLAY-ID if
|
||||
DATA is displayed as a widget."
|
||||
(jupyter-with-insertion-bounds
|
||||
beg end (jupyter-insert data metadata)
|
||||
;; Don't add display IDs to widgets since those are currently implemented
|
||||
;; using an external browser and not in the current buffer.
|
||||
(when (and (not (memq :application/vnd.jupyter.widget-view+json data))
|
||||
(< beg end))
|
||||
(let ((id (gethash display-id jupyter-display-ids)))
|
||||
(unless id
|
||||
(setq id (puthash display-id display-id jupyter-display-ids)))
|
||||
(put-text-property beg end 'jupyter-display id)
|
||||
(put-text-property beg (1+ beg) 'jupyter-display-begin t)))))
|
||||
|
||||
(cl-defgeneric jupyter-current-display ()
|
||||
"Return the display ID for the display at `point'.
|
||||
|
||||
The default implementation returns the jupyter-display text
|
||||
property at `point'."
|
||||
(get-text-property (point) 'jupyter-display))
|
||||
|
||||
(cl-defgeneric jupyter-beginning-of-display ()
|
||||
"Go to the beginning of the current Jupyter display.
|
||||
|
||||
The default implementation moves `point' to the position of the
|
||||
character with a jupyter-display-begin property. If `point' is
|
||||
already at a character with such a property, then `point' is
|
||||
returned."
|
||||
(if (get-text-property (point) 'jupyter-display-begin) (point)
|
||||
(goto-char
|
||||
(previous-single-property-change
|
||||
(point) 'jupyter-display-begin nil (point-min)))))
|
||||
|
||||
(cl-defgeneric jupyter-end-of-display ()
|
||||
"Go to the end of the current Jupyter display."
|
||||
(goto-char
|
||||
(min (next-single-property-change
|
||||
(point) 'jupyter-display nil (point-max))
|
||||
(next-single-property-change
|
||||
(min (1+ (point)) (point-max))
|
||||
'jupyter-display-begin nil (point-max)))))
|
||||
|
||||
(cl-defgeneric jupyter-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'.
|
||||
|
||||
The default implementation searches the current buffer for text
|
||||
with a jupyter-display text property matching ID."
|
||||
(or (and (bobp) (eq id (get-text-property (point) 'jupyter-display)))
|
||||
(let ((pos (next-single-property-change (point) 'jupyter-display-begin)))
|
||||
(while (and pos (not (eq (get-text-property pos 'jupyter-display) id)))
|
||||
(setq pos (next-single-property-change pos 'jupyter-display-begin)))
|
||||
(and pos (goto-char pos)))))
|
||||
|
||||
(cl-defgeneric jupyter-delete-current-display ()
|
||||
"Delete the current Jupyter display.
|
||||
|
||||
The default implementation checks if `point' has a non-nil
|
||||
jupyter-display text property, if so, it deletes the surrounding
|
||||
region around `point' containing that same jupyter-display
|
||||
property."
|
||||
(when (jupyter-current-display)
|
||||
(delete-region
|
||||
(save-excursion (jupyter-beginning-of-display) (point))
|
||||
(save-excursion (jupyter-end-of-display) (point)))))
|
||||
|
||||
(cl-defmethod jupyter-update-display ((display-id string) data &optional metadata)
|
||||
"Update the display with DISPLAY-ID using DATA.
|
||||
DATA and METADATA have the same meaning as in a `:display-data'
|
||||
message."
|
||||
(let ((id (and jupyter-display-ids
|
||||
(gethash display-id jupyter-display-ids))))
|
||||
(unless id
|
||||
(error "Display ID not found (%s)" display-id))
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(let (bounds)
|
||||
(while (jupyter-next-display-with-id id)
|
||||
(jupyter-delete-current-display)
|
||||
(jupyter-with-insertion-bounds
|
||||
beg end (if bounds (insert-buffer-substring
|
||||
(current-buffer) (car bounds) (cdr bounds))
|
||||
(jupyter-insert id data metadata))
|
||||
(unless bounds
|
||||
(setq bounds (cons (copy-marker beg) (copy-marker end))))
|
||||
(pulse-momentary-highlight-region beg end 'secondary-selection)))
|
||||
(when bounds
|
||||
(set-marker (car bounds) nil)
|
||||
(set-marker (cdr bounds) nil)))
|
||||
(when (= (point) (point-min))
|
||||
(error "No display matching id (%s)" id)))))
|
||||
|
||||
;;; Pandoc
|
||||
|
||||
(defun jupyter-pandoc-convert (from to from-string &optional callback)
|
||||
"Use pandoc to convert a string in FROM format to TO format.
|
||||
Starts a process and converts FROM-STRING, assumed to be in FROM
|
||||
format, to a string in TO format and returns the converted
|
||||
string.
|
||||
|
||||
If CALLBACK is specified, return the process object. When the
|
||||
process exits, call CALLBACK with zero arguments and with the
|
||||
buffer containing the converted string current."
|
||||
(cl-assert (executable-find "pandoc"))
|
||||
(let* ((process-connection-type nil)
|
||||
(proc (start-process
|
||||
"jupyter-pandoc"
|
||||
(generate-new-buffer " *jupyter-pandoc*")
|
||||
"pandoc" "-f" from "-t" to "--")))
|
||||
(set-process-sentinel
|
||||
proc (lambda (proc _)
|
||||
(when (memq (process-status proc) '(exit signal))
|
||||
(with-current-buffer (process-buffer proc)
|
||||
(funcall callback)
|
||||
(kill-buffer (process-buffer proc))))))
|
||||
(process-send-string proc from-string)
|
||||
(process-send-eof proc)
|
||||
(if callback proc
|
||||
(let ((to-string ""))
|
||||
(setq callback (lambda () (setq to-string (buffer-string))))
|
||||
(while (zerop (length to-string))
|
||||
(accept-process-output nil 1))
|
||||
to-string))))
|
||||
|
||||
(provide 'jupyter-mime)
|
||||
|
||||
;;; jupyter-mime.el ends here
|
||||
494
lisp/jupyter/jupyter-monads.el
Normal file
494
lisp/jupyter/jupyter-monads.el
Normal file
@@ -0,0 +1,494 @@
|
||||
;;; jupyter-monads.el --- Monadic Jupyter -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2020-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 11 May 2020
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; TODO: Generalize `jupyter-with-io' and `jupyter-do' for any monad,
|
||||
;; not just the I/O one.
|
||||
;;
|
||||
;; TODO: Allow pcase patterns in mlet*
|
||||
;;
|
||||
;; (jupyter-mlet* ((value (jupyter-server-kernel-io kernel)))
|
||||
;; (pcase-let ((`(,kernel-sub ,event-pub) value))
|
||||
;; ...))
|
||||
;;
|
||||
;; into
|
||||
;;
|
||||
;; (jupyter-mlet* ((`(,kernel-sub ,event-pub)
|
||||
;; (jupyter-server-kernel-io kernel)))
|
||||
;; ...)
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-base)
|
||||
(require 'thunk)
|
||||
|
||||
(declare-function jupyter-handle-message "jupyter-client")
|
||||
(declare-function jupyter-kernel-io "jupyter-client")
|
||||
(declare-function jupyter-generate-request "jupyter-client")
|
||||
(declare-function jupyter-wait-until-idle "jupyter-client" (req &optional timeout progress-msg))
|
||||
|
||||
(defgroup jupyter-monads nil
|
||||
"Monadic Jupyter"
|
||||
:group 'jupyter)
|
||||
|
||||
(defconst jupyter--return-nil (lambda (state) (cons nil state)))
|
||||
|
||||
(defun jupyter-return (value)
|
||||
"Return a monadic value wrapping VALUE."
|
||||
(declare (indent 0)
|
||||
(compiler-macro
|
||||
(lambda (exp)
|
||||
(cond
|
||||
((null value)
|
||||
'jupyter--return-nil)
|
||||
((if (atom value)
|
||||
(not (symbolp value))
|
||||
(eq (car value) 'quote))
|
||||
`(lambda (state) (cons ,value state)))
|
||||
(t exp)))))
|
||||
(lambda (state) (cons value state)))
|
||||
|
||||
(defun jupyter-get-state ()
|
||||
"Return a monadic valid whose unwrapped value is the current state."
|
||||
(lambda (state) (cons state state)))
|
||||
|
||||
(defun jupyter-put-state (value)
|
||||
"Return a monadic value that sets the current state to VALUE.
|
||||
The unwrapped value is nil."
|
||||
(lambda (_state) (cons nil value)))
|
||||
|
||||
(defun jupyter-bind (mvalue mfn)
|
||||
"Bind MVALUE to MFN."
|
||||
(declare (indent 1))
|
||||
(lambda (state)
|
||||
(pcase-let* ((`(,value . ,state) (funcall mvalue state)))
|
||||
(funcall (funcall mfn value) state))))
|
||||
|
||||
(defmacro jupyter-mlet* (varlist &rest body)
|
||||
"Bind the monadic values in VARLIST, evaluate BODY.
|
||||
Return the result of evaluating BODY. The result of evaluating
|
||||
BODY should be another monadic value."
|
||||
(declare (indent 1) (debug ((&rest (symbolp form)) body)))
|
||||
(if (null varlist)
|
||||
(if (zerop (length body)) 'jupyter--return-nil
|
||||
`(progn ,@body))
|
||||
(pcase-let ((`(,name ,mvalue) (car varlist)))
|
||||
`(jupyter-bind ,mvalue
|
||||
(lambda (,name)
|
||||
(jupyter-mlet* ,(cdr varlist)
|
||||
,@body))))))
|
||||
|
||||
(defmacro jupyter-do (&rest actions)
|
||||
"Return a monadic value that performs all actions in ACTIONS.
|
||||
The actions are evaluated in the order given. The result of the
|
||||
returned action is the result of the last action in ACTIONS."
|
||||
(declare (indent 0) (debug (body)))
|
||||
(if (zerop (length actions)) 'jupyter--return-nil
|
||||
(let ((result (make-symbol "result")))
|
||||
`(jupyter-mlet*
|
||||
,(cl-loop
|
||||
for action being the elements of actions using (index i)
|
||||
for sym = (if (= i (1- (length actions))) result '_)
|
||||
collect `(,sym ,action))
|
||||
(jupyter-return ,result)))))
|
||||
|
||||
(defun jupyter-run-with-state (state mvalue)
|
||||
"Pass STATE as the state to MVALUE, return the resulting value."
|
||||
(declare (indent 1))
|
||||
;; Discard the final state
|
||||
(car (funcall mvalue state)))
|
||||
|
||||
(defmacro jupyter-run-with-io (io &rest body)
|
||||
"Return the result of evaluating the I/O value BODY evaluates to.
|
||||
All I/O operations are done in the context of IO."
|
||||
(declare (indent 1) (debug (form body)))
|
||||
`(jupyter-run-with-state ,io (progn ,@body)))
|
||||
|
||||
(defmacro jupyter-run-with-client (client &rest body)
|
||||
"Return the result of evaluating the monadic value BODY evaluates to.
|
||||
The initial state given to the monadic value is CLIENT."
|
||||
(declare (indent 1) (debug (form body)))
|
||||
`(jupyter-run-with-state ,client (progn ,@body)))
|
||||
|
||||
(defmacro jupyter-with-io (io &rest body)
|
||||
"Return an I/O action evaluating BODY in IO's context.
|
||||
The result of the returned action is the result of the I/O action
|
||||
BODY evaluates to."
|
||||
(declare (indent 1) (debug (form body)))
|
||||
`(lambda (_)
|
||||
(jupyter-run-with-io ,io ,@body)))
|
||||
|
||||
;;; Publisher/subscriber
|
||||
|
||||
(define-error 'jupyter-subscribed-subscriber
|
||||
"A subscriber cannot be subscribed to.")
|
||||
|
||||
(defun jupyter-subscriber (sub-fn)
|
||||
"Return a subscriber evaluating SUB-FN on published content.
|
||||
SUB-FN should return the result of evaluating
|
||||
`jupyter-unsubscribe' if the subscriber's subscription should be
|
||||
canceled.
|
||||
|
||||
Ex. Unsubscribe after consuming one message
|
||||
|
||||
(jupyter-subscriber
|
||||
(lambda (value)
|
||||
(message \"The published content: %s\" value)
|
||||
(jupyter-unsubscribe)))
|
||||
|
||||
Used like this, where sub is the above subscriber:
|
||||
|
||||
(jupyter-run-with-io (jupyter-publisher)
|
||||
(jupyter-subscribe sub)
|
||||
(jupyter-publish (list \='topic \"today's news\")))"
|
||||
(declare (indent 0))
|
||||
(lambda (sub-content)
|
||||
(pcase sub-content
|
||||
(`(content ,content) (funcall sub-fn content))
|
||||
(`(subscribe ,_) (signal 'jupyter-subscribed-subscriber nil))
|
||||
(_ (error "Unhandled subscriber content: %s" sub-content)))))
|
||||
|
||||
(defun jupyter-content (value)
|
||||
"Arrange for VALUE to be sent to subscribers of a publisher."
|
||||
(list 'content value))
|
||||
|
||||
(defsubst jupyter-unsubscribe ()
|
||||
"Arrange for the current subscription to be canceled.
|
||||
A subscriber (or publisher with a subscription) can return the
|
||||
result of this function to cancel its subscription with the
|
||||
publisher providing content."
|
||||
(list 'unsubscribe))
|
||||
|
||||
(define-error 'jupyter-publisher-subscribers-had-errors
|
||||
"Publisher's subscribers had errors")
|
||||
|
||||
(defun jupyter-pseudo-bind-content (pub-fn content subs)
|
||||
"Apply PUB-FN on submitted CONTENT to produce published content.
|
||||
Call each subscriber in SUBS on the published content. Remove
|
||||
those subscribers that cancel their subscription.
|
||||
|
||||
When a subscriber signals an error it is noted and the remaining
|
||||
subscribers are processed. After processing all subscribers, a
|
||||
`jupyter-publisher-errors' error is raised with the data being
|
||||
the list of errors raised when calling subscribers. Note, when a
|
||||
subscriber errors, it remains in the list of subscribers."
|
||||
(pcase (funcall pub-fn content)
|
||||
((and `(content ,_) sub-content)
|
||||
;; NOTE: The first element of SUBS is ignored here so that the
|
||||
;; pointer to the subscriber list remains the same for each
|
||||
;; publisher, even when subscribers are being destructively
|
||||
;; removed.
|
||||
(let ((errors nil))
|
||||
(while (cadr subs)
|
||||
(condition-case err
|
||||
;; Publish subscriber content to subscribers
|
||||
(pcase (funcall (cadr subs) sub-content)
|
||||
;; Destructively remove the subscriber when it returns an
|
||||
;; unsubscribe value.
|
||||
('(unsubscribe) (setcdr subs (cddr subs)))
|
||||
(_ (pop subs)))
|
||||
(error
|
||||
;; Skip over any subscribers that raised an error.
|
||||
(pop subs)
|
||||
(push err errors))))
|
||||
;; Inform about the errors.
|
||||
(when errors
|
||||
(signal 'jupyter-publisher-subscribers-had-errors errors)))
|
||||
nil)
|
||||
;; Cancel a publisher's subscription to another publisher.
|
||||
('(unsubscribe) '(unsubscribe))
|
||||
(_ nil)))
|
||||
|
||||
(defun jupyter-publisher (&optional pub-fn)
|
||||
"Return a publisher function.
|
||||
A publisher function is a closure, function with a local scope,
|
||||
that maintains a list of subscribers and distributes the content
|
||||
that PUB-FN returns to each of them.
|
||||
|
||||
PUB-FN is a function that optionally returns content to
|
||||
publish (by returning the result of `jupyter-content' on a
|
||||
value). It's called when a value is submitted for publishing
|
||||
using `jupyter-publish', like this:
|
||||
|
||||
(let ((pub (jupyter-publisher
|
||||
(lambda (submitted-value)
|
||||
(message \"Publishing %s to subscribers\" submitted-value)
|
||||
(jupyter-content submitted-value)))))
|
||||
(jupyter-run-with-io pub
|
||||
(jupyter-publish (list 1 2 3))))
|
||||
|
||||
The default for PUB-FN is `jupyter-content'. See
|
||||
`jupyter-subscribe' for an example on how to subscribe to a
|
||||
publisher.
|
||||
|
||||
If no content is returned by PUB-FN, no content is sent to
|
||||
subscribers.
|
||||
|
||||
A publisher can also be a subscriber of another publisher. In
|
||||
this case, if PUB-FN returns the result of `jupyter-unsubscribe'
|
||||
its subscription is canceled.
|
||||
|
||||
Ex. Publish the value 1 regardless of what is given to PUB-FN.
|
||||
|
||||
(jupyter-publisher
|
||||
(lambda (_)
|
||||
(jupyter-content 1)))
|
||||
|
||||
Ex. Publish \='app if \='app is given to a publisher, nothing is sent
|
||||
to subscribers otherwise. In this case, a publisher is a
|
||||
filter of the value given to it for publishing.
|
||||
|
||||
(jupyter-publisher
|
||||
(lambda (value)
|
||||
(if (eq value \='app)
|
||||
(jupyter-content value))))"
|
||||
(declare (indent 0))
|
||||
(let ((subs (list 'subscribers))
|
||||
(pub-fn (or pub-fn #'jupyter-content)))
|
||||
;; A publisher value is either a value representing a subscriber
|
||||
;; or a value representing content to send to subscribers.
|
||||
(lambda (pub-value)
|
||||
(pcase (car-safe pub-value)
|
||||
('content (jupyter-pseudo-bind-content pub-fn (cadr pub-value) subs))
|
||||
('subscribe (cl-pushnew (cadr pub-value) (cdr subs)))
|
||||
(_ (error "Unhandled publisher content: %s" pub-value))))))
|
||||
|
||||
(defun jupyter-subscribe (sub)
|
||||
"Return an I/O action that subscribes SUB to published content.
|
||||
If a subscriber (or a publisher with a subscription to another
|
||||
publisher) returns the result of `jupyter-unsubscribe', its
|
||||
subscription is canceled.
|
||||
|
||||
Ex. Subscribe to a publisher and unsubscribe after receiving two
|
||||
messages.
|
||||
|
||||
(let* ((msgs \='())
|
||||
(pub (jupyter-publisher))
|
||||
(sub (jupyter-subscriber
|
||||
(lambda (n)
|
||||
(if (> n 2) (jupyter-unsubscribe)
|
||||
(push n msgs))))))
|
||||
(jupyter-run-with-io pub
|
||||
(jupyter-subscribe sub))
|
||||
(cl-loop
|
||||
for x in \='(1 2 3)
|
||||
do (jupyter-run-with-io pub
|
||||
(jupyter-publish x)))
|
||||
(reverse msgs)) ; => \='(1 2)"
|
||||
(declare (indent 0))
|
||||
(lambda (io)
|
||||
(funcall io (list 'subscribe sub))
|
||||
(cons nil io)))
|
||||
|
||||
(defun jupyter-publish (value)
|
||||
"Return an I/O action that submits VALUE to publish as content."
|
||||
(declare (indent 0))
|
||||
(lambda (io)
|
||||
(funcall io (jupyter-content value))
|
||||
(cons nil io)))
|
||||
|
||||
;;; Working with requests
|
||||
|
||||
(define-error 'jupyter-timeout-before-idle "Timeout before idle")
|
||||
|
||||
(defun jupyter-sent (dreq)
|
||||
(jupyter-mlet* ((client (jupyter-get-state))
|
||||
(req dreq))
|
||||
(let ((type (jupyter-request-type req)))
|
||||
(jupyter-run-with-io (jupyter-kernel-io client)
|
||||
(jupyter-do
|
||||
(jupyter-subscribe (jupyter-request-message-publisher req))
|
||||
(jupyter-publish
|
||||
(list 'send
|
||||
(jupyter-channel-from-request-type type)
|
||||
type
|
||||
(jupyter-request-content req)
|
||||
(jupyter-request-id req))))))
|
||||
(jupyter-return req)))
|
||||
|
||||
(defun jupyter-idle (dreq &optional timeout)
|
||||
"Wait until DREQ has become idle, return DREQ.
|
||||
Signal a `jupyter-timeout-before-idle' error if TIMEOUT seconds
|
||||
elapses and the request has not become idle yet."
|
||||
(jupyter-mlet* ((req (jupyter-sent dreq)))
|
||||
(or (jupyter-wait-until-idle req timeout)
|
||||
(signal 'jupyter-timeout-before-idle (list req)))
|
||||
(jupyter-return req)))
|
||||
|
||||
(defun jupyter-messages (dreq &optional timeout)
|
||||
"Return all the messages of REQ.
|
||||
TIMEOUT has the same meaning as in `jupyter-idle'."
|
||||
(jupyter-mlet* ((req (jupyter-idle dreq timeout)))
|
||||
(jupyter-return (jupyter-request-messages req))))
|
||||
|
||||
(defun jupyter-find-message (msg-type msgs)
|
||||
"Return a message whose type is MSG-TYPE in MSGS."
|
||||
(cl-find-if
|
||||
(lambda (msg)
|
||||
(let ((type (jupyter-message-type msg)))
|
||||
(string= type msg-type)))
|
||||
msgs))
|
||||
|
||||
(defun jupyter-reply (dreq &optional timeout)
|
||||
"Return the reply message of REQ.
|
||||
TIMEOUT has the same meaning as in `jupyter-idle'."
|
||||
(jupyter-mlet* ((msgs (jupyter-messages dreq timeout)))
|
||||
(jupyter-return
|
||||
(cl-find-if
|
||||
(lambda (msg)
|
||||
(let ((type (jupyter-message-type msg)))
|
||||
(string-suffix-p "_reply" type)))
|
||||
msgs))))
|
||||
|
||||
(defun jupyter-result (dreq &optional timeout)
|
||||
"Return the result message of REQ.
|
||||
TIMEOUT has the same meaning as in `jupyter-idle'."
|
||||
(jupyter-mlet* ((msgs (jupyter-messages dreq timeout)))
|
||||
(jupyter-return
|
||||
(cl-find-if
|
||||
(lambda (msg)
|
||||
(let ((type (jupyter-message-type msg)))
|
||||
(string-suffix-p "_result" type)))
|
||||
msgs))))
|
||||
|
||||
(defun jupyter-message-subscribed (dreq cbs)
|
||||
"Return an IO action that subscribes CBS to a request's message publisher.
|
||||
IO-REQ is an IO action that evaluates to a sent request. CBS is
|
||||
an alist mapping message types to callback functions like
|
||||
|
||||
`((\"execute_reply\" ,(lambda (msg) ...))
|
||||
...)
|
||||
|
||||
The returned IO action returns the sent request after subscribing
|
||||
the callbacks."
|
||||
(jupyter-mlet* ((req dreq))
|
||||
(jupyter-run-with-io
|
||||
(jupyter-request-message-publisher req)
|
||||
(jupyter-subscribe
|
||||
(jupyter-subscriber
|
||||
(lambda (msg)
|
||||
(when-let*
|
||||
((msg-type (jupyter-message-type msg))
|
||||
(fn (car (alist-get msg-type cbs nil nil #'string=))))
|
||||
(funcall fn msg))))))
|
||||
(jupyter-return req)))
|
||||
|
||||
;; When replaying messages, the request message publisher is already
|
||||
;; unsubscribed from any upstream publishers.
|
||||
(defun jupyter--debug-replay-requests ()
|
||||
(setq jupyter--debug-request-queue (nreverse jupyter--debug-request-queue))
|
||||
(while jupyter--debug-request-queue
|
||||
(pcase-let ((`(,client ,req) (pop jupyter--debug-request-queue)))
|
||||
(cl-loop
|
||||
for msg in (jupyter-request-messages req)
|
||||
do (condition-case nil
|
||||
(jupyter-handle-message
|
||||
client (plist-get msg :channel)
|
||||
(cl-list* :parent-request req msg))
|
||||
(error (setq jupyter--debug-request-queue
|
||||
(nreverse jupyter--debug-request-queue))))))))
|
||||
|
||||
;;; Request
|
||||
|
||||
(defun jupyter-message-publisher (req)
|
||||
(let ((id (jupyter-request-id req)))
|
||||
(jupyter-publisher
|
||||
(lambda (msg)
|
||||
(pcase (jupyter-message-type msg)
|
||||
;; Send what doesn't appear to be a message as is.
|
||||
((pred null) (jupyter-content msg))
|
||||
;; A status message after a request goes idle means there is
|
||||
;; a new request and there will, theoretically, be no more
|
||||
;; messages for the idle one.
|
||||
;;
|
||||
;; FIXME: Is that true? Figure out the difference between a
|
||||
;; status: busy and a status: idle message.
|
||||
((and type (guard (jupyter-request-idle-p req))
|
||||
(guard (string= type "status")))
|
||||
(jupyter-unsubscribe))
|
||||
;; TODO: `jupyter-message-parent-id' -> `jupyter-parent-id'
|
||||
;; and the like.
|
||||
((guard (string= id (jupyter-message-parent-id msg)))
|
||||
(setf (jupyter-request-last-message req) msg)
|
||||
(cl-callf nconc (jupyter-request-messages req) (list msg))
|
||||
(when (or (jupyter-message-status-idle-p msg)
|
||||
;; Jupyter protocol 5.1, IPython
|
||||
;; implementation 7.5.0 doesn't give
|
||||
;; status: busy or status: idle messages
|
||||
;; on kernel-info-requests. Whereas
|
||||
;; IPython implementation 6.5.0 does.
|
||||
;; Seen on Appveyor tests.
|
||||
;;
|
||||
;; TODO: May be related
|
||||
;; jupyter/notebook#3705 as the problem
|
||||
;; does happen after a kernel restart
|
||||
;; when testing.
|
||||
(string= (jupyter-message-type msg) "kernel_info_reply")
|
||||
;; No idle message is received after a
|
||||
;; shutdown reply so consider REQ as
|
||||
;; having received an idle message in
|
||||
;; this case.
|
||||
(string= (jupyter-message-type msg) "shutdown_reply"))
|
||||
(setf (jupyter-request-idle-p req) t))
|
||||
(jupyter-content
|
||||
(cl-list* :parent-request req msg))))))))
|
||||
|
||||
(defvar jupyter-inhibit-handlers)
|
||||
|
||||
(defun jupyter-request (type &rest content)
|
||||
"Return an IO action that sends a `jupyter-request'.
|
||||
TYPE is the message type of the message that CONTENT, a property
|
||||
list, represents."
|
||||
(declare (indent 1))
|
||||
(let ((ih jupyter-inhibit-handlers))
|
||||
(lambda (client)
|
||||
(let* ((req (jupyter-generate-request
|
||||
client
|
||||
:type type
|
||||
:content content
|
||||
:client client
|
||||
;; Anything sent to stdin is a reply not a request
|
||||
;; so consider the "request" completed.
|
||||
:idle-p (string= "stdin"
|
||||
(jupyter-channel-from-request-type type))
|
||||
:inhibited-handlers ih))
|
||||
(pub (jupyter-message-publisher req)))
|
||||
(setf (jupyter-request-message-publisher req) pub)
|
||||
(if (eq jupyter--debug 'message)
|
||||
(push (list client req) jupyter--debug-request-queue)
|
||||
(when (string= (jupyter-request-type req)
|
||||
"execute_request")
|
||||
(jupyter-server-mode-set-client client))
|
||||
(jupyter-run-with-io pub
|
||||
(jupyter-subscribe
|
||||
(jupyter-subscriber
|
||||
(lambda (msg)
|
||||
;; Only handle what looks to be a Jupyter message.
|
||||
(when (jupyter-message-type msg)
|
||||
(let ((channel (plist-get msg :channel)))
|
||||
(jupyter-handle-message client channel msg))))))))
|
||||
(cons req client)))))
|
||||
|
||||
(provide 'jupyter-monads)
|
||||
|
||||
;;; jupyter-monads.el ends here
|
||||
1899
lisp/jupyter/jupyter-org-client.el
Normal file
1899
lisp/jupyter/jupyter-org-client.el
Normal file
File diff suppressed because it is too large
Load Diff
686
lisp/jupyter/jupyter-org-extensions.el
Normal file
686
lisp/jupyter/jupyter-org-extensions.el
Normal file
@@ -0,0 +1,686 @@
|
||||
;;; jupyter-org-extensions.el --- Jupyter Org Extensions -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Carlos Garcia C. <carlos@binarycharly.com>
|
||||
;; Created: 01 March 2019
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Functions that extend the functionality of Org mode to interact with
|
||||
;; jupyter source blocks.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-kernelspec)
|
||||
(require 'jupyter-org-client)
|
||||
(eval-when-compile (require 'subr-x))
|
||||
|
||||
(declare-function org-babel-jupyter-initiate-session "ob-jupyter" (&optional session params))
|
||||
(declare-function org-babel-jupyter-session-initiated-p "ob-jupyter" (&optional session params))
|
||||
(declare-function org-babel-jupyter-src-block-session "ob-jupyter" ())
|
||||
(declare-function org-babel-jupyter-language-p "ob-jupyter" (lang))
|
||||
(declare-function org-in-src-block-p "org" (&optional inside))
|
||||
(declare-function org-narrow-to-subtree "org" ())
|
||||
(declare-function org-previous-line-empty-p "org" ())
|
||||
(declare-function org-fold-show-context "org-fold" (&optional key))
|
||||
(declare-function org-next-line-empty-p "org" ())
|
||||
(declare-function org-element-context "org-element" (&optional element))
|
||||
(declare-function org-element-type "org-element" (element))
|
||||
(declare-function org-element-property "org-element" (property element))
|
||||
(declare-function org-element-interpret-data "org-element" (data))
|
||||
(declare-function org-element-at-point "org-element" ())
|
||||
(declare-function org-element-put-property "org-element" (element property value))
|
||||
(declare-function outline-show-entry "outline" ())
|
||||
(declare-function avy-jump "ext:avy")
|
||||
(declare-function ivy-read "ext:ivy")
|
||||
|
||||
(defcustom jupyter-org-jump-to-block-context-lines 3
|
||||
"Number of lines to show when showing the context of a block.
|
||||
The function `jupyter-org-jump-to-block' uses these many lines from the
|
||||
beginning of a source block in a list."
|
||||
:group 'ob-jupyter
|
||||
:type 'integer)
|
||||
|
||||
(defun jupyter-org-closest-jupyter-language (&optional query)
|
||||
"Return the language of the closest Jupyter source block.
|
||||
If QUERY is non-nil, ask for a language to use instead. Asking
|
||||
for which language to use is also done if no Jupyter source
|
||||
blocks could be found in the buffer.
|
||||
|
||||
Distance is line based, not character based. Also, `point' is
|
||||
assumed to not be inside a source block."
|
||||
(org-save-outline-visibility nil
|
||||
(or (save-excursion
|
||||
(and (null query)
|
||||
(cl-loop
|
||||
with start = (line-number-at-pos)
|
||||
with previous = (ignore-errors
|
||||
(save-excursion
|
||||
(org-babel-previous-src-block)
|
||||
(point)))
|
||||
with next = (ignore-errors
|
||||
(save-excursion
|
||||
(org-babel-next-src-block)
|
||||
(point)))
|
||||
with maybe-return-lang =
|
||||
(lambda ()
|
||||
(let ((info (org-babel-get-src-block-info 'light)))
|
||||
(when (org-babel-jupyter-language-p (nth 0 info))
|
||||
(cl-return (nth 0 info)))))
|
||||
while (or previous next) do
|
||||
(cond
|
||||
((or
|
||||
;; Maybe return the previous Jupyter source block's language
|
||||
;; if it is closer to the start point than the next source
|
||||
;; block
|
||||
(and previous next (< (- start (line-number-at-pos previous))
|
||||
(- (line-number-at-pos next) start)))
|
||||
;; or when there is no next source block
|
||||
(and (null next) previous))
|
||||
(goto-char previous)
|
||||
(funcall maybe-return-lang)
|
||||
(setq previous (ignore-errors
|
||||
(org-babel-previous-src-block)
|
||||
(point))))
|
||||
(next
|
||||
(goto-char next)
|
||||
(funcall maybe-return-lang)
|
||||
(setq next (ignore-errors
|
||||
(org-babel-next-src-block)
|
||||
(point))))))))
|
||||
;; If all else fails, query for the language to use
|
||||
(let* ((kernelspec (jupyter-completing-read-kernelspec))
|
||||
(lang (plist-get
|
||||
(jupyter-kernelspec-plist kernelspec)
|
||||
:language)))
|
||||
(if (org-babel-jupyter-language-p lang) lang
|
||||
(format "jupyter-%s" lang))))))
|
||||
|
||||
(defun jupyter-org-between-block-end-and-result-p ()
|
||||
"If `point' is between a src-block and its result, return the result end.
|
||||
`point' is considered between a src-block and its result when the
|
||||
result begins where the src-block ends, i.e. when only whitespace
|
||||
separates the two."
|
||||
;; Move after a src block's results first if `point' is between a src
|
||||
;; block and it's results. Don't do this if the results are not directly
|
||||
;; after a src block, e.g. for named results that appear somewhere else.
|
||||
(save-excursion
|
||||
(let ((start (point)))
|
||||
(when-let* ((src (and (org-save-outline-visibility nil
|
||||
(ignore-errors (org-babel-previous-src-block)))
|
||||
(org-element-context)))
|
||||
(end (org-element-property :end src))
|
||||
(result-pos (org-babel-where-is-src-block-result)))
|
||||
(goto-char end)
|
||||
(skip-chars-backward " \n\t\r")
|
||||
(when (and (= result-pos end)
|
||||
(< (point) start result-pos))
|
||||
(goto-char result-pos)
|
||||
(org-element-property :end (org-element-context)))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-insert-src-block (&optional below query)
|
||||
"Insert a src-block above `point'.
|
||||
With prefix arg BELOW, insert it below `point'.
|
||||
|
||||
If `point' is in a src-block use the language of the src-block and
|
||||
copy the header to the new block.
|
||||
|
||||
If QUERY is non-nil and `point' is not in a src-block, ask for
|
||||
the language to use for the new block. Otherwise try to select a
|
||||
language based on the src-block's near `point'."
|
||||
(interactive (list current-prefix-arg nil))
|
||||
(if (org-in-src-block-p)
|
||||
(let* ((src (org-element-context))
|
||||
(start (org-element-property :begin src))
|
||||
(end (org-element-property :end src))
|
||||
(lang (org-element-property :language src))
|
||||
(switches (org-element-property :switches src))
|
||||
(parameters (org-element-property :parameters src)))
|
||||
(if below
|
||||
(let ((location (progn
|
||||
(goto-char start)
|
||||
(org-babel-where-is-src-block-result))))
|
||||
(if (not location)
|
||||
(goto-char end)
|
||||
(goto-char location)
|
||||
(goto-char (org-element-property :end (org-element-context))))
|
||||
(unless (org-previous-line-empty-p)
|
||||
(insert "\n"))
|
||||
(insert
|
||||
(org-element-interpret-data
|
||||
(org-element-put-property
|
||||
(jupyter-org-src-block lang parameters "\n" switches)
|
||||
:post-blank 1)))
|
||||
(forward-line -3))
|
||||
;; after current block
|
||||
(goto-char (org-element-property :begin src))
|
||||
(unless (org-previous-line-empty-p)
|
||||
(insert "\n"))
|
||||
(insert
|
||||
(org-element-interpret-data
|
||||
(org-element-put-property
|
||||
(jupyter-org-src-block lang parameters "\n" switches)
|
||||
:post-blank 1)))
|
||||
(forward-line -3)))
|
||||
;; not in a src block, insert a new block, query for jupyter kernel
|
||||
(beginning-of-line)
|
||||
(let* ((lang (jupyter-org-closest-jupyter-language query))
|
||||
(src-block (jupyter-org-src-block lang nil "\n")))
|
||||
(when-let* ((pos (jupyter-org-between-block-end-and-result-p)))
|
||||
(goto-char pos)
|
||||
(skip-chars-backward " \n\t\r"))
|
||||
(unless (looking-at-p "^[\t ]*$")
|
||||
;; Move past the current element first
|
||||
(let ((elem (org-element-at-point)) parent)
|
||||
(while (and (setq parent (org-element-property :parent elem))
|
||||
(not (memq (org-element-type parent)
|
||||
'(inlinetask))))
|
||||
(setq elem parent))
|
||||
(when elem
|
||||
(goto-char (org-element-property
|
||||
(if below :end :begin) elem))))
|
||||
(cond
|
||||
(below
|
||||
(skip-chars-backward " \n\t\r")
|
||||
(insert "\n"))
|
||||
(t
|
||||
(insert "\n")
|
||||
(forward-line -1))))
|
||||
(unless (or (bobp) (org-previous-line-empty-p))
|
||||
(insert "\n"))
|
||||
(insert (string-trim-right (org-element-interpret-data src-block)))
|
||||
(unless (org-next-line-empty-p)
|
||||
(insert "\n"))
|
||||
(skip-chars-backward "\n")
|
||||
(forward-line -1))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-split-src-block (&optional below)
|
||||
"Split the current src block with point in upper block.
|
||||
|
||||
With a prefix BELOW move point to lower block."
|
||||
(interactive "P")
|
||||
(unless (org-in-src-block-p)
|
||||
(error "Not in a source block"))
|
||||
(beginning-of-line)
|
||||
(org-babel-demarcate-block)
|
||||
(if below
|
||||
(progn
|
||||
(org-babel-next-src-block)
|
||||
(forward-line)
|
||||
(open-line 1))
|
||||
(forward-line -2)
|
||||
(end-of-line)))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-execute-and-next-block (&optional new)
|
||||
"Execute his block and jump or add a new one.
|
||||
|
||||
If a new block is created, use the same language, switches and parameters.
|
||||
With prefix arg NEW, always insert new cell."
|
||||
(interactive "P")
|
||||
(unless (org-in-src-block-p)
|
||||
(error "Not in a source block"))
|
||||
(let ((next-src-block
|
||||
(save-excursion (ignore-errors (org-babel-next-src-block)))))
|
||||
;; instert a new block before executing the current block; otherwise, the new
|
||||
;; ... block gets added to the results of the next block (due to how
|
||||
;; ... jupyter works)
|
||||
(when (or new (not next-src-block))
|
||||
(save-excursion
|
||||
(jupyter-org-insert-src-block t)))
|
||||
(org-babel-execute-src-block)
|
||||
(org-babel-next-src-block)))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-execute-to-point (any)
|
||||
"Execute Jupyter source blocks that start before point.
|
||||
Only execute Jupyter source blocks that have the same session.
|
||||
Non-Jupyter source blocks are evaluated conditionally.
|
||||
|
||||
The session is selected in the following way:
|
||||
|
||||
* If `point' is at a Jupyter source block, use its session.
|
||||
|
||||
* If `point' is not at a Jupyter source block, examine the
|
||||
source blocks before `point' and ask the user to select a
|
||||
session if multiple exist. If there is only one session, use
|
||||
it without asking.
|
||||
|
||||
* Finally, if a session could not be found, then no Jupyter
|
||||
source blocks exist before `point'. In this case, no session
|
||||
is selected and all the source blocks before `point' will be
|
||||
evaluated, e.g. when all source blocks before `point' are
|
||||
shell source blocks.
|
||||
|
||||
NOTE: If a session could be selected, only Jupyter source blocks
|
||||
that have the same session are evaluated *without* evaluating any
|
||||
other source blocks. You can also evaluate ANY source block that
|
||||
doesn't have a Jupyter session by providing a prefix argument.
|
||||
This is useful, e.g. to evaluate shell source blocks along with
|
||||
Jupyter source blocks."
|
||||
(interactive "P")
|
||||
;; Use a marker here to account for buffer changes during evaluation of
|
||||
;; source blocks.
|
||||
(let* ((p (point-marker))
|
||||
(session
|
||||
(or (org-babel-jupyter-src-block-session)
|
||||
(let (this-session sessions)
|
||||
(catch 'done
|
||||
(org-babel-map-src-blocks nil
|
||||
(when (> (point) p)
|
||||
(throw 'done t))
|
||||
(when (and (setq this-session
|
||||
(org-babel-jupyter-src-block-session))
|
||||
(not (member this-session sessions)))
|
||||
(push this-session sessions))))
|
||||
(setq sessions (nreverse sessions))
|
||||
(if (> (length sessions) 1)
|
||||
(completing-read "Select session: " sessions)
|
||||
(car sessions))))))
|
||||
;; Move P after insertion at P
|
||||
(set-marker-insertion-type p t)
|
||||
(catch 'done
|
||||
(org-babel-map-src-blocks nil
|
||||
(when (> (point) p)
|
||||
(throw 'done t))
|
||||
;; If there is no SESSION that can be found, just evaluate any source
|
||||
;; block.
|
||||
;;
|
||||
;; If a Jupyter based SESSION could be found, only source blocks that
|
||||
;; have a Jupyter session matching SESSION are evaluated. When a source
|
||||
;; block doesn't have a Jupyter session, it is only evaluated when ANY
|
||||
;; is non-nil.
|
||||
(when (or (null session)
|
||||
(let ((this-session (org-babel-jupyter-src-block-session)))
|
||||
(if (null this-session) any
|
||||
(equal session this-session))))
|
||||
(org-babel-execute-src-block))))
|
||||
(goto-char p)
|
||||
(set-marker p nil)))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-execute-subtree (any)
|
||||
"Execute Jupyter source blocks that start before point in the current subtree.
|
||||
This function narrows the buffer to the current subtree and calls
|
||||
`jupyter-org-execute-to-point'. See that function for the meaning
|
||||
of the ANY argument."
|
||||
(interactive "P")
|
||||
(save-restriction
|
||||
(org-narrow-to-subtree)
|
||||
(jupyter-org-execute-to-point any)))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-next-busy-src-block (arg &optional backward)
|
||||
"Jump to the next busy source block.
|
||||
|
||||
With a prefix argument ARG, jump forward ARG many blocks.
|
||||
|
||||
When BACKWARD is non-nil, jump to the previous block."
|
||||
(interactive "p")
|
||||
(org-save-outline-visibility nil
|
||||
(cl-loop
|
||||
with count = (abs (or arg 1))
|
||||
with origin = (point)
|
||||
while (ignore-errors
|
||||
(if backward (org-babel-previous-src-block)
|
||||
(org-babel-next-src-block)))
|
||||
thereis (when (jupyter-org-request-at-point)
|
||||
(zerop (cl-decf count)))
|
||||
finally (goto-char origin)
|
||||
(user-error "No %s busy code blocks" (if backward "previous" "further"))))
|
||||
(save-match-data (org-fold-show-context)))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-previous-busy-src-block (arg)
|
||||
"Jump to the previous busy source block.
|
||||
|
||||
With a prefix argument ARG, jump backward ARG many source blocks."
|
||||
(interactive "p")
|
||||
(jupyter-org-next-busy-src-block arg 'backward))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-inspect-src-block ()
|
||||
"Inspect the symbol under point when in a source block."
|
||||
(interactive)
|
||||
(unless (jupyter-org-with-src-block-client
|
||||
(jupyter-inspect-at-point)
|
||||
t)
|
||||
(error "Not in a source block")))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-restart-kernel-execute-block ()
|
||||
"Restart the kernel of the source block where point is and execute it."
|
||||
(interactive)
|
||||
(jupyter-org-with-src-block-client
|
||||
(jupyter-repl-restart-kernel))
|
||||
(org-babel-execute-src-block-maybe))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-restart-and-execute-to-point (&optional any)
|
||||
"Kill the kernel and run all Jupyter src-blocks to point.
|
||||
With a prefix argument, run ANY source block that doesn't have a
|
||||
Jupyter session as well.
|
||||
|
||||
See `jupyter-org-execute-to-point' for more information on which
|
||||
source blocks are evaluated."
|
||||
(interactive "P")
|
||||
(jupyter-org-with-src-block-client
|
||||
(jupyter-repl-restart-kernel))
|
||||
(jupyter-org-execute-to-point any))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-restart-kernel-execute-buffer ()
|
||||
"Restart kernel and execute buffer."
|
||||
(interactive)
|
||||
(jupyter-org-with-src-block-client
|
||||
(jupyter-repl-restart-kernel))
|
||||
(org-babel-execute-buffer))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-jump-to-block (&optional context)
|
||||
"Jump to a source block in the buffer using `ivy'.
|
||||
If narrowing is in effect, jump to a block in the narrowed region.
|
||||
Use a numeric prefix CONTEXT to specify how many lines of context to showin the
|
||||
process of selecting a source block.
|
||||
Defaults to `jupyter-org-jump-to-block-context-lines'."
|
||||
(interactive
|
||||
(list (if current-prefix-arg
|
||||
(prefix-numeric-value current-prefix-arg)
|
||||
jupyter-org-jump-to-block-context-lines)))
|
||||
(unless (require 'ivy nil t)
|
||||
(error "Package `ivy' not installed"))
|
||||
(let ((blocks '()))
|
||||
(when (or (null context) (< context 1))
|
||||
(setq context jupyter-org-jump-to-block-context-lines))
|
||||
;; consider the #+SRC_BLOCK line of the block, thereby making CONTEXT
|
||||
;; ... equivalent to actual lines after the block header
|
||||
(setq context (1+ context))
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(while (re-search-forward org-babel-src-block-regexp nil t)
|
||||
(push (list (format "line %s:\n%s"
|
||||
(line-number-at-pos (match-beginning 0))
|
||||
(save-excursion
|
||||
(goto-char (match-beginning 0))
|
||||
(let ((s (point)))
|
||||
(forward-line context)
|
||||
(buffer-substring s (point)))))
|
||||
(line-number-at-pos (match-beginning 0)))
|
||||
blocks)))
|
||||
(ivy-read "block: " (reverse blocks)
|
||||
:action (lambda (candidate)
|
||||
(goto-char (point-min))
|
||||
(forward-line (1- (nth 1 candidate)))
|
||||
(ignore-errors (outline-show-entry))
|
||||
(recenter)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-jump-to-visible-block ()
|
||||
"Jump to a visible src block with avy."
|
||||
(interactive)
|
||||
(unless (require 'avy nil t)
|
||||
(error "Package `avy' not installed"))
|
||||
;; Jumping through these hoops to avoid depending on `avy'
|
||||
(defalias 'jupyter-org-jump-to-visible-block
|
||||
(byte-compile
|
||||
`(lambda ()
|
||||
(interactive)
|
||||
(avy-with #'jupyter-org-jump-to-block
|
||||
(avy-jump "#\\+begin_src"
|
||||
:beg (point-min)
|
||||
:end (point-max)))))
|
||||
(documentation 'jupyter-org-jump-to-visible-block))
|
||||
;; Now call the new definition
|
||||
(jupyter-org-jump-to-visible-block))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-edit-header ()
|
||||
"Edit the src-block header in the minibuffer."
|
||||
(interactive)
|
||||
(let ((src-info (org-babel-get-src-block-info 'light)))
|
||||
(unless src-info
|
||||
(error "Not in a source block"))
|
||||
(let* ((header-start (nth 5 src-info))
|
||||
(header-end (save-excursion (goto-char header-start)
|
||||
(line-end-position))))
|
||||
(let ((header (read-string "Header: "
|
||||
(buffer-substring header-start header-end))))
|
||||
(save-excursion
|
||||
(delete-region header-start header-end)
|
||||
(goto-char header-start)
|
||||
(insert header))))))
|
||||
|
||||
(defun jupyter-org-src-block-bounds ()
|
||||
"Return the region containing the current source block.
|
||||
If the source block has results, include the results in the
|
||||
returned region. The region is returned as (BEGIN . END)"
|
||||
(unless (org-in-src-block-p)
|
||||
(error "Not in a source block"))
|
||||
(let* ((src (org-element-context))
|
||||
(results-start (org-babel-where-is-src-block-result))
|
||||
(results-end
|
||||
(when results-start
|
||||
(save-excursion
|
||||
(goto-char results-start)
|
||||
(goto-char (org-babel-result-end))
|
||||
;; if results are empty, take its empy line
|
||||
(when (looking-at-p org-babel-result-regexp)
|
||||
(forward-line 1))
|
||||
(point)))))
|
||||
`(,(org-element-property :begin src) .
|
||||
,(or results-end (jupyter-org-element-end-before-blanks src)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-kill-block-and-results ()
|
||||
"Kill the block and its results."
|
||||
(interactive)
|
||||
(let ((region (jupyter-org-src-block-bounds)))
|
||||
(kill-region (car region) (cdr region))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-copy-block-and-results ()
|
||||
"Copy the src block at the current point and its results."
|
||||
(interactive)
|
||||
(let ((region (jupyter-org-src-block-bounds)))
|
||||
(kill-new (buffer-substring (car region) (cdr region)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-clone-block (&optional below)
|
||||
"Clone the block above the current block.
|
||||
|
||||
If BELOW is non-nil, add the cloned block below."
|
||||
(interactive "P")
|
||||
(let* ((src (org-element-context))
|
||||
(code (org-element-property :value src)))
|
||||
(unless (org-in-src-block-p)
|
||||
(error "Not in a source block"))
|
||||
(jupyter-org-insert-src-block below)
|
||||
(delete-char 1)
|
||||
(insert code)
|
||||
;; move to the end of the last line of the cloned block
|
||||
(forward-line -1)
|
||||
(end-of-line)))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-merge-blocks ()
|
||||
"Merge the current block with the next block."
|
||||
(interactive)
|
||||
(unless (org-in-src-block-p)
|
||||
(error "Not in a source block"))
|
||||
(let ((current-src-block (org-element-context)))
|
||||
(org-babel-remove-result)
|
||||
(org-babel-next-src-block)
|
||||
(let* ((next-src-block (prog1 (org-element-context)
|
||||
(org-babel-remove-result)))
|
||||
(next-src-block-beg (set-marker
|
||||
(make-marker)
|
||||
(org-element-property :begin next-src-block)))
|
||||
(next-src-block-end (set-marker
|
||||
(make-marker)
|
||||
(jupyter-org-element-end-before-blanks next-src-block))))
|
||||
(goto-char (jupyter-org-element-end-before-blanks current-src-block))
|
||||
(forward-line -1)
|
||||
(insert
|
||||
(delete-and-extract-region
|
||||
(save-excursion
|
||||
(goto-char (jupyter-org-element-begin-after-affiliated next-src-block))
|
||||
(forward-line 1)
|
||||
(point))
|
||||
(save-excursion
|
||||
(goto-char next-src-block-end)
|
||||
(forward-line -1)
|
||||
(point))))
|
||||
;; delete a leftover space
|
||||
(save-excursion
|
||||
(goto-char next-src-block-end)
|
||||
(when (looking-at-p "[[:space:]]*$")
|
||||
(set-marker next-src-block-end (+ (line-end-position) 1))))
|
||||
(delete-region next-src-block-beg next-src-block-end)
|
||||
(set-marker next-src-block-beg nil)
|
||||
(set-marker next-src-block-end nil)))
|
||||
;; move to the end of the last line
|
||||
(forward-line -1)
|
||||
(end-of-line))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-move-src-block (&optional below)
|
||||
"Move source block before of after another.
|
||||
|
||||
If BELOW is non-nil, move the block down, otherwise move it up."
|
||||
(interactive)
|
||||
(unless (org-in-src-block-p)
|
||||
(error "Not in a source block"))
|
||||
;; throw error if there's no previous or next source block
|
||||
(when (ignore-errors
|
||||
(save-excursion
|
||||
(if below
|
||||
(org-babel-next-src-block)
|
||||
(org-babel-previous-src-block))))
|
||||
(let* ((region (jupyter-org-src-block-bounds))
|
||||
(block (delete-and-extract-region (car region) (cdr region))))
|
||||
;; if there is an empty line remaining, take that line as part of the
|
||||
;; ... block
|
||||
(when (and (looking-at-p "[[:space:]]*$") (/= (point) (point-max)))
|
||||
(delete-region (line-beginning-position) (+ (line-end-position) 1))
|
||||
(setq block (concat block "\n")))
|
||||
(if below
|
||||
;; if below, move past the next source block or its result
|
||||
(let ((next-src-block-head (org-babel-where-is-src-block-head)))
|
||||
(if next-src-block-head
|
||||
(goto-char next-src-block-head)
|
||||
(org-babel-next-src-block))
|
||||
(let ((next-src-block (org-element-context))
|
||||
(next-results-start (org-babel-where-is-src-block-result)))
|
||||
(if (not next-results-start)
|
||||
(goto-char (org-element-property :end next-src-block))
|
||||
(goto-char next-results-start)
|
||||
(goto-char (org-babel-result-end))
|
||||
(when (and (looking-at-p org-babel-result-regexp)
|
||||
(/= (point) (point-max)))
|
||||
;; the results are empty, take the next empty line
|
||||
(forward-line 1))
|
||||
(when (looking-at-p "[[:space:]]*$")
|
||||
(forward-line 1)))))
|
||||
;; else, move to the begining of the previous block
|
||||
(org-babel-previous-src-block))
|
||||
;; keep cursor where the insertion takes place
|
||||
(save-excursion (insert block)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-clear-all-results ()
|
||||
"Clear all results in the buffer."
|
||||
(interactive)
|
||||
(org-save-outline-visibility nil
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(while (org-babel-next-src-block)
|
||||
(org-babel-remove-result)))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-org-interrupt-kernel ()
|
||||
"Interrupt the kernel."
|
||||
(interactive)
|
||||
(unless (org-in-src-block-p)
|
||||
(error "Not in a source block"))
|
||||
(jupyter-org-with-src-block-client
|
||||
(jupyter-repl-interrupt-kernel)))
|
||||
|
||||
(defun jupyter-org-hydra/body ()
|
||||
"Hack to bind a hydra only if the hydra package exists."
|
||||
(interactive)
|
||||
(unless (require 'hydra nil t)
|
||||
(error "Package `hydra' not installed"))
|
||||
;; unbinding this function and define the hydra
|
||||
(fmakunbound 'jupyter-org-hydra/body)
|
||||
(eval `(defhydra jupyter-org-hydra (:color blue :hint nil)
|
||||
"
|
||||
Execute Navigate Edit Misc
|
||||
-------------------------------------------------------------------------------------------
|
||||
_<return>_: current _p_: previous _C-p_: move up _/_: inspect
|
||||
_C-<return>_: current to next _P_: previous busy _C-n_: move down _l_: clear result
|
||||
_M-<return>_: to point _n_: next _x_: kill _L_: clear all
|
||||
_C-M-<return>_: subtree to point _N_: next busy _c_: copy _i_: interrupt
|
||||
_S-<return>_: Restart/block _g_: visible _o_: clone _C-s_: scratch buffer
|
||||
_S-C-<return>_: Restart/to point _G_: any _m_: merge
|
||||
_S-M-<return>_: Restart/buffer _<tab>_: (un)fold _s_: split
|
||||
_r_: Goto repl ^ ^ _+_: insert above
|
||||
^ ^ ^ ^ _=_: insert below
|
||||
^ ^ ^ ^ _h_: header"
|
||||
("<return>" org-ctrl-c-ctrl-c :color red)
|
||||
("C-<return>" jupyter-org-execute-and-next-block :color red)
|
||||
("M-<return>" jupyter-org-execute-to-point)
|
||||
("C-M-<return>" jupyter-org-execute-subtree)
|
||||
("S-<return>" jupyter-org-restart-kernel-execute-block)
|
||||
("S-C-<return>" jupyter-org-restart-and-execute-to-point)
|
||||
("S-M-<return>" jupyter-org-restart-kernel-execute-buffer)
|
||||
("r" org-babel-switch-to-session)
|
||||
|
||||
("p" org-babel-previous-src-block :color red)
|
||||
("P" jupyter-org-previous-busy-src-block :color red)
|
||||
("n" org-babel-next-src-block :color red)
|
||||
("N" jupyter-org-next-busy-src-block :color red)
|
||||
("g" jupyter-org-jump-to-visible-block)
|
||||
("G" jupyter-org-jump-to-block)
|
||||
("<tab>" org-cycle :color red)
|
||||
|
||||
("C-p" jupyter-org-move-src-block :color red)
|
||||
("C-n" (jupyter-org-move-src-block t) :color red)
|
||||
("x" jupyter-org-kill-block-and-results)
|
||||
("c" jupyter-org-copy-block-and-results)
|
||||
("o" (jupyter-org-clone-block t))
|
||||
("m" jupyter-org-merge-blocks)
|
||||
("s" jupyter-org-split-src-block)
|
||||
("+" (jupyter-org-insert-src-block nil current-prefix-arg))
|
||||
("=" (jupyter-org-insert-src-block t current-prefix-arg))
|
||||
("l" org-babel-remove-result)
|
||||
("L" jupyter-org-clear-all-results)
|
||||
("h" jupyter-org-edit-header)
|
||||
|
||||
("/" jupyter-org-inspect-src-block)
|
||||
("i" jupyter-org-interrupt-kernel)
|
||||
("C-s" org-babel-jupyter-scratch-buffer)))
|
||||
(call-interactively #'jupyter-org-hydra/body))
|
||||
|
||||
(define-key jupyter-org-interaction-mode-map (kbd "C-c h") #'jupyter-org-hydra/body)
|
||||
|
||||
(provide 'jupyter-org-extensions)
|
||||
|
||||
;;; jupyter-org-extensions.el ends here
|
||||
17
lisp/jupyter/jupyter-pkg.el
Normal file
17
lisp/jupyter/jupyter-pkg.el
Normal file
@@ -0,0 +1,17 @@
|
||||
(define-package "jupyter" "20240418.1642" "Jupyter"
|
||||
'((emacs "26")
|
||||
(cl-lib "0.5")
|
||||
(org "9.1.6")
|
||||
(zmq "0.10.10")
|
||||
(simple-httpd "1.5.0")
|
||||
(websocket "1.9"))
|
||||
:commit "f1394d303be76a1fa44d4135b4f3ceab9387a16b" :authors
|
||||
'(("Nathaniel Nicandro" . "nathanielnicandro@gmail.com"))
|
||||
:maintainers
|
||||
'(("Nathaniel Nicandro" . "nathanielnicandro@gmail.com"))
|
||||
:maintainer
|
||||
'("Nathaniel Nicandro" . "nathanielnicandro@gmail.com")
|
||||
:url "https://github.com/emacs-jupyter/jupyter")
|
||||
;; Local Variables:
|
||||
;; no-byte-compile: t
|
||||
;; End:
|
||||
110
lisp/jupyter/jupyter-python.el
Normal file
110
lisp/jupyter/jupyter-python.el
Normal file
@@ -0,0 +1,110 @@
|
||||
;;; jupyter-python.el --- Jupyter support for python -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 23 Oct 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Support methods for integration with Python.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-repl)
|
||||
(require 'jupyter-org-client)
|
||||
|
||||
(declare-function org-babel-python-table-or-string "ob-python")
|
||||
|
||||
(cl-defmethod jupyter-handle-error :after ((client jupyter-repl-client) req msg
|
||||
&context (jupyter-lang python)
|
||||
(major-mode jupyter-repl-mode))
|
||||
"Add spacing between the first occurance of ENAME and \"Traceback\".
|
||||
Do this only when the traceback of REQ was inserted into the REPL
|
||||
buffer."
|
||||
(unless (equal (jupyter-message-parent-type msg) "comm_msg")
|
||||
(jupyter-with-repl-buffer client
|
||||
(jupyter-with-message-content msg (ename)
|
||||
(save-excursion
|
||||
(jupyter-repl-goto-cell req)
|
||||
(goto-char (jupyter-repl-cell-code-end-position))
|
||||
(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)))))
|
||||
(insert-and-inherit
|
||||
(propertize (make-string (if (> len 4) len 4) ? )
|
||||
'read-only t)))))))))
|
||||
|
||||
(cl-defmethod jupyter-insert :around ((msg cons)
|
||||
&context (jupyter-lang python)
|
||||
&rest _ignore)
|
||||
"Fontify docstrings after inserting inspect messages."
|
||||
(let ((mime (cl-call-next-method)))
|
||||
(prog1 mime
|
||||
(cond
|
||||
((and (eq mime :text/plain)
|
||||
(string= (jupyter-message-type msg) "inspect_reply"))
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(when (re-search-forward "^Docstring:" nil t)
|
||||
(jupyter-fontify-region-according-to-mode
|
||||
#'rst-mode (1+ (point))
|
||||
(or (and (re-search-forward "^\\(File\\|Type\\):" nil t)
|
||||
(line-beginning-position))
|
||||
(point-max))))))
|
||||
(t nil)))))
|
||||
|
||||
(cl-defmethod jupyter-load-file-code (file &context (jupyter-lang python))
|
||||
(concat "%run " file))
|
||||
|
||||
;;; `jupyter-org'
|
||||
|
||||
(cl-defmethod jupyter-org-result ((_mime (eql :text/plain)) _content params
|
||||
&context (jupyter-lang python))
|
||||
(let ((result (cl-call-next-method)))
|
||||
(cond
|
||||
((and (stringp result)
|
||||
(not (member "scalar" (alist-get :result-params params))))
|
||||
(org-babel-python-table-or-string result))
|
||||
(t result))))
|
||||
|
||||
(cl-defmethod jupyter-org-error-location (&context (jupyter-lang python))
|
||||
(and (or (save-excursion (re-search-forward "^----> \\([0-9]+\\)" nil t))
|
||||
(re-search-forward "^[\t ]*File.+line \\([0-9]+\\)$" nil t))
|
||||
(string-to-number (match-string 1))))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-transform-code (code changelist &context (jupyter-lang python))
|
||||
(when (plist-get changelist :dir)
|
||||
(setq code
|
||||
(format "\
|
||||
import os
|
||||
__JUPY_saved_dir = os.getcwd()
|
||||
os.chdir(\"%s\")
|
||||
try:
|
||||
get_ipython().run_cell(r\"\"\"%s\"\"\")
|
||||
finally:
|
||||
os.chdir(__JUPY_saved_dir)"
|
||||
(plist-get changelist :dir) code)))
|
||||
code)
|
||||
|
||||
(provide 'jupyter-python)
|
||||
|
||||
;;; jupyter-python.el ends here
|
||||
2179
lisp/jupyter/jupyter-repl.el
Normal file
2179
lisp/jupyter/jupyter-repl.el
Normal file
File diff suppressed because it is too large
Load Diff
1103
lisp/jupyter/jupyter-rest-api.el
Normal file
1103
lisp/jupyter/jupyter-rest-api.el
Normal file
File diff suppressed because it is too large
Load Diff
375
lisp/jupyter/jupyter-server-kernel.el
Normal file
375
lisp/jupyter/jupyter-server-kernel.el
Normal file
@@ -0,0 +1,375 @@
|
||||
;;; jupyter-server-kernel.el --- Working with kernels behind a Jupyter server -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2020-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 23 Apr 2020
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Holds the definitions of `jupyter-server', what communicates to the
|
||||
;; Jupyter server using the REST API, and `jupyter-kernel-server' a
|
||||
;; representation of a kernel on a server.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-kernel)
|
||||
(require 'jupyter-rest-api)
|
||||
(require 'jupyter-monads)
|
||||
(require 'websocket)
|
||||
|
||||
(declare-function jupyter-encode-raw-message "jupyter-messages")
|
||||
(declare-function jupyter-tramp-server-from-file-name "jupyter-tramp")
|
||||
(declare-function jupyter-tramp-file-name-p "jupyter-tramp")
|
||||
(declare-function jupyter-server-kernel-id-from-name "jupyter-server")
|
||||
|
||||
(defgroup jupyter-server-kernel nil
|
||||
"Kernel behind a Jupyter server"
|
||||
:group 'jupyter)
|
||||
|
||||
;;; `jupyter-server'
|
||||
|
||||
(defvar-local jupyter-current-server nil
|
||||
"The `jupyter-server' associated with the current buffer.
|
||||
Used in, e.g. a `jupyter-server-kernel-list-mode' buffer.")
|
||||
|
||||
(put 'jupyter-current-server 'permanent-local t)
|
||||
|
||||
(defvar jupyter--servers nil)
|
||||
|
||||
;; TODO: We should really rename `jupyter-server' to something like
|
||||
;; `jupyter-server-client' since it isn't a representation of a server, but a
|
||||
;; communication channel with one.
|
||||
(defclass jupyter-server (jupyter-rest-client eieio-instance-tracker)
|
||||
((tracking-symbol :initform 'jupyter--servers)
|
||||
(kernelspecs
|
||||
:type json-plist
|
||||
:initform nil
|
||||
:documentation "Kernelspecs for the kernels available behind
|
||||
this gateway. Access them through `jupyter-kernelspecs'.")))
|
||||
|
||||
(cl-defmethod make-instance ((_class (subclass jupyter-server)) &rest slots)
|
||||
(cl-assert (plist-get slots :url))
|
||||
(or (cl-loop
|
||||
with url = (plist-get slots :url)
|
||||
for server in jupyter--servers
|
||||
if (equal url (oref server url)) return server)
|
||||
(cl-call-next-method)))
|
||||
|
||||
(defun jupyter-servers ()
|
||||
"Return a list of all `jupyter-server's."
|
||||
(jupyter-gc-servers)
|
||||
jupyter--servers)
|
||||
|
||||
(defun jupyter-gc-servers ()
|
||||
"Delete `jupyter-server' instances that are no longer accessible."
|
||||
(dolist (server jupyter--servers)
|
||||
(unless (jupyter-api-server-exists-p server)
|
||||
(jupyter-api-delete-cookies (oref server url))
|
||||
(delete-instance server))))
|
||||
|
||||
(cl-defmethod jupyter-api-request :around ((server jupyter-server) _method &rest _plist)
|
||||
(condition-case nil
|
||||
(cl-call-next-method)
|
||||
(jupyter-api-unauthenticated
|
||||
(if (memq jupyter-api-authentication-method '(ask token password))
|
||||
(oset server auth jupyter-api-authentication-method)
|
||||
(error "Unauthenticated request, can't attempt re-authentication \
|
||||
with default `jupyter-api-authentication-method'"))
|
||||
(prog1 (cl-call-next-method)
|
||||
(jupyter-reauthenticate-websockets server)))))
|
||||
|
||||
(cl-defmethod jupyter-kernelspecs ((client jupyter-rest-client) &optional _refresh)
|
||||
(or (jupyter-api-get-kernelspec client)
|
||||
(error "Can't retrieve kernelspecs from server @ %s"
|
||||
(oref client url))))
|
||||
|
||||
(cl-defmethod jupyter-kernelspecs ((server jupyter-server) &optional refresh)
|
||||
"Return the kernelspecs on SERVER.
|
||||
By default the available kernelspecs are cached. To force an
|
||||
update of the cached kernelspecs, give a non-nil value to
|
||||
REFRESH."
|
||||
(when (or refresh (null (oref server kernelspecs)))
|
||||
(let ((specs (cl-call-next-method)))
|
||||
(plist-put specs :kernelspecs
|
||||
(cl-loop
|
||||
for (_ spec) on (plist-get specs :kernelspecs) by #'cddr
|
||||
for name = (plist-get spec :name)
|
||||
collect (make-jupyter-kernelspec
|
||||
:name name
|
||||
:plist (plist-get spec :spec))))
|
||||
(oset server kernelspecs specs)))
|
||||
(plist-get (oref server kernelspecs) :kernelspecs))
|
||||
|
||||
(cl-defmethod jupyter-kernelspecs :extra "server" ((host string) &optional refresh)
|
||||
(if (jupyter-tramp-file-name-p host)
|
||||
(jupyter-kernelspecs (jupyter-tramp-server-from-file-name host) refresh)
|
||||
(cl-call-next-method)))
|
||||
|
||||
(cl-defmethod jupyter-server-has-kernelspec-p ((server jupyter-server) name)
|
||||
"Return non-nil if SERVER can launch kernels with kernelspec NAME."
|
||||
(jupyter-guess-kernelspec name (jupyter-kernelspecs server)))
|
||||
|
||||
;;; Kernel definition
|
||||
|
||||
(cl-defstruct (jupyter-server-kernel
|
||||
(:include jupyter-kernel))
|
||||
(server jupyter-current-server
|
||||
:read-only t
|
||||
:documentation "The kernel server.")
|
||||
;; TODO: Make this read only by only allowing creating
|
||||
;; representations of kernels that have already been launched and
|
||||
;; have a connection to the kernel.
|
||||
(id nil
|
||||
:type (or null string)
|
||||
:documentation "The kernel ID."))
|
||||
|
||||
(cl-defmethod jupyter-alive-p ((kernel jupyter-server-kernel))
|
||||
(pcase-let (((cl-struct jupyter-server-kernel server id) kernel))
|
||||
(and id server
|
||||
;; TODO: Cache this call
|
||||
(condition-case err
|
||||
(jupyter-api-get-kernel server id)
|
||||
(file-error nil) ; Non-existent server
|
||||
(jupyter-api-http-error
|
||||
(unless (= (nth 1 err) 404) ; Not Found
|
||||
(signal (car err) (cdr err)))))
|
||||
(cl-call-next-method))))
|
||||
|
||||
(defun jupyter-server-kernel (&rest args)
|
||||
"Return a `jupyter-server-kernel' initialized with ARGS."
|
||||
(apply #'make-jupyter-server-kernel args))
|
||||
|
||||
(cl-defmethod jupyter-kernel :extra "server" (&rest args)
|
||||
"Return a representation of a kernel on a Jupyter server.
|
||||
If ARGS has a :server key, return a `jupyter-server-kernel'
|
||||
initialized using ARGS. If ARGS also has a :spec key, whose
|
||||
value is the name of a kernelspec, the returned kernel's spec
|
||||
slot will be the corresponding `jupyter-kernelspec'.
|
||||
|
||||
Call the next method if ARGS does not contain :server."
|
||||
(let ((server (plist-get args :server)))
|
||||
(if (not server) (cl-call-next-method)
|
||||
(cl-assert (object-of-class-p server 'jupyter-server))
|
||||
(let ((spec (plist-get args :spec)))
|
||||
(when (stringp spec)
|
||||
(plist-put args :spec
|
||||
;; TODO: (jupyter-server-kernelspec server "python3")
|
||||
;; which returns an I/O action and then arrange
|
||||
;; for that action to be bound by mlet* and set
|
||||
;; as the spec value. Or better yet, have
|
||||
;; `jupyter-kernel' return a delayed kernel with
|
||||
;; the server connection already open and
|
||||
;; kernelspecs already retrieved.
|
||||
(or (jupyter-guess-kernelspec
|
||||
spec (jupyter-kernelspecs server))
|
||||
;; TODO: Return the error to the I/O context.
|
||||
(error "No kernelspec matching %s @ %s" spec
|
||||
(oref server url))))))
|
||||
(apply #'jupyter-server-kernel args))))
|
||||
|
||||
;;; Websocket IO
|
||||
|
||||
(defvar jupyter--reauth-subscribers (make-hash-table :weakness 'key :test 'eq))
|
||||
|
||||
(defun jupyter-reauthenticate-websockets (server)
|
||||
"Re-authenticate WebSocket connections of SERVER."
|
||||
(when-let* ((pub (gethash server jupyter--reauth-subscribers)))
|
||||
(jupyter-run-with-io pub
|
||||
(jupyter-publish 'reauthenticate))))
|
||||
|
||||
(cl-defmethod jupyter-websocket-io ((kernel jupyter-server-kernel))
|
||||
"Return a list representing an IO connection to KERNEL.
|
||||
The list is composed of two elements (IO-PUB ACTION-SUB), IO-PUB
|
||||
is a publisher used to send/receive messages to/from KERNEL and
|
||||
ACTION-SUB is a subscriber of kernel actions to perform on
|
||||
KERNEL.
|
||||
|
||||
To send a message to KERNEL, publish a list of the form
|
||||
|
||||
(list \='send CHANNEL MSG-TYPE CONTENT MSG-ID)
|
||||
|
||||
to IO-PUB, e.g.
|
||||
|
||||
(jupyter-run-with-io IO-PUB
|
||||
(jupyter-publish (list \='send CHANNEL MSG-TYPE CONTENT MSG-ID)))
|
||||
|
||||
To receive messages from KERNEL, subscribe to IO-PUB e.g.
|
||||
|
||||
(jupyter-run-with-io IO-PUB
|
||||
(jupyter-subscribe
|
||||
(jupyter-subscriber
|
||||
(lambda (msg)
|
||||
...))))
|
||||
|
||||
The value \='interrupt or \='shutdown can be published to ACTION-SUB
|
||||
to interrupt or shutdown KERNEL. The value (list \='action FN)
|
||||
where FN is a single argument function can also be published, in
|
||||
this case FN will be evaluated on KERNEL."
|
||||
(jupyter-launch kernel)
|
||||
(pcase-let* (((cl-struct jupyter-server-kernel server id) kernel))
|
||||
(letrec ((status-pub (jupyter-publisher))
|
||||
(reauth-pub (or (gethash server jupyter--reauth-subscribers)
|
||||
(setf (gethash server jupyter--reauth-subscribers)
|
||||
(jupyter-publisher))))
|
||||
(shutdown nil)
|
||||
(kernel-io
|
||||
(jupyter-publisher
|
||||
(lambda (event)
|
||||
(pcase event
|
||||
(`(message . ,rest) (jupyter-content rest))
|
||||
(`(send ,channel ,msg-type ,content ,msg-id)
|
||||
(when shutdown
|
||||
(error "Attempting to send message to shutdown kernel"))
|
||||
(let ((send
|
||||
(lambda ()
|
||||
(websocket-send-text
|
||||
ws (let* ((cd (websocket-client-data ws))
|
||||
(session (plist-get cd :session)))
|
||||
(jupyter-encode-raw-message session msg-type
|
||||
:channel channel
|
||||
:msg-id msg-id
|
||||
:content content))))))
|
||||
(condition-case nil
|
||||
(funcall send)
|
||||
(websocket-closed
|
||||
(setq ws (funcall make-websocket))
|
||||
(funcall send)))))
|
||||
('start
|
||||
(when shutdown
|
||||
(error "Can't start I/O connection to shutdown kernel"))
|
||||
(unless (websocket-openp ws)
|
||||
(setq ws (funcall make-websocket))))
|
||||
('stop (websocket-close ws))))))
|
||||
(ws-failed-to-open t)
|
||||
(make-websocket
|
||||
(lambda ()
|
||||
(jupyter-api-kernel-websocket
|
||||
server id
|
||||
:custom-header-alist (jupyter-api-auth-headers server)
|
||||
:on-open
|
||||
(lambda (_ws)
|
||||
(setq ws-failed-to-open nil))
|
||||
:on-close
|
||||
(lambda (_ws)
|
||||
(if ws-failed-to-open
|
||||
;; TODO: Retry?
|
||||
(error "Kernel connection could not be established")
|
||||
(setq ws-failed-to-open t)))
|
||||
;; TODO: on-error publishes to status-pub
|
||||
:on-message
|
||||
(lambda (_ws frame)
|
||||
(pcase (websocket-frame-opcode frame)
|
||||
((or 'text 'binary)
|
||||
(let ((msg (jupyter-read-plist-from-string
|
||||
(websocket-frame-payload frame))))
|
||||
(jupyter-run-with-io kernel-io
|
||||
(jupyter-publish (cons 'message msg)))))
|
||||
(_
|
||||
(jupyter-run-with-io status-pub
|
||||
(jupyter-publish
|
||||
(list 'error (websocket-frame-opcode frame))))))))))
|
||||
(ws (prog1 (funcall make-websocket)
|
||||
(jupyter-run-with-io reauth-pub
|
||||
(jupyter-subscribe
|
||||
(jupyter-subscriber
|
||||
(lambda (_reauth)
|
||||
(if shutdown (jupyter-unsubscribe)
|
||||
(jupyter-run-with-io kernel-io
|
||||
(jupyter-do
|
||||
(jupyter-publish 'stop)
|
||||
(jupyter-publish 'start)))))))))))
|
||||
(list kernel-io
|
||||
(jupyter-subscriber
|
||||
(lambda (action)
|
||||
(pcase action
|
||||
('interrupt
|
||||
(jupyter-interrupt kernel))
|
||||
('shutdown
|
||||
(jupyter-shutdown kernel)
|
||||
(setq shutdown t)
|
||||
(when (websocket-openp ws)
|
||||
(websocket-close ws)))
|
||||
('restart
|
||||
(jupyter-restart kernel))
|
||||
(`(action ,fn)
|
||||
(funcall fn kernel)))))))))
|
||||
|
||||
(cl-defmethod jupyter-io ((kernel jupyter-server-kernel))
|
||||
(jupyter-websocket-io kernel))
|
||||
|
||||
;;; Kernel management
|
||||
|
||||
;; The KERNEL argument is optional here so that `jupyter-launch'
|
||||
;; does not require more than one argument just to handle this case.
|
||||
(cl-defmethod jupyter-launch ((server jupyter-server) &optional kernel)
|
||||
(cl-check-type kernel string)
|
||||
(let* ((spec (jupyter-guess-kernelspec
|
||||
kernel (jupyter-kernelspecs server)))
|
||||
(plist (jupyter-api-start-kernel
|
||||
server (jupyter-kernelspec-name spec))))
|
||||
(jupyter-kernel :server server :id (plist-get plist :id) :spec spec)))
|
||||
|
||||
;; FIXME: Don't allow creating kernels without them being launched.
|
||||
(cl-defmethod jupyter-launch ((kernel jupyter-server-kernel))
|
||||
"Launch KERNEL based on its kernelspec.
|
||||
When KERNEL does not have an ID yet, launch KERNEL on SERVER
|
||||
using its SPEC."
|
||||
(pcase-let (((cl-struct jupyter-server-kernel server id spec session) kernel))
|
||||
(unless session
|
||||
(and id (setq id (or (jupyter-server-kernel-id-from-name server id) id)))
|
||||
(if id
|
||||
;; When KERNEL already has an ID before it has a session,
|
||||
;; assume we are connecting to an already launched kernel. In
|
||||
;; this case, make sure the KERNEL's SPEC is the same as the
|
||||
;; one being connected to.
|
||||
;;
|
||||
;; Note, this also has the side effect of raising an error
|
||||
;; when the ID does not match one on the server.
|
||||
(unless spec
|
||||
(let ((model (jupyter-api-get-kernel server id)))
|
||||
(setf (jupyter-kernel-spec kernel)
|
||||
(jupyter-guess-kernelspec
|
||||
(plist-get model :name)
|
||||
(jupyter-kernelspecs server)))))
|
||||
(let ((plist (jupyter-api-start-kernel
|
||||
server (jupyter-kernelspec-name spec))))
|
||||
(setf (jupyter-server-kernel-id kernel) (plist-get plist :id))
|
||||
(sit-for 1)))
|
||||
;; TODO: Replace with the real session object
|
||||
(setf (jupyter-kernel-session kernel) (jupyter-session))))
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-shutdown ((kernel jupyter-server-kernel))
|
||||
(pcase-let (((cl-struct jupyter-server-kernel server id session) kernel))
|
||||
(cl-call-next-method)
|
||||
(when session
|
||||
(jupyter-api-shutdown-kernel server id))))
|
||||
|
||||
(cl-defmethod jupyter-restart ((kernel jupyter-server-kernel))
|
||||
(pcase-let (((cl-struct jupyter-server-kernel server id session) kernel))
|
||||
(when session
|
||||
(jupyter-api-restart-kernel server id))))
|
||||
|
||||
(cl-defmethod jupyter-interrupt ((kernel jupyter-server-kernel))
|
||||
(pcase-let (((cl-struct jupyter-server-kernel server id) kernel))
|
||||
(jupyter-api-interrupt-kernel server id)))
|
||||
|
||||
(provide 'jupyter-server-kernel)
|
||||
|
||||
;;; jupyter-server-kernel.el ends here
|
||||
574
lisp/jupyter/jupyter-server.el
Normal file
574
lisp/jupyter/jupyter-server.el
Normal file
@@ -0,0 +1,574 @@
|
||||
;;; jupyter-server.el --- Support for the Jupyter kernel servers -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 02 Apr 2019
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Overview of implementation
|
||||
;;
|
||||
;; A `jupyter-server' communicates with a Jupyter kernel server (either the
|
||||
;; notebook or a kernel gateway) via the Jupyter REST API. Given the URL and
|
||||
;; Websocket URL for the server, the `jupyter-server' object can launch kernels
|
||||
;; using the function `jupyter-server-start-new-kernel'. The kernelspecs
|
||||
;; available on the server can be accessed by calling
|
||||
;; `jupyter-kernelspecs'.
|
||||
;;
|
||||
;; Starting REPLs
|
||||
;;
|
||||
;; You can launch kernels without connecting clients to them by using
|
||||
;; `jupyter-server-launch-kernel'. To connect a REPL to a launched kernel use
|
||||
;; `jupyter-connect-server-repl'. To both launch and connect a REPL use
|
||||
;; `jupyter-run-server-repl'. All of the previous commands determine the server
|
||||
;; to use by using the `jupyter-current-server' function, which see.
|
||||
;;
|
||||
;; Managing kernels on a server
|
||||
;;
|
||||
;; To get an overview of all live kernels on a server you can call
|
||||
;; `jupyter-server-list-kernels'. From the buffer displayed there are a number
|
||||
;; of keys bound that enable you to manage the kernels on the server. See
|
||||
;; `jupyter-server-kernel-list-mode-map'.
|
||||
;;
|
||||
;; TODO: Find where it would be appropriate to call `delete-instance' on a
|
||||
;;`jupyter-server' that does not have any websockets open, clients connected,
|
||||
;; or HTTP connections open, or is not bound to `jupyter-current-server' in any
|
||||
;; buffer.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(eval-when-compile (require 'subr-x))
|
||||
(require 'jupyter-repl)
|
||||
(require 'jupyter-server-kernel)
|
||||
|
||||
(declare-function jupyter-tramp-file-name-p "jupyter-tramp" (filename))
|
||||
(declare-function jupyter-tramp-server-from-file-name "jupyter-tramp" (filename))
|
||||
(declare-function jupyter-tramp-file-name-from-url "jupyter-tramp" (url))
|
||||
|
||||
(defgroup jupyter-server nil
|
||||
"Support for the Jupyter kernel gateway"
|
||||
:group 'jupyter)
|
||||
|
||||
;;; Assigning names to kernel IDs
|
||||
|
||||
(defvar jupyter-server-kernel-names nil
|
||||
"An alist mapping URLs to alists mapping kernel IDs to human friendly names.
|
||||
For example
|
||||
|
||||
\((\"http://localhost:8888\"
|
||||
(\"72d92ded-1275-440a-852f-90f655197305\" . \"thermo\"))\)
|
||||
|
||||
You can persist this alist across Emacs sessions using `desktop',
|
||||
`savehist', or any other session persistence package. For
|
||||
example, when using `savehist' you can add the following to your
|
||||
init file to persist the server names across Emacs sessions.
|
||||
|
||||
\(savehist-mode\)
|
||||
\(add-to-list \='savehist-additional-variables \='jupyter-server-kernel-names\).")
|
||||
|
||||
(defun jupyter-server-cull-kernel-names (&optional server)
|
||||
"Ensure all names in `jupyter-server-kernel-names' map to existing kernels.
|
||||
If SERVER is non-nil only check the kernels on SERVER, otherwise
|
||||
check all kernels on all existing servers."
|
||||
(let ((servers (if server (list server)
|
||||
(jupyter-gc-servers)
|
||||
(jupyter-servers))))
|
||||
(unless server
|
||||
;; Only remove non-existing servers when culling all kernels on all
|
||||
;; servers.
|
||||
(let ((urls (mapcar (lambda (x) (oref x url)) servers)))
|
||||
(cl-callf2 cl-remove-if-not (lambda (x) (member (car x) urls))
|
||||
jupyter-server-kernel-names)))
|
||||
(dolist (server servers)
|
||||
(when-let* ((names (assoc (oref server url) jupyter-server-kernel-names)))
|
||||
(setf (alist-get (oref server url)
|
||||
jupyter-server-kernel-names nil nil #'equal)
|
||||
(cl-loop
|
||||
for kernel across (jupyter-api-get-kernel server)
|
||||
for name = (assoc (plist-get kernel :id) names)
|
||||
when name collect name))))))
|
||||
|
||||
(defun jupyter-server-kernel-name (server id)
|
||||
"Return the associated name of the kernel with ID on SERVER.
|
||||
If there is no name associated, return nil. See
|
||||
`jupyter-server-kernel-names'."
|
||||
(cl-check-type server jupyter-server)
|
||||
(let ((kernel-names (assoc (oref server url) jupyter-server-kernel-names)))
|
||||
(cdr (assoc id kernel-names))))
|
||||
|
||||
(defun jupyter-server-kernel-id-from-name (server name)
|
||||
"Return the ID of the kernel that has NAME on SERVER.
|
||||
If NAME does not have a kernel associated, return nil. See
|
||||
`jupyter-server-kernel-names'."
|
||||
(cl-check-type server jupyter-server)
|
||||
(jupyter-server-cull-kernel-names server)
|
||||
(let ((kernel-names (assoc (oref server url) jupyter-server-kernel-names)))
|
||||
(car (rassoc name kernel-names))))
|
||||
|
||||
(defun jupyter-server-name-kernel (server id name)
|
||||
"NAME the kernel with ID on SERVER.
|
||||
See `jupyter-server-kernel-names'."
|
||||
(cl-check-type server jupyter-server)
|
||||
(setf (alist-get id
|
||||
(alist-get (oref server url)
|
||||
jupyter-server-kernel-names
|
||||
nil nil #'equal)
|
||||
nil nil #'equal)
|
||||
name))
|
||||
|
||||
(defun jupyter-server-name-client-kernel (client name)
|
||||
"For the kernel connected to CLIENT associate NAME.
|
||||
CLIENT must be communicating with a `jupyter-server-kernel', the
|
||||
CLIENT must be communicating with a `jupyter-server-kernel', see
|
||||
`jupyter-server-kernel-names'."
|
||||
(cl-check-type client jupyter-kernel-client)
|
||||
(jupyter-kernel-action client
|
||||
(lambda (kernel)
|
||||
(pcase-let (((cl-struct jupyter-server-kernel server id) kernel))
|
||||
(jupyter-server-name-kernel server id name)))))
|
||||
|
||||
;;; Launching notebook processes
|
||||
|
||||
(defvar jupyter-notebook-procs nil)
|
||||
|
||||
(defvar jupyter-default-notebook-port 8888)
|
||||
|
||||
(defun jupyter-port-available-p (port)
|
||||
"Return non-nil if PORT is available."
|
||||
(let ((proc
|
||||
(condition-case nil
|
||||
(make-network-process
|
||||
:name "jupyter-port-available-p"
|
||||
:server t
|
||||
:host "127.0.0.1"
|
||||
:service port)
|
||||
(file-error nil))))
|
||||
(when proc
|
||||
(prog1 t
|
||||
(delete-process proc)))))
|
||||
|
||||
(defun jupyter-launch-notebook (&optional port authentication)
|
||||
"Launch a Jupyter notebook on PORT with AUTHENTICATION.
|
||||
If PORT is nil, launch the notebook on the
|
||||
`jupyter-default-notebook-port' if available. Launch the
|
||||
notebook on a random port otherwise. Return the actual port
|
||||
used.
|
||||
|
||||
If AUTHENTICATION is t, use the default, token, authentication of
|
||||
a Jupyter notebook. If AUTHENTICATION is a string, it is
|
||||
interpreted as the password to the notebook. Any other value of
|
||||
AUTHENTICATION means the notebook is not authenticated."
|
||||
(let ((port (if port
|
||||
(if (jupyter-port-available-p port)
|
||||
port
|
||||
(error "Port %s not available" port))
|
||||
(if (jupyter-port-available-p jupyter-default-notebook-port)
|
||||
jupyter-default-notebook-port
|
||||
(car (jupyter-available-local-ports 1))))))
|
||||
(prog1 port
|
||||
(let ((buffer (generate-new-buffer "*jupyter-notebook*"))
|
||||
(args (append
|
||||
(list "notebook" "--no-browser" "--debug"
|
||||
(format "--NotebookApp.port=%s" port))
|
||||
(cond
|
||||
((eq authentication t)
|
||||
(list))
|
||||
((stringp authentication)
|
||||
(list
|
||||
"--NotebookApp.token=''"
|
||||
(format "--NotebookApp.password='%s'"
|
||||
authentication)))
|
||||
(t
|
||||
(list
|
||||
"--NotebookApp.token=''"
|
||||
"--NotebookApp.password=''"))))))
|
||||
(setq jupyter-notebook-procs
|
||||
(cl-loop for (port . proc) in jupyter-notebook-procs
|
||||
if (process-live-p proc) collect (cons port proc)))
|
||||
(push
|
||||
(cons port
|
||||
(apply #'start-file-process
|
||||
"jupyter-notebook" buffer "jupyter" args))
|
||||
jupyter-notebook-procs)
|
||||
(with-current-buffer buffer
|
||||
(jupyter-with-timeout ((format "Launching notebook process on port %s..." port) 5)
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(re-search-forward "Jupyter Notebook.+running at:$" nil t))))))))
|
||||
|
||||
(defun jupyter-notebook-process (server)
|
||||
"Return a process object for the notebook associated with SERVER.
|
||||
Return nil if the associated notebook process was not launched by
|
||||
Emacs."
|
||||
(let ((url (url-generic-parse-url (oref server url))))
|
||||
(cdr (assoc (url-port url) jupyter-notebook-procs))))
|
||||
|
||||
;;; Helpers for commands
|
||||
|
||||
(defun jupyter-completing-read-server-kernel (server)
|
||||
"Use `completing-read' to select a kernel on SERVER.
|
||||
A model of the kernel is returned as a property list and has at
|
||||
least the following keys:
|
||||
|
||||
- :id :: The ID used to identify the kernel on the server
|
||||
- :last_activity :: The last channel activity of the kernel
|
||||
- :name :: The kernelspec name used to start the kernel
|
||||
- :execution_state :: The status of the kernel
|
||||
- :connections :: The number of websocket connections for the kernel"
|
||||
(let* ((kernels (jupyter-api-get-kernel server))
|
||||
(display-names
|
||||
(if (null kernels) (error "No kernels @ %s" (oref server url))
|
||||
(mapcar (lambda (k)
|
||||
(cl-destructuring-bind
|
||||
(&key name id last_activity &allow-other-keys) k
|
||||
(concat name " (last activity: " last_activity ", id: " id ")")))
|
||||
kernels)))
|
||||
(name (completing-read "kernel: " display-names nil t)))
|
||||
(when (equal name "")
|
||||
(error "No kernel selected"))
|
||||
(nth (- (length display-names)
|
||||
(length (member name display-names)))
|
||||
(append kernels nil))))
|
||||
|
||||
(define-error 'jupyter-server-non-existent
|
||||
"The server doesn't exist")
|
||||
|
||||
(defun jupyter-current-server (&optional ask)
|
||||
"Return an existing `jupyter-server' or ASK for a new one.
|
||||
If ASK is non-nil, always ask for a URL and return the
|
||||
`jupyter-server' object corresponding to it. If no Jupyter server
|
||||
at URL exists, `signal' a `jupyter-server-non-existent' error
|
||||
with error data being URL.
|
||||
|
||||
If the buffer local value of `jupyter-current-server' is non-nil,
|
||||
return its value. If `jupyter-current-server' is nil and the
|
||||
`jupyter-current-client' is communicating with a kernel behind a
|
||||
kernel server, return the `jupyter-server' managing the
|
||||
connection.
|
||||
|
||||
If `jupyter-current-client' is nil or not communicating with a
|
||||
kernel behind a server and if `default-directory' is a Jupyter
|
||||
remote file name, return the `jupyter-server' object
|
||||
corresponding to that connection.
|
||||
|
||||
If all of the above fails, either return the most recently used
|
||||
`jupyter-server' object if there is one or ask for one based off
|
||||
a URL."
|
||||
(interactive "P")
|
||||
(let ((read-url-make-server
|
||||
(lambda ()
|
||||
;; From the list of available server
|
||||
;; (if (> (length jupyter--servers) 1)
|
||||
;; (let ((server (cdr (completing-read
|
||||
;; "Jupyter Server: "
|
||||
;; (mapcar (lambda (x) (cons (oref x url) x))
|
||||
;; jupyter--servers)))))
|
||||
;; )
|
||||
(jupyter-gc-servers)
|
||||
(let* ((url (read-string "Server URL: " "http://localhost:8888"))
|
||||
(ws-url (read-string "Websocket URL: "
|
||||
(let ((u (url-generic-parse-url url)))
|
||||
(setf (url-type u) "ws")
|
||||
(url-recreate-url u)))))
|
||||
(let ((server (jupyter-server :url url :ws-url ws-url)))
|
||||
(if (jupyter-api-server-exists-p server) server
|
||||
(delete-instance server)
|
||||
(signal 'jupyter-server-non-existent (list url))))))))
|
||||
(let ((server
|
||||
(if ask (funcall read-url-make-server)
|
||||
(cond
|
||||
(jupyter-current-server)
|
||||
;; Server of the current kernel client
|
||||
((and jupyter-current-client
|
||||
(jupyter-kernel-action
|
||||
jupyter-current-client
|
||||
(lambda (kernel)
|
||||
(and (jupyter-server-kernel-p kernel)
|
||||
(jupyter-server-kernel-server kernel))))))
|
||||
;; Server of the current TRAMP remote context
|
||||
((and (file-remote-p default-directory)
|
||||
(jupyter-tramp-file-name-p default-directory)
|
||||
(jupyter-tramp-server-from-file-name default-directory)))
|
||||
;; Most recently accessed
|
||||
(t
|
||||
(or (car jupyter--servers)
|
||||
(funcall read-url-make-server)))))))
|
||||
(prog1 server
|
||||
(setq jupyter--servers
|
||||
(cons server (delq server jupyter--servers)))))))
|
||||
|
||||
;;; Commands
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-server-launch-kernel (server)
|
||||
"Start a kernel on SERVER.
|
||||
|
||||
With a prefix argument, ask to select a server if there are
|
||||
mutiple to choose from, otherwise the most recently used server
|
||||
is used as determined by `jupyter-current-server'."
|
||||
(interactive (list (jupyter-current-server current-prefix-arg)))
|
||||
(let* ((specs (jupyter-kernelspecs server))
|
||||
(spec (jupyter-completing-read-kernelspec specs)))
|
||||
(jupyter-api-start-kernel server (jupyter-kernelspec-name spec))))
|
||||
|
||||
;;; REPL
|
||||
|
||||
;; TODO: When closing the REPL buffer and it is the last connected client as
|
||||
;; shown by the :connections key of a `jupyter-api-get-kernel' call, ask to
|
||||
;; also shutdown the kernel.
|
||||
(defun jupyter-server-repl (kernel &optional repl-name associate-buffer client-class display)
|
||||
(or client-class (setq client-class 'jupyter-repl-client))
|
||||
(jupyter-error-if-not-client-class-p client-class 'jupyter-repl-client)
|
||||
(jupyter-bootstrap-repl
|
||||
(jupyter-client kernel client-class)
|
||||
repl-name associate-buffer display))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-run-server-repl
|
||||
(server kernel-name &optional repl-name associate-buffer client-class display)
|
||||
"On SERVER start a kernel with KERNEL-NAME.
|
||||
|
||||
With a prefix argument, ask to select a server if there are
|
||||
mutiple to choose from, otherwise the most recently used server
|
||||
is used as determined by `jupyter-current-server'.
|
||||
|
||||
REPL-NAME, ASSOCIATE-BUFFER, CLIENT-CLASS, and DISPLAY all have
|
||||
the same meaning as in `jupyter-run-repl'."
|
||||
(interactive
|
||||
(let ((server (jupyter-current-server current-prefix-arg)))
|
||||
(list server
|
||||
(jupyter-completing-read-kernelspec
|
||||
(jupyter-kernelspecs server))
|
||||
;; FIXME: Ambiguity with `jupyter-current-server' and
|
||||
;; `current-prefix-arg'
|
||||
(when (and current-prefix-arg
|
||||
(y-or-n-p "Name REPL? "))
|
||||
(read-string "REPL Name: "))
|
||||
t nil t)))
|
||||
(jupyter-server-repl
|
||||
(jupyter-kernel :server server :spec kernel-name)
|
||||
repl-name associate-buffer client-class display))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-connect-server-repl
|
||||
(server kernel-id &optional repl-name associate-buffer client-class display)
|
||||
"On SERVER, connect to the kernel with KERNEL-ID.
|
||||
|
||||
With a prefix argument, ask to select a server if there are
|
||||
mutiple to choose from, otherwise the most recently used server
|
||||
is used as determined by `jupyter-current-server'.
|
||||
|
||||
REPL-NAME, ASSOCIATE-BUFFER, CLIENT-CLASS, and DISPLAY all have
|
||||
the same meaning as in `jupyter-connect-repl'."
|
||||
(interactive
|
||||
(let ((server (jupyter-current-server current-prefix-arg)))
|
||||
(list server
|
||||
(completing-read
|
||||
"Kernel ID: "
|
||||
(mapcar (lambda (kernel)
|
||||
(cl-destructuring-bind (&key id &allow-other-keys)
|
||||
kernel
|
||||
(or (jupyter-server-kernel-name server id) id)))
|
||||
(jupyter-api-get-kernel server)))
|
||||
;; FIXME: Ambiguity with `jupyter-current-server' and
|
||||
;; `current-prefix-arg'
|
||||
(when (and current-prefix-arg
|
||||
(y-or-n-p "Name REPL? "))
|
||||
(read-string "REPL Name: "))
|
||||
t nil t)))
|
||||
(jupyter-server-repl
|
||||
(jupyter-kernel
|
||||
:server server
|
||||
:id (or (jupyter-server-kernel-id-from-name server kernel-id)
|
||||
kernel-id))
|
||||
repl-name associate-buffer client-class display))
|
||||
|
||||
;;; `jupyter-server-kernel-list'
|
||||
|
||||
(defun jupyter-server-kernel-list-do-shutdown ()
|
||||
"Shutdown the kernel corresponding to the current entry."
|
||||
(interactive)
|
||||
(when-let* ((id (tabulated-list-get-id))
|
||||
(really (yes-or-no-p
|
||||
(format "Really shutdown %s kernel? "
|
||||
(aref (tabulated-list-get-entry) 0)))))
|
||||
(jupyter-api-shutdown-kernel jupyter-current-server id)
|
||||
(tabulated-list-delete-entry)))
|
||||
|
||||
(defun jupyter-server-kernel-list-do-restart ()
|
||||
"Restart the kernel corresponding to the current entry."
|
||||
(interactive)
|
||||
(when-let* ((id (tabulated-list-get-id))
|
||||
(really (yes-or-no-p "Really restart kernel? ")))
|
||||
(jupyter-api-restart-kernel jupyter-current-server id)
|
||||
(revert-buffer)))
|
||||
|
||||
(defun jupyter-server-kernel-list-do-interrupt ()
|
||||
"Interrupt the kernel corresponding to the current entry."
|
||||
(interactive)
|
||||
(when-let* ((id (tabulated-list-get-id)))
|
||||
(jupyter-api-interrupt-kernel jupyter-current-server id)
|
||||
(revert-buffer)))
|
||||
|
||||
(defun jupyter-server-kernel-list-new-repl ()
|
||||
"Connect a REPL to the kernel corresponding to the current entry."
|
||||
(interactive)
|
||||
(when-let* ((id (tabulated-list-get-id)))
|
||||
(let ((jupyter-current-client
|
||||
(jupyter-server-repl
|
||||
(jupyter-kernel
|
||||
:server jupyter-current-server
|
||||
:id id))))
|
||||
(revert-buffer)
|
||||
(jupyter-repl-pop-to-buffer))))
|
||||
|
||||
(defun jupyter-server-kernel-list-launch-kernel ()
|
||||
"Launch a new kernel on the server."
|
||||
(interactive)
|
||||
(jupyter-server-launch-kernel jupyter-current-server)
|
||||
(revert-buffer))
|
||||
|
||||
(defun jupyter-server-kernel-list-name-kernel ()
|
||||
"Name the kernel under `point'."
|
||||
(interactive)
|
||||
(when-let* ((id (tabulated-list-get-id))
|
||||
(name (read-string
|
||||
(let ((cname (jupyter-server-kernel-name
|
||||
jupyter-current-server id)))
|
||||
(if cname (format "Rename %s to: " cname)
|
||||
(format "Name kernel [%s]: " id))))))
|
||||
(when (zerop (length name))
|
||||
(jupyter-server-kernel-list-name-kernel))
|
||||
(jupyter-server-name-kernel jupyter-current-server id name)
|
||||
(revert-buffer)))
|
||||
|
||||
(defvar jupyter-server-kernel-list-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "C-c C-i") #'jupyter-server-kernel-list-do-interrupt)
|
||||
(define-key map (kbd "d") #'jupyter-server-kernel-list-do-shutdown)
|
||||
(define-key map (kbd "C-c C-d") #'jupyter-server-kernel-list-do-shutdown)
|
||||
(define-key map (kbd "C-c C-r") #'jupyter-server-kernel-list-do-restart)
|
||||
(define-key map [follow-link] nil) ;; allows mouse-1 to be activated
|
||||
(define-key map [mouse-1] #'jupyter-server-kernel-list-new-repl)
|
||||
(define-key map (kbd "RET") #'jupyter-server-kernel-list-new-repl)
|
||||
(define-key map (kbd "C-RET") #'jupyter-server-kernel-list-launch-kernel)
|
||||
(define-key map (kbd "C-<return>") #'jupyter-server-kernel-list-launch-kernel)
|
||||
(define-key map (kbd "<return>") #'jupyter-server-kernel-list-new-repl)
|
||||
(define-key map "R" #'jupyter-server-kernel-list-name-kernel)
|
||||
(define-key map "r" #'revert-buffer)
|
||||
(define-key map "g" #'revert-buffer)
|
||||
map))
|
||||
|
||||
(define-derived-mode jupyter-server-kernel-list-mode
|
||||
tabulated-list-mode "Jupyter Server Kernels"
|
||||
"A list of live kernels on a Jupyter kernel server."
|
||||
(tabulated-list-init-header)
|
||||
(tabulated-list-print)
|
||||
(let ((inhibit-read-only t)
|
||||
(url (oref jupyter-current-server url)))
|
||||
(overlay-put
|
||||
(make-overlay 1 2)
|
||||
'before-string
|
||||
(concat (propertize url 'face '(fixed-pitch default)) "\n")))
|
||||
;; So that `dired-jump' will visit the directory of the kernel server.
|
||||
(setq default-directory
|
||||
(jupyter-tramp-file-name-from-url
|
||||
(oref jupyter-current-server url))))
|
||||
|
||||
(defun jupyter-server--kernel-list-format ()
|
||||
(let* ((get-time
|
||||
(lambda (a)
|
||||
(or (get-text-property 0 'jupyter-time a)
|
||||
(let ((time (jupyter-decode-time a)))
|
||||
(prog1 time
|
||||
(put-text-property 0 1 'jupyter-time time a))))))
|
||||
(time-sort
|
||||
(lambda (a b)
|
||||
(time-less-p
|
||||
(funcall get-time (aref (nth 1 a) 2))
|
||||
(funcall get-time (aref (nth 1 b) 2)))))
|
||||
(conn-sort
|
||||
(lambda (a b)
|
||||
(< (string-to-number (aref (nth 1 a) 4))
|
||||
(string-to-number (aref (nth 1 b) 4))))))
|
||||
`[("Name" 17 t)
|
||||
("ID" 38 nil)
|
||||
("Activity" 20 ,time-sort)
|
||||
("State" 10 nil)
|
||||
("Conns." 6 ,conn-sort)]))
|
||||
|
||||
(defun jupyter-server--kernel-list-entries ()
|
||||
(cl-loop
|
||||
with names = nil
|
||||
for kernel across (jupyter-api-get-kernel jupyter-current-server)
|
||||
collect
|
||||
(cl-destructuring-bind
|
||||
(&key name id last_activity execution_state
|
||||
connections &allow-other-keys)
|
||||
kernel
|
||||
(let* ((time (jupyter-decode-time last_activity))
|
||||
(name (propertize
|
||||
(or (jupyter-server-kernel-name jupyter-current-server id)
|
||||
(let ((same (cl-remove-if-not
|
||||
(lambda (x) (string-prefix-p name x)) names)))
|
||||
(when same (setq name (format "%s<%d>" name (length same))))
|
||||
(push name names)
|
||||
name))
|
||||
'face 'font-lock-constant-face))
|
||||
(activity (propertize (jupyter-format-time-low-res time)
|
||||
'face 'font-lock-doc-face
|
||||
'jupyter-time time))
|
||||
(conns (propertize (number-to-string connections)
|
||||
'face 'shadow))
|
||||
(state (propertize execution_state
|
||||
'face (pcase execution_state
|
||||
("busy" 'warning)
|
||||
("idle" 'shadow)
|
||||
("starting" 'success)))))
|
||||
(list id (vector name id activity state conns))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-server-list-kernels (server)
|
||||
"Display a list of live kernels on SERVER.
|
||||
When called interactively, ask to select a SERVER when given a
|
||||
prefix argument otherwise the `jupyter-current-server' will be
|
||||
used."
|
||||
(interactive (list (jupyter-current-server current-prefix-arg)))
|
||||
(if (zerop (length (jupyter-api-get-kernel server)))
|
||||
(when (yes-or-no-p (format "No kernels at %s; launch one? "
|
||||
(oref server url)))
|
||||
(jupyter-server-launch-kernel server)
|
||||
(jupyter-server-list-kernels server))
|
||||
(with-current-buffer
|
||||
(jupyter-get-buffer-create (format "kernels[%s]" (oref server url)))
|
||||
(setq jupyter-current-server server)
|
||||
(if (eq major-mode 'jupyter-server-kernel-list-mode)
|
||||
(revert-buffer)
|
||||
(setq tabulated-list-format (jupyter-server--kernel-list-format)
|
||||
tabulated-list-entries #'jupyter-server--kernel-list-entries
|
||||
tabulated-list-sort-key (cons "Activity" t))
|
||||
(jupyter-server-kernel-list-mode)
|
||||
;; So that `dired-jump' will visit the directory of the kernel server.
|
||||
(setq default-directory
|
||||
(jupyter-tramp-file-name-from-url (oref server url))))
|
||||
(jupyter-display-current-buffer-reuse-window))))
|
||||
|
||||
(provide 'jupyter-server)
|
||||
|
||||
;;; jupyter-server.el ends here
|
||||
881
lisp/jupyter/jupyter-tramp.el
Normal file
881
lisp/jupyter/jupyter-tramp.el
Normal file
@@ -0,0 +1,881 @@
|
||||
;;; jupyter-tramp.el --- TRAMP interface to the Jupyter REST API -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 25 May 2019
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Integrate the Jupyter REST API contents endpoint with Emacs' file handling
|
||||
;; facilities for remote files. Adds two new remote file methods, /jpy: and
|
||||
;; /jpys:, the former being HTTP connections and the latter being HTTPS
|
||||
;; connections.
|
||||
;;
|
||||
;; If you run a local notebook server on port 8888 then reading and writing
|
||||
;; files to the server is as easy as
|
||||
;;
|
||||
;; (write-region "xxxx" nil "/jpy:localhost:happy.txt")
|
||||
;;
|
||||
;; or
|
||||
;;
|
||||
;; (find-file "/jpy:localhost:serious.py")
|
||||
;;
|
||||
;; To open a `dired' listing to the base directory of the notebook server
|
||||
;;
|
||||
;; (dired "/jpy:localhost:/")
|
||||
;;
|
||||
;; You can change the default port by changing the `tramp-default-port' entry
|
||||
;; of the jpy or jpys method in `tramp-methods' or you can specify a port
|
||||
;; inline using something like /jpy:localhost#8888:/.
|
||||
;;
|
||||
;; You can also set an entry in `tramp-default-host-alist' like
|
||||
;;
|
||||
;; (add-to-list 'tramp-default-host-alist (list "jpy" nil "HOST"))
|
||||
;;
|
||||
;; Then specifying filenames like /jpy::/foo is equivalent to /jpy:HOST:
|
||||
;;
|
||||
;; TODO: Same messages for implemented file operations that TRAMP and Emacs
|
||||
;; give.
|
||||
;;
|
||||
;; TODO: How can checkpoints be used with: `auto-save-mode',
|
||||
;; `diff-latest-backup-file', ...
|
||||
|
||||
;;; Code:
|
||||
|
||||
(eval-when-compile
|
||||
(require 'subr-x)
|
||||
(require 'tramp-compat))
|
||||
(require 'jupyter-rest-api)
|
||||
(require 'jupyter-server)
|
||||
(require 'tramp)
|
||||
(require 'tramp-cache)
|
||||
|
||||
(defgroup jupyter-tramp nil
|
||||
"TRAMP integration with the Jupyter Contents REST API"
|
||||
:group 'jupyter)
|
||||
|
||||
(declare-function jupyter-decode-time "jupyter-messages" (str))
|
||||
|
||||
(defmacro jupyter-tramp-with-api-connection (file &rest body)
|
||||
"Set `jupyter-current-server' based on FILE, evaluate BODY.
|
||||
FILE must be a remote file name recognized as corresponding to a
|
||||
file on a server that can be communicated with using the Jupyter
|
||||
notebook REST API.
|
||||
|
||||
Note, BODY is wrapped with a call to
|
||||
`with-parsed-tramp-file-name' so that the variables method, user,
|
||||
host, localname, ..., are all bound to values parsed from FILE."
|
||||
(declare (indent 1) (debug ([&or stringp symbolp] body)))
|
||||
`(with-parsed-tramp-file-name ,file nil
|
||||
;; FIXME: There is a dilemma here, a `jupyter-server' is a more particular
|
||||
;; object than what we need. There is really no reason to have it here, we
|
||||
;; just need a `jupyter-rest-client'. Is there a reason this needs to be
|
||||
;; here?
|
||||
(let ((jupyter-current-server
|
||||
(jupyter-tramp-server-from-file-name ,file)))
|
||||
,@body)))
|
||||
|
||||
;;; File name handler setup
|
||||
|
||||
;; Actual functions implemented by `jupyter-tramp' all the others are either
|
||||
;; ignored or handled by the TRAMP handlers.
|
||||
;;
|
||||
;; jupyter-tramp-copy-file
|
||||
;; jupyter-tramp-delete-directory
|
||||
;; jupyter-tramp-delete-file
|
||||
;; jupyter-tramp-expand-file-name
|
||||
;; jupyter-tramp-file-attributes
|
||||
;; jupyter-tramp-file-directory-p
|
||||
;; jupyter-tramp-file-exists-p
|
||||
;; jupyter-tramp-file-local-copy
|
||||
;; jupyter-tramp-file-name-all-completions
|
||||
;; jupyter-tramp-file-remote-p
|
||||
;; jupyter-tramp-file-symlink-p
|
||||
;; jupyter-tramp-file-writable-p
|
||||
;; jupyter-tramp-make-directory-internal
|
||||
;; jupyter-tramp-rename-file
|
||||
;; jupyter-tramp-write-region
|
||||
;;;###autoload
|
||||
(defconst jupyter-tramp-file-name-handler-alist
|
||||
'((access-file . tramp-handle-access-file)
|
||||
(add-name-to-file . tramp-handle-add-name-to-file)
|
||||
;; `byte-compiler-base-file-name' performed by default handler.
|
||||
;; `copy-directory' performed by default handler.
|
||||
(copy-file . jupyter-tramp-copy-file)
|
||||
(delete-directory . jupyter-tramp-delete-directory)
|
||||
(delete-file . jupyter-tramp-delete-file)
|
||||
;; TODO: Use the `checkpoint' file? I think we can only create a checkpoint
|
||||
;; or restore a file from a checkpoint so maybe we can do something with
|
||||
;; auto-save and checkpoints?
|
||||
;; `diff-latest-backup-file' performed by default handler.
|
||||
(directory-file-name . tramp-handle-directory-file-name)
|
||||
(directory-files . tramp-handle-directory-files)
|
||||
(directory-files-and-attributes . tramp-handle-directory-files-and-attributes)
|
||||
(dired-compress-file . ignore)
|
||||
(dired-uncache . tramp-handle-dired-uncache)
|
||||
(expand-file-name . jupyter-tramp-expand-file-name)
|
||||
(file-accessible-directory-p . tramp-handle-file-accessible-directory-p)
|
||||
(file-acl . ignore)
|
||||
(file-attributes . jupyter-tramp-file-attributes)
|
||||
(file-directory-p . jupyter-tramp-file-directory-p)
|
||||
(file-equal-p . tramp-handle-file-equal-p)
|
||||
(file-executable-p . tramp-handle-file-exists-p)
|
||||
(file-exists-p . jupyter-tramp-file-exists-p)
|
||||
(file-in-directory-p . tramp-handle-file-in-directory-p)
|
||||
(file-local-copy . jupyter-tramp-file-local-copy)
|
||||
(file-modes . tramp-handle-file-modes)
|
||||
(file-name-all-completions . jupyter-tramp-file-name-all-completions)
|
||||
(file-name-as-directory . tramp-handle-file-name-as-directory)
|
||||
(file-name-case-insensitive-p . tramp-handle-file-name-case-insensitive-p)
|
||||
(file-name-completion . tramp-handle-file-name-completion)
|
||||
(file-name-directory . tramp-handle-file-name-directory)
|
||||
(file-name-nondirectory . tramp-handle-file-name-nondirectory)
|
||||
;; `file-name-sans-versions' performed by default handler.
|
||||
(file-newer-than-file-p . tramp-handle-file-newer-than-file-p)
|
||||
(file-notify-add-watch . tramp-handle-file-notify-add-watch)
|
||||
(file-notify-rm-watch . tramp-handle-file-notify-rm-watch)
|
||||
(file-notify-valid-p . tramp-handle-file-notify-valid-p)
|
||||
(file-ownership-preserved-p . ignore)
|
||||
(file-readable-p . tramp-handle-file-exists-p)
|
||||
(file-regular-p . tramp-handle-file-regular-p)
|
||||
;; NOTE: We can't use `tramp-handle-file-remote-p' since it expects a
|
||||
;; process to check for the connected argument whereas we are using an HTTP
|
||||
;; connection which may or may not be as long lived as something like an
|
||||
;; SSH connection as the liveness depends on the Keep-Alive header of an
|
||||
;; HTTP request.
|
||||
(file-remote-p . jupyter-tramp-file-remote-p)
|
||||
(file-selinux-context . tramp-handle-file-selinux-context)
|
||||
(file-symlink-p . jupyter-tramp-file-symlink-p)
|
||||
(file-system-info . ignore)
|
||||
(file-truename . tramp-handle-file-truename)
|
||||
(file-writable-p . jupyter-tramp-file-writable-p)
|
||||
;; TODO: Can we do something here with checkpoints on the remote?
|
||||
(find-backup-file-name . ignore)
|
||||
;; `find-file-noselect' performed by default handler.
|
||||
;; `get-file-buffer' performed by default handler.
|
||||
(insert-directory . tramp-handle-insert-directory)
|
||||
;; Uses `file-local-copy' to get the contents so be sure thats implemented
|
||||
(insert-file-contents . tramp-handle-insert-file-contents)
|
||||
(load . tramp-handle-load)
|
||||
(make-auto-save-file-name . tramp-handle-make-auto-save-file-name)
|
||||
(make-directory . jupyter-tramp-make-directory)
|
||||
(make-directory-internal . ignore)
|
||||
(make-nearby-temp-file . tramp-handle-make-nearby-temp-file)
|
||||
(make-symbolic-link . tramp-handle-make-symbolic-link)
|
||||
;; `process-file' performed by default handler.
|
||||
(rename-file . jupyter-tramp-rename-file)
|
||||
(set-file-acl . ignore)
|
||||
(set-file-modes . ignore)
|
||||
(set-file-selinux-context . ignore)
|
||||
(set-file-times . ignore)
|
||||
(set-visited-file-modtime . tramp-handle-set-visited-file-modtime)
|
||||
;; `shell-command' performed by default handler.
|
||||
;; `start-file-process' performed by default handler.
|
||||
(substitute-in-file-name . tramp-handle-substitute-in-file-name)
|
||||
(temporary-file-directory . tramp-handle-temporary-file-directory)
|
||||
;; Important that we have this so that `call-process' and friends don't try
|
||||
;; to set a Jupyter notebook directory as a directory in which a process
|
||||
;; should run.
|
||||
(unhandled-file-name-directory . ignore)
|
||||
(vc-registered . ignore)
|
||||
(verify-visited-file-modtime . tramp-handle-verify-visited-file-modtime)
|
||||
(write-region . jupyter-tramp-write-region))
|
||||
"Alist of handler functions for Tramp Jupyter method.
|
||||
Operations not mentioned here will be handled by the default Emacs primitives.")
|
||||
|
||||
;;;###autoload
|
||||
(defconst jupyter-tramp-methods '("jpy" "jpys")
|
||||
"Methods to connect Jupyter kernel servers.")
|
||||
|
||||
;;;###autoload
|
||||
(with-eval-after-load 'tramp
|
||||
(mapc (lambda (method)
|
||||
(add-to-list
|
||||
'tramp-methods
|
||||
(list method
|
||||
(list 'tramp-default-port 8888)
|
||||
(list 'tramp-tmpdir "/tmp"))))
|
||||
jupyter-tramp-methods)
|
||||
(tramp-register-foreign-file-name-handler
|
||||
'jupyter-tramp-file-name-p 'jupyter-tramp-file-name-handler)
|
||||
(add-to-list 'tramp-default-host-alist
|
||||
'("\\`jpys?\\'" nil "localhost")))
|
||||
|
||||
;;;###autoload
|
||||
(defsubst jupyter-tramp-file-name-method-p (method)
|
||||
"Return METHOD if it corresponds to a Jupyter filename method or nil."
|
||||
(and (string-match-p "\\`jpys?\\'" method) method))
|
||||
|
||||
;; Port of `tramp-ensure-dissected-file-name' in Emacs 29
|
||||
;;;###autoload
|
||||
(defun jupyter-tramp-ensure-dissected-file-name (vec-or-filename)
|
||||
(cond
|
||||
((tramp-file-name-p vec-or-filename) vec-or-filename)
|
||||
((tramp-tramp-file-p vec-or-filename)
|
||||
(tramp-dissect-file-name vec-or-filename))))
|
||||
|
||||
;; NOTE: Needs to be a `defsubst' to avoid recursive loading.
|
||||
;;;###autoload
|
||||
(defsubst jupyter-tramp-file-name-p (vec-or-filename)
|
||||
"If FILENAME is a Jupyter filename, return its method otherwise nil."
|
||||
(when-let* ((vec (jupyter-tramp-ensure-dissected-file-name vec-or-filename)))
|
||||
(jupyter-tramp-file-name-method-p (tramp-file-name-method vec))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-tramp-file-name-handler (operation &rest args)
|
||||
(let ((handler (assq operation jupyter-tramp-file-name-handler-alist)))
|
||||
(if (not handler)
|
||||
(tramp-run-real-handler operation args)
|
||||
(apply (cdr handler) args))))
|
||||
|
||||
;;;; Converting file names to authenticated `jupyter-rest-client' instances
|
||||
|
||||
(defvar tramp-current-method)
|
||||
(defvar tramp-current-user)
|
||||
(defvar tramp-current-domain)
|
||||
(defvar tramp-current-host)
|
||||
(defvar tramp-current-port)
|
||||
|
||||
(defun jupyter-tramp-read-passwd (filename &optional prompt)
|
||||
"Read a password based off of FILENAME's TRAMP filename components.
|
||||
Use PROMPT to prompt the user for the password if needed, PROMPT
|
||||
defaults to \"Password:\"."
|
||||
(unless (jupyter-tramp-file-name-p filename)
|
||||
(error "Not a Jupyter filename"))
|
||||
(with-parsed-tramp-file-name filename nil
|
||||
(let ((tramp-current-method method)
|
||||
(tramp-current-user (or user user-login-name))
|
||||
(tramp-current-domain nil)
|
||||
(tramp-current-host host)
|
||||
(tramp-current-port port))
|
||||
(tramp-read-passwd nil (or prompt "Password: ")))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-tramp-file-name-from-url (url)
|
||||
"Return a Jupyter TRAMP filename for the root directory of a kernel server.
|
||||
The filename is based off of URL's host and port if any."
|
||||
(let ((url (if (url-p url) url
|
||||
(url-generic-parse-url url))))
|
||||
(format "/jpy%s:%s%s:/"
|
||||
(if (equal (url-type url) "https") "s" "")
|
||||
(url-host url)
|
||||
(let ((port (url-port-if-non-default url)))
|
||||
(if port (format "#%d" port) "")))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-tramp-url-from-file-name (filename)
|
||||
"Return a URL string based off the method, host, and port of FILENAME."
|
||||
(with-parsed-tramp-file-name filename nil
|
||||
(unless port (setq port (when (functionp 'tramp-file-name-port-or-default)
|
||||
;; This function was introduced in Emacs 26.1
|
||||
(tramp-file-name-port-or-default v))))
|
||||
(format "%s://%s%s" (if (equal method "jpys") "https" "http")
|
||||
host (if port (format ":%s" port) ""))))
|
||||
|
||||
;;;###autoload
|
||||
(defun jupyter-tramp-server-from-file-name (filename)
|
||||
"Return a `jupyter-server' instance based off of FILENAME's remote components.
|
||||
If the connection has not been authenticated by the server,
|
||||
attempt to authenticate the connection. Raise an error if that
|
||||
fails."
|
||||
(unless (jupyter-tramp-file-name-p filename)
|
||||
(error "Not a Jupyter filename"))
|
||||
(with-parsed-tramp-file-name filename nil
|
||||
(let* ((url (jupyter-tramp-url-from-file-name filename))
|
||||
(client (jupyter-server :url url)))
|
||||
(prog1 client
|
||||
(unless (jupyter-api-server-accessible-p client)
|
||||
(cond
|
||||
((y-or-n-p (format "Login to %s using a token? " url))
|
||||
(jupyter-api-authenticate client 'token))
|
||||
(t
|
||||
;; This is here so that reading a password using
|
||||
;; `tramp-read-passwd' via `jupyter-tramp-read-passwd' will check
|
||||
;; auth sources.
|
||||
(tramp-set-connection-property v "first-password-request" t)
|
||||
(jupyter-api-authenticate client
|
||||
'password
|
||||
(let ((remote (file-remote-p filename)))
|
||||
(lambda ()
|
||||
(jupyter-tramp-read-passwd
|
||||
filename (format "Password [%s]: " remote))))))))))))
|
||||
|
||||
;;; Getting information about file models
|
||||
|
||||
(defalias 'jupyter-tramp-flush-file-properties
|
||||
(if (functionp 'tramp-flush-file-properties)
|
||||
;; New in Emacs 27
|
||||
'tramp-flush-file-properties
|
||||
'tramp-flush-file-property))
|
||||
|
||||
(defun jupyter-tramp--get-directory-or-file-model (file localname path no-content)
|
||||
(cond
|
||||
(no-content
|
||||
(jupyter-tramp-get-file-model (file-name-directory file)))
|
||||
(t
|
||||
(condition-case err
|
||||
;; Unset `signal-hook-function' so that TRAMP in Emacs >= 27 does not
|
||||
;; mess with the signal data until we have a chance to look at it.
|
||||
(let (signal-hook-function)
|
||||
(jupyter-api-get-file-model jupyter-current-server localname))
|
||||
(jupyter-api-http-error
|
||||
(cl-destructuring-bind (_ code msg) err
|
||||
(if (and (eq code 404)
|
||||
(string-match-p
|
||||
"\\(?:No such \\)?file or directory\\(?:does not exist\\)?"
|
||||
msg))
|
||||
(list :path path :name nil
|
||||
;; If a file doesn't exist we need to check if the
|
||||
;; containing directory is writable to determine if
|
||||
;; FILE is.
|
||||
:writable (plist-get
|
||||
(jupyter-tramp-get-file-model
|
||||
(file-name-directory
|
||||
(directory-file-name file))
|
||||
'no-content)
|
||||
:writable))
|
||||
(signal (car err) (cdr err)))))
|
||||
(error (signal (car err) (cdr err)))))))
|
||||
|
||||
(defun jupyter-tramp--get-file-model (file localname no-content)
|
||||
(let* ((path (jupyter-api-content-path localname))
|
||||
(model (jupyter-tramp--get-directory-or-file-model
|
||||
file localname path no-content)))
|
||||
(or (jupyter-api-find-model path model)
|
||||
;; We reach here when MODEL is a directory that does
|
||||
;; not contain PATH. PATH is writable if the
|
||||
;; directory is.
|
||||
(list :path path :name nil
|
||||
:writable (plist-get model :writable)))))
|
||||
|
||||
(defun jupyter-tramp-get-file-model (file &optional no-content)
|
||||
"Return a model of FILE or raise an error.
|
||||
For non-existent files the model
|
||||
|
||||
(:path PATH :name nil :writable WRITABLE)
|
||||
|
||||
is returned, where PATH is a local path name to FILE on the
|
||||
server, i.e. excludes the remote part of FILE. WRITABLE will be t
|
||||
if FILE can be created on the server or nil if PATH is outside
|
||||
the base directory the server was started in.
|
||||
|
||||
When NO-CONTENT is non-nil, return a model for file that excludes
|
||||
:content if an actual request needs to be made. The :content key
|
||||
may or may not be present in this case. If NO-CONTENT is nil,
|
||||
guarantee that we request FILE's content as well.
|
||||
|
||||
See `jupyter-tramp-get-file-model' for details on what a file model is."
|
||||
(setq file (expand-file-name file))
|
||||
(jupyter-tramp-with-api-connection file
|
||||
(let ((value (or (tramp-get-file-property v localname "model" nil)
|
||||
(when no-content
|
||||
(tramp-get-file-property v localname "nc-model" nil)))))
|
||||
(unless value
|
||||
(setq value (jupyter-tramp--get-file-model file localname no-content))
|
||||
(tramp-set-file-property
|
||||
v localname (if no-content "nc-model" "model") value))
|
||||
value)))
|
||||
|
||||
(defun jupyter-tramp-flush-file-and-directory-properties (filename)
|
||||
(with-parsed-tramp-file-name filename nil
|
||||
(jupyter-tramp-flush-file-properties v localname)
|
||||
(jupyter-tramp-flush-file-properties v (file-name-directory localname))))
|
||||
|
||||
;;; Predicates
|
||||
|
||||
(defun jupyter-tramp--barf-if-not-file (file)
|
||||
(unless (file-exists-p file)
|
||||
(error "No such file or directory: %s" file)))
|
||||
|
||||
(defun jupyter-tramp--barf-if-not-regular-file (file)
|
||||
(jupyter-tramp--barf-if-not-file file)
|
||||
(unless (file-regular-p file)
|
||||
(error "Not a file: %s" file)))
|
||||
|
||||
(defun jupyter-tramp--barf-if-not-directory (directory)
|
||||
(jupyter-tramp--barf-if-not-file directory)
|
||||
(unless (file-directory-p directory)
|
||||
(error "Not a directory: %s" (expand-file-name directory))))
|
||||
|
||||
(defun jupyter-tramp-file-writable-p (filename)
|
||||
(jupyter-tramp-with-api-connection filename
|
||||
(plist-get (jupyter-tramp-get-file-model filename 'no-content) :writable)))
|
||||
|
||||
;; Actually this may not be true, but there is no way to tell if a file is a
|
||||
;; symlink or not
|
||||
(defun jupyter-tramp-file-symlink-p (_filename)
|
||||
nil)
|
||||
|
||||
(defun jupyter-tramp-file-directory-p (filename)
|
||||
(jupyter-tramp-with-api-connection filename
|
||||
(equal (plist-get (jupyter-tramp-get-file-model filename 'no-content) :type)
|
||||
"directory")))
|
||||
|
||||
(defvar url-http-open-connections)
|
||||
|
||||
(defun jupyter-tramp-connected-p (vec-or-filename)
|
||||
"Return non-nil if connected to a Jupyter based remote host."
|
||||
(let* ((vec (tramp-ensure-dissected-file-name vec-or-filename))
|
||||
(port (tramp-file-name-port-or-default vec))
|
||||
(key (cons (tramp-file-name-host vec)
|
||||
(if (numberp port) port
|
||||
(string-to-number port)))))
|
||||
(catch 'connected
|
||||
(dolist (conn (gethash key url-http-open-connections))
|
||||
(when (memq (process-status conn) '(run open connect))
|
||||
(throw 'connected t))))))
|
||||
|
||||
(defun jupyter-tramp-file-remote-p (file &optional identification connected)
|
||||
(when (file-name-absolute-p file)
|
||||
(with-parsed-tramp-file-name file nil
|
||||
(when (or (null connected)
|
||||
(jupyter-tramp-connected-p v))
|
||||
(cl-case identification
|
||||
(method method)
|
||||
(host host)
|
||||
(user user)
|
||||
(localname localname)
|
||||
(t (tramp-make-tramp-file-name v "")))))))
|
||||
|
||||
;; Adapted from `tramp-handle-file-exists-p'
|
||||
(defun jupyter-tramp-file-exists-p (filename)
|
||||
;; `file-exists-p' is used as predicate in file name completion.
|
||||
;; We don't want to run it when `non-essential' is t, or there is
|
||||
;; no connection process yet.
|
||||
(when (or (jupyter-tramp-connected-p filename)
|
||||
(not non-essential))
|
||||
(with-parsed-tramp-file-name (expand-file-name filename) nil
|
||||
(with-tramp-file-property v localname "file-exists-p"
|
||||
(not (null (file-attributes filename)))))))
|
||||
|
||||
|
||||
;;; File name manipulation
|
||||
|
||||
(defun jupyter-tramp-expand-file-name (name &optional directory)
|
||||
;; From `tramp-sh-handle-expand-file-name'
|
||||
(setq directory (or directory default-directory "/"))
|
||||
(unless (file-name-absolute-p name)
|
||||
(setq name (concat (file-name-as-directory directory) name)))
|
||||
(if (tramp-tramp-file-p name)
|
||||
(let ((v (tramp-dissect-file-name name)))
|
||||
(if (jupyter-tramp-file-name-method-p (tramp-file-name-method v))
|
||||
(tramp-make-tramp-file-name
|
||||
v
|
||||
(tramp-drop-volume-letter
|
||||
(tramp-run-real-handler
|
||||
'expand-file-name (list (tramp-file-name-localname v) "/"))))
|
||||
(let ((tramp-foreign-file-name-handler-alist
|
||||
(remove (cons 'jupyter-tramp-file-name-p
|
||||
'jupyter-tramp-file-name-handler)
|
||||
tramp-foreign-file-name-handler-alist)))
|
||||
(expand-file-name name))))
|
||||
(tramp-run-real-handler 'expand-file-name (list name directory))))
|
||||
|
||||
;;; File operations
|
||||
|
||||
;; Adapted from `tramp-smb-handle-rename-file'
|
||||
(defun jupyter-tramp-rename-file (filename newname &optional ok-if-already-exists)
|
||||
(setq filename (expand-file-name filename)
|
||||
newname (expand-file-name newname))
|
||||
|
||||
(when (and (not ok-if-already-exists)
|
||||
(file-exists-p newname))
|
||||
(tramp-error
|
||||
(tramp-dissect-file-name
|
||||
(if (tramp-tramp-file-p filename) filename newname))
|
||||
'file-already-exists newname))
|
||||
|
||||
(with-tramp-progress-reporter
|
||||
(tramp-dissect-file-name
|
||||
(if (tramp-tramp-file-p filename) filename newname))
|
||||
0 (format "Renaming %s to %s" filename newname)
|
||||
|
||||
(if (and (not (file-exists-p newname))
|
||||
(tramp-equal-remote filename newname))
|
||||
;; We can rename directly.
|
||||
(jupyter-tramp-with-api-connection filename
|
||||
;; We must also flush the cache of the directory, because
|
||||
;; `file-attributes' reads the values from there.
|
||||
(jupyter-tramp-flush-file-and-directory-properties filename)
|
||||
(jupyter-tramp-flush-file-and-directory-properties newname)
|
||||
(jupyter-api-rename-file jupyter-current-server
|
||||
filename newname))
|
||||
|
||||
;; We must rename via copy.
|
||||
(copy-file filename newname ok-if-already-exists)
|
||||
(if (file-directory-p filename)
|
||||
(delete-directory filename 'recursive)
|
||||
(delete-file filename)))))
|
||||
|
||||
;; NOTE: Deleting to trash is configured on the server.
|
||||
(defun jupyter-tramp-delete-directory (directory &optional recursive _trash)
|
||||
(jupyter-tramp--barf-if-not-directory directory)
|
||||
(jupyter-tramp-with-api-connection directory
|
||||
(jupyter-tramp-flush-file-properties v localname)
|
||||
(let ((files (cl-remove-if
|
||||
(lambda (x) (member x '("." "..")))
|
||||
(directory-files directory nil nil t))))
|
||||
(unless (or recursive (not files))
|
||||
(error "Directory %s not empty" directory))
|
||||
(let ((deleted
|
||||
;; Try to delete the directory, if we get an error because its not
|
||||
;; empty, manually delete all files below and then try again.
|
||||
(condition-case err
|
||||
(prog1 t
|
||||
;; Unset `signal-hook-function' so that TRAMP in Emacs >= 27
|
||||
;; does not mess with the signal data until we have a chance
|
||||
;; to look at it.
|
||||
(let (signal-hook-function)
|
||||
(jupyter-api-delete-file
|
||||
jupyter-current-server
|
||||
directory)))
|
||||
(jupyter-api-http-error
|
||||
(unless (and (= (nth 1 err) 400)
|
||||
(string-match-p "not empty" (caddr err)))
|
||||
(signal (car err) (cdr err))))
|
||||
(error (signal (car err) (cdr err))))))
|
||||
(unless deleted
|
||||
;; Recursive delete, we need to do this manually since we can get a 400
|
||||
;; error on Windows when deleting to trash and also in general when not
|
||||
;; deleting to trash if the directory isn't empty, see
|
||||
;; jupyter/notebook/notebook/services/contents/filemanager.py
|
||||
(while files
|
||||
(let ((file (expand-file-name (pop files) directory)))
|
||||
(if (file-directory-p file)
|
||||
(delete-directory file recursive)
|
||||
(delete-file file))))
|
||||
(jupyter-api-delete-file jupyter-current-server directory))))
|
||||
;; Need to uncache both the file and its directory
|
||||
(jupyter-tramp-flush-file-and-directory-properties directory)))
|
||||
|
||||
(defun jupyter-tramp-delete-file (filename &optional _trash)
|
||||
(jupyter-tramp--barf-if-not-regular-file filename)
|
||||
(jupyter-tramp-with-api-connection filename
|
||||
(jupyter-api-delete-file jupyter-current-server filename)
|
||||
;; Need to uncache both the file and its directory
|
||||
(jupyter-tramp-flush-file-and-directory-properties filename)))
|
||||
|
||||
;; Adapted from `tramp-smb-handle-copy-file'
|
||||
(defun jupyter-tramp-copy-file (filename newname &optional ok-if-already-exists
|
||||
keep-date _preserve-uid-gid _preserve-permissions)
|
||||
(setq filename (expand-file-name filename)
|
||||
newname (expand-file-name newname))
|
||||
(with-tramp-progress-reporter
|
||||
(tramp-dissect-file-name
|
||||
(if (tramp-tramp-file-p filename) filename newname))
|
||||
0 (format "Copying %s to %s" filename newname)
|
||||
|
||||
(if (file-directory-p filename)
|
||||
(copy-directory filename newname keep-date 'parents 'copy-contents)
|
||||
|
||||
(cond
|
||||
((tramp-equal-remote filename newname)
|
||||
(jupyter-tramp-with-api-connection newname
|
||||
(when (and (not ok-if-already-exists)
|
||||
(file-exists-p newname))
|
||||
(tramp-error v 'file-already-exists newname))
|
||||
(jupyter-api-copy-file jupyter-current-server filename newname)))
|
||||
(t
|
||||
(let ((tmpfile (file-local-copy filename)))
|
||||
(if tmpfile
|
||||
;; Remote filename.
|
||||
(condition-case err
|
||||
(rename-file tmpfile newname ok-if-already-exists)
|
||||
((error quit)
|
||||
(delete-file tmpfile)
|
||||
(signal (car err) (cdr err))))
|
||||
|
||||
;; Remote newname.
|
||||
(when (and (file-directory-p newname)
|
||||
(directory-name-p newname))
|
||||
(setq newname
|
||||
(expand-file-name (file-name-nondirectory filename) newname)))
|
||||
|
||||
(with-parsed-tramp-file-name newname nil
|
||||
(when (and (not ok-if-already-exists)
|
||||
(file-exists-p newname))
|
||||
(tramp-error v 'file-already-exists newname))
|
||||
|
||||
(with-temp-file newname
|
||||
(insert-file-contents-literally filename)))))))
|
||||
|
||||
(when (tramp-tramp-file-p newname)
|
||||
;; We must also flush the cache of the directory, because
|
||||
;; `file-attributes' reads the values from there.
|
||||
(jupyter-tramp-flush-file-and-directory-properties newname)))))
|
||||
|
||||
;; Ported from `trapm-skeleton-make-directory' in Emacs 29
|
||||
(defun jupyter-tramp-make-directory (dir &optional parents)
|
||||
(jupyter-tramp-with-api-connection dir
|
||||
(let* ((dir (directory-file-name (expand-file-name dir)))
|
||||
(par (file-name-directory dir)))
|
||||
(when (and (null parents) (file-exists-p dir))
|
||||
(tramp-error v 'file-already-exists dir))
|
||||
;; Make missing directory parts.
|
||||
(when parents
|
||||
(unless (file-directory-p par)
|
||||
(make-directory par parents)))
|
||||
;; Just do it.
|
||||
(if (file-exists-p dir) t
|
||||
(jupyter-tramp-flush-file-and-directory-properties dir)
|
||||
(jupyter-api-make-directory jupyter-current-server dir)
|
||||
nil))))
|
||||
|
||||
;;; File name completion
|
||||
|
||||
(defun jupyter-tramp-file-name-all-completions (filename directory)
|
||||
(when (jupyter-tramp-file-name-p directory)
|
||||
(all-completions
|
||||
filename (mapcar #'car (jupyter-tramp-directory-file-models directory))
|
||||
(lambda (f)
|
||||
(let ((ext (file-name-extension f t)))
|
||||
(and (or (null ext) (not (member ext completion-ignored-extensions)))
|
||||
(or (null completion-regexp-list)
|
||||
(not (cl-loop for re in completion-regexp-list
|
||||
thereis (not (string-match-p re f)))))))))))
|
||||
|
||||
;;; Insert file contents
|
||||
|
||||
;; XXX: WIP
|
||||
(defun jupyter-tramp--recover-this-file (orig)
|
||||
"If the `current-buffer' is Jupyter file, revert back to a checkpoint.
|
||||
If no checkpoints exist, revert back to the file that exists on
|
||||
the server. For any other file, call ORIG, which is the function
|
||||
`recover-this-file'"
|
||||
(interactive)
|
||||
(let ((file (buffer-file-name)))
|
||||
(if (not (jupyter-tramp-file-name-p file)) (funcall orig)
|
||||
(jupyter-tramp-with-api-connection file
|
||||
(let ((checkpoint (jupyter-api-get-latest-checkpoint
|
||||
jupyter-current-server
|
||||
file)))
|
||||
(when checkpoint
|
||||
(jupyter-api-restore-checkpoint
|
||||
jupyter-current-server
|
||||
file checkpoint))
|
||||
(let ((tmpfile (file-local-copy file)))
|
||||
(unwind-protect
|
||||
(save-restriction
|
||||
(widen)
|
||||
(insert-file-contents tmpfile nil nil nil 'replace)
|
||||
;; TODO: What else needs to be done here
|
||||
(set-buffer-modified-p nil))
|
||||
(delete-file tmpfile))))))))
|
||||
|
||||
;; TODO: Something that doesn't use advise
|
||||
;; (advice-add 'recover-this-file :around 'jupyter-tramp--recover-this-file)
|
||||
|
||||
;; TODO: What to do about reading and writing large files? Also the out of
|
||||
;; band functions of TRAMP.
|
||||
;;
|
||||
;; Adapted from `tramp-sh-handle-write-region'
|
||||
(defun jupyter-tramp-write-region (start end filename &optional append visit lockname mustbenew)
|
||||
(setq filename (expand-file-name filename))
|
||||
(when (and mustbenew (file-exists-p filename)
|
||||
(or (eq mustbenew 'excl)
|
||||
(not
|
||||
(y-or-n-p
|
||||
(format "File %s exists; overwrite anyway? " filename)))))
|
||||
(signal 'file-already-exists (list filename)))
|
||||
(jupyter-tramp-with-api-connection filename
|
||||
;; Ensure we don't use stale model contents
|
||||
(jupyter-tramp-flush-file-and-directory-properties filename)
|
||||
(if (and append (file-exists-p filename))
|
||||
(let* ((tmpfile (file-local-copy filename))
|
||||
(model (jupyter-tramp-get-file-model filename))
|
||||
(binary (jupyter-api-binary-content-p model))
|
||||
(coding-system-for-read (if binary 'no-conversion 'utf-8))
|
||||
(coding-system-for-write (if binary 'no-conversion 'utf-8)))
|
||||
(condition-case err
|
||||
(tramp-run-real-handler
|
||||
'write-region
|
||||
(list start end tmpfile append 'no-message lockname mustbenew))
|
||||
(error
|
||||
(delete-file tmpfile)
|
||||
(signal (car err) (cdr err))))
|
||||
(unwind-protect
|
||||
(with-temp-buffer
|
||||
(insert-file-contents-literally tmpfile)
|
||||
(decode-coding-region (point-min) (point-max) 'utf-8-auto)
|
||||
(jupyter-api-write-file-content
|
||||
jupyter-current-server
|
||||
filename (buffer-string) binary))
|
||||
(delete-file tmpfile)))
|
||||
(let ((source (if (stringp start) start
|
||||
(if (null start) (buffer-string)
|
||||
(buffer-substring-no-properties start end))))
|
||||
(binary (coding-system-equal
|
||||
(or coding-system-for-write
|
||||
(if enable-multibyte-characters 'utf-8
|
||||
'binary))
|
||||
'binary)))
|
||||
(jupyter-api-write-file-content
|
||||
jupyter-current-server
|
||||
filename source binary)
|
||||
;; Adapted from `tramp-sh-handle-write-region'
|
||||
(when (or (eq visit t) (stringp visit))
|
||||
(let ((file-attr (file-attributes filename)))
|
||||
(when (stringp visit)
|
||||
(setq buffer-file-name visit))
|
||||
(set-buffer-modified-p nil)
|
||||
(set-visited-file-modtime
|
||||
;; We must pass modtime explicitly, because FILENAME can
|
||||
;; be different from (buffer-file-name), f.e. if
|
||||
;; `file-precious-flag' is set.
|
||||
(or (file-attribute-modification-time file-attr)
|
||||
(current-time)))))
|
||||
(when (and (null noninteractive)
|
||||
(or (eq visit t) (null visit) (stringp visit)))
|
||||
(tramp-message v 0 "Wrote %s" filename))))
|
||||
;; Another flush after writing for consistency
|
||||
;; TODO: Figure out more exactly where these should go
|
||||
(jupyter-tramp-flush-file-and-directory-properties filename)))
|
||||
|
||||
;; TODO: Set `jupyter-current-server' in every buffer that visits a file, this
|
||||
;; way `jupyter-current-server' will always use the right server for file
|
||||
;; operations if there happen to be more than one server.
|
||||
;;
|
||||
;; NOTE: Not currently used since `file-local-copy' is used as a way to get
|
||||
;; files from the server and then `write-region' is used to write them back.
|
||||
(defun jupyter-tramp-insert-file-contents (filename &optional visit beg end replace)
|
||||
(setq filename (expand-file-name filename))
|
||||
(let ((do-visit
|
||||
(lambda ()
|
||||
(setq buffer-file-name filename)
|
||||
(set-buffer-modified-p nil))))
|
||||
(condition-case err
|
||||
(jupyter-tramp--barf-if-not-file filename)
|
||||
(error
|
||||
(and visit (funcall do-visit))
|
||||
(signal (car err) (cdr err))))
|
||||
(jupyter-tramp-with-api-connection filename
|
||||
;; Ensure we grab a fresh model since the cached version may be out of
|
||||
;; sync with the server.
|
||||
(jupyter-tramp-flush-file-properties v localname)
|
||||
(let ((model (jupyter-tramp-get-file-model filename)))
|
||||
(when (and visit (jupyter-api-binary-content-p model))
|
||||
(set-buffer-multibyte nil))
|
||||
(let ((pos (point)))
|
||||
(jupyter-api-insert-model-content model replace beg end)
|
||||
(and visit (funcall do-visit))
|
||||
(list filename (- (point) pos)))))))
|
||||
|
||||
(defun jupyter-tramp-file-local-copy (filename)
|
||||
(jupyter-tramp-with-api-connection filename
|
||||
(unless (file-exists-p filename)
|
||||
(tramp-error
|
||||
v 'file-missing
|
||||
"Cannot make local copy of non-existing file `%s'" filename))
|
||||
;; Ensure we grab a fresh model since the cached version may be out of
|
||||
;; sync with the server.
|
||||
(jupyter-tramp-flush-file-properties v localname)
|
||||
(let ((model (jupyter-tramp-get-file-model filename)))
|
||||
(when (jupyter-api-notebook-p model)
|
||||
(error "Notebooks not supported yet"))
|
||||
(let ((coding-system-for-write
|
||||
(if (jupyter-api-binary-content-p model)
|
||||
'no-conversion
|
||||
'utf-8)))
|
||||
(tramp-run-real-handler
|
||||
'make-temp-file
|
||||
(list "jupyter-tramp." nil (file-name-extension filename t)
|
||||
(with-current-buffer (jupyter-api-content-buffer model)
|
||||
(buffer-string))))))))
|
||||
|
||||
;;; File/directory attributes
|
||||
|
||||
(defun jupyter-tramp-file-attributes-from-model (model &optional id-format)
|
||||
;; :name is nil if the corresponding file of MODEL doesn't exist, see
|
||||
;; `jupyter-tramp-get-file-model'.
|
||||
(when (plist-get model :name)
|
||||
(let* ((dirp (equal (plist-get model :type) "directory"))
|
||||
(last-modified (plist-get model :last_modified))
|
||||
(created (plist-get model :created))
|
||||
(mtime (or (and last-modified (jupyter-decode-time last-modified))
|
||||
(current-time)))
|
||||
(ctime (or (and created (jupyter-decode-time created))
|
||||
(current-time)))
|
||||
;; Sometimes the model doesn't contain a size
|
||||
(size (or (plist-get model :size) 64))
|
||||
;; FIXME: What to use for these two?
|
||||
(ugid (if (eq id-format 'string) "jupyter" 100))
|
||||
(mbits (format "%sr%s%s-------"
|
||||
(if dirp "d" "-")
|
||||
(if (plist-get model :writable) "w" "")
|
||||
(if dirp "x" ""))))
|
||||
(list dirp 1 user-login-name ugid
|
||||
mtime mtime ctime size mbits nil -1 -1))))
|
||||
|
||||
(defun jupyter-tramp-file-attributes (filename &optional id-format)
|
||||
(jupyter-tramp-file-attributes-from-model
|
||||
(jupyter-tramp-with-api-connection filename
|
||||
(jupyter-tramp-get-file-model filename 'no-content))
|
||||
id-format))
|
||||
|
||||
(defun jupyter-tramp-directory-file-models (directory &optional full match)
|
||||
"Return the files contained in DIRECTORY as Jupyter file models.
|
||||
The returned files have the form (PATH . MODEL) where PATH is
|
||||
relative to DIRECTORY unless FULL is non-nil. In that case PATH
|
||||
is an absolute file name. PATH will have an ending / character if
|
||||
MODEL corresponds to a directory.
|
||||
|
||||
If MATCH is non-nil, it should be a regular expression. Only
|
||||
return files that match it.
|
||||
|
||||
If DIRECTORY does not correspond to a directory on the server,
|
||||
return nil."
|
||||
(when (file-directory-p directory)
|
||||
(jupyter-tramp-with-api-connection directory
|
||||
(let ((dir-model (jupyter-tramp-get-file-model directory)))
|
||||
(cl-loop
|
||||
for model across (plist-get dir-model :content)
|
||||
for dirp = (equal (plist-get model :type) "directory")
|
||||
for name = (concat (plist-get model :name) (and dirp "/"))
|
||||
for path = (if full (expand-file-name name directory) name)
|
||||
if match when (string-match-p match name)
|
||||
collect (cons path model) into files end
|
||||
else collect (cons path model) into files
|
||||
finally return
|
||||
(let ((pdir-model (jupyter-tramp-get-file-model
|
||||
(file-name-directory
|
||||
(directory-file-name directory)))))
|
||||
(dolist (d (list (cons "../" pdir-model)
|
||||
(cons "./" dir-model)))
|
||||
(when (or (null match)
|
||||
(string-match-p match (car d)))
|
||||
(when full
|
||||
(setcar d (expand-file-name (car d) directory)))
|
||||
(push d files)))
|
||||
files))))))
|
||||
|
||||
(defun jupyter-tramp-directory-files-and-attributes
|
||||
(directory &optional full match nosort id-format)
|
||||
(jupyter-tramp--barf-if-not-directory directory)
|
||||
(let ((files
|
||||
(cl-loop
|
||||
for (file . model)
|
||||
in (jupyter-tramp-directory-file-models directory full match)
|
||||
for attrs = (jupyter-tramp-file-attributes-from-model model id-format)
|
||||
collect (cons file attrs))))
|
||||
(if nosort files
|
||||
(sort files (lambda (a b) (string-lessp (car a) (car b)))))))
|
||||
|
||||
(provide 'jupyter-tramp)
|
||||
|
||||
;;; jupyter-tramp.el ends here
|
||||
287
lisp/jupyter/jupyter-widget-client.el
Normal file
287
lisp/jupyter/jupyter-widget-client.el
Normal file
@@ -0,0 +1,287 @@
|
||||
;;; jupyter-widget-client.el --- Widget support -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 21 May 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Use an external browser to interact with Jupyter widgets.
|
||||
;;
|
||||
;; A `jupyter-kernel-client' does not come with any widget support by default,
|
||||
;; the purpose of the `jupyter-widget-client' class is to provide such support.
|
||||
;; This is done by opening an external browser and serving it the necessary
|
||||
;; resources to display widgets using the `simple-httpd' package. Emacs then
|
||||
;; acts as an intermediary for the widget comm messages sent between the
|
||||
;; browser and the kernel, communicating with the kernel through `zmq' and with
|
||||
;; the browser through `websocket'.
|
||||
;;
|
||||
;; To add widget support to a client, subclass `jupyter-widget-client'.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'simple-httpd)
|
||||
(require 'websocket)
|
||||
(require 'jupyter-client)
|
||||
|
||||
(defvar jupyter-widgets-initialized nil
|
||||
"A client local variable that is non-nil if a browser for widgets is opened.")
|
||||
|
||||
(defvar jupyter-widgets-server nil
|
||||
"The `websocket-server' redirecting kernel messages.")
|
||||
|
||||
(defvar jupyter-widgets-port 8090
|
||||
"The port that `jupyter-widgets-server' listens on.")
|
||||
|
||||
(defvar jupyter-widgets-supported-targets '("jupyter.widget")
|
||||
"A list of the supported widget target names.")
|
||||
|
||||
(defvar jupyter-widgets-url-format
|
||||
"http://127.0.0.1:%d/jupyter/widgets?username=%s&clientId=%s&port=%d"
|
||||
"Format of the URL that will be visited to display widgets.")
|
||||
|
||||
(defclass jupyter-widget-client (jupyter-kernel-client)
|
||||
((widget-sock
|
||||
:type (or null websocket)
|
||||
:initform nil
|
||||
:documentation "The `websocket' connected to the browser
|
||||
displaying the widgets for this client.")
|
||||
(widget-state
|
||||
:type string
|
||||
:initform "null"
|
||||
:documentation "The JSON encode string representing the
|
||||
widget state. When a browser displaying the widgets of the client
|
||||
is closed, the state of the widgets is sent back to Emacs so that
|
||||
the state can be recovred when a new browser is opened.")
|
||||
(widget-messages
|
||||
:type list
|
||||
:initform nil
|
||||
:documentation "A list of pending messages to send to the
|
||||
widget socket."))
|
||||
:abstract t)
|
||||
|
||||
;;; Websocket handlers
|
||||
|
||||
(defsubst jupyter-widgets--send-deferred (client)
|
||||
(cl-loop for msg in (nreverse (oref client widget-messages))
|
||||
do (websocket-send-text (oref client widget-sock) msg))
|
||||
(oset client widget-messages nil))
|
||||
|
||||
(defun jupyter-widgets-on-message (ws frame)
|
||||
"When websocket, WS, receives a message FRAME, handle it.
|
||||
Send the contents of the message FRAME to the kernel and register
|
||||
callbacks."
|
||||
(cl-assert (eq (websocket-frame-opcode frame) 'text))
|
||||
(let* ((msg (jupyter-read-plist-from-string
|
||||
(websocket-frame-payload frame)))
|
||||
(client (jupyter-find-client-for-session
|
||||
(jupyter-message-session msg))))
|
||||
(cl-assert client)
|
||||
(unless (equal ws (oref client widget-sock))
|
||||
;; TODO: Handle multiple clients and sending widget state to new clients
|
||||
(oset client widget-sock ws))
|
||||
(pcase (jupyter-message-type msg)
|
||||
("connect"
|
||||
(jupyter-widgets--send-deferred client))
|
||||
(_
|
||||
;; Any other message the browser sends is meant for the kernel so do the
|
||||
;; redirection and setup the callbacks
|
||||
(let* ((msg-type (jupyter-message-type msg))
|
||||
(content (jupyter-message-content msg)))
|
||||
(jupyter-run-with-client client
|
||||
(jupyter-sent
|
||||
(jupyter-message-subscribed
|
||||
(let ((jupyter-inhibit-handlers
|
||||
(if (member msg-type '("comm_info_request"))
|
||||
'("comm_msg" "status" "comm_info_reply")
|
||||
'("comm_msg"))))
|
||||
(apply #'jupyter-request msg-type content))
|
||||
(let ((fn (apply-partially #'jupyter-widgets-send-message client)))
|
||||
`(("comm_open" ,fn)
|
||||
("comm_close" ,fn)
|
||||
("comm_info_reply" ,fn)
|
||||
("comm_msg" ,fn)
|
||||
("status" ,fn)))))))))))
|
||||
|
||||
(defun jupyter-widgets-on-close (ws)
|
||||
"Uninitialize the client whose widget-sock is WS."
|
||||
(cl-loop
|
||||
for client in jupyter--clients
|
||||
when (and (object-of-class-p client 'jupyter-widget-client)
|
||||
(equal ws (oref client widget-sock)))
|
||||
do (oset client widget-sock nil)
|
||||
(jupyter-set client 'jupyter-widgets-initialized nil)))
|
||||
|
||||
;;; Working with comm messages
|
||||
|
||||
(defun jupyter-widgets-normalize-comm-msg (msg)
|
||||
"Ensure that a comm MSG's fields are not ambiguous before encoding.
|
||||
For example, for fields that are supposed to be arrays, ensure
|
||||
that they will be encoded as such. In addition, add fields
|
||||
required by the JupyterLab widget manager."
|
||||
(prog1 msg
|
||||
(when (member (jupyter-message-type msg)
|
||||
'("comm_open" "comm_close" "comm_msg"))
|
||||
(let ((buffers (plist-member msg :buffers)))
|
||||
(if (null buffers) (plist-put msg :buffers [])
|
||||
(when (eq (cadr buffers) nil)
|
||||
(setcar (cdr buffers) [])))
|
||||
(unless (equal (cadr buffers) [])
|
||||
(setq buffers (cadr buffers))
|
||||
(while (car buffers)
|
||||
(setcar buffers
|
||||
(base64-encode-string
|
||||
(encode-coding-string (car buffers) 'utf-8-auto t) t))
|
||||
(setq buffers (cdr buffers))))
|
||||
;; Needed by WidgetManager
|
||||
(unless (jupyter-message-metadata msg)
|
||||
(plist-put msg :metadata '(:version "2.0")))))))
|
||||
|
||||
(cl-defmethod jupyter-widgets-send-message ((client jupyter-widget-client) msg)
|
||||
"Send a MSG to CLIENT's `widget-sock' `websocket'."
|
||||
(setq msg (jupyter-widgets-normalize-comm-msg msg))
|
||||
(let ((msg-type (jupyter-message-type msg)))
|
||||
(plist-put msg :channel
|
||||
(cond
|
||||
((member msg-type '("status" "comm_msg"
|
||||
"comm_close" "comm_open"))
|
||||
:iopub)
|
||||
((member msg-type '("comm_info_reply"))
|
||||
:shell)))
|
||||
(push (jupyter--encode msg) (oref client widget-messages))
|
||||
(when (websocket-openp (oref client widget-sock))
|
||||
(jupyter-widgets--send-deferred client))))
|
||||
|
||||
;;; Displaying widgets in the browser
|
||||
;; NOTE: The "display_model" and "clear_display" messages below are not true
|
||||
;; Jupyter messages, but are only used for communication between the browser
|
||||
;; and Emacs.
|
||||
|
||||
(cl-defmethod jupyter-widgets-display-model ((client jupyter-widget-client) model-id)
|
||||
"Display the model with MODEL-ID for the kernel CLIENT is connected to."
|
||||
;; (jupyter-widgets-clear-display client)
|
||||
(jupyter-widgets-send-message
|
||||
client (list :msg_type "display_model"
|
||||
:content (list :model_id model-id))))
|
||||
|
||||
(cl-defmethod jupyter-widgets-clear-display ((client jupyter-widget-client))
|
||||
"Clear the models being displayed for CLIENT."
|
||||
(jupyter-widgets-send-message client (list :msg_type "clear_display")))
|
||||
|
||||
;;; `jupyter-kernel-client' methods
|
||||
|
||||
(defun jupyter-widgets-start-websocket-server ()
|
||||
"Start the `jupyter-widgets-server' if necessary."
|
||||
(unless (process-live-p jupyter-widgets-server)
|
||||
(setq jupyter-widgets-server
|
||||
(websocket-server
|
||||
jupyter-widgets-port
|
||||
:host 'local
|
||||
:on-message #'jupyter-widgets-on-message
|
||||
:on-close #'jupyter-widgets-on-close))))
|
||||
|
||||
(defun jupyter-widgets--initialize-client (client)
|
||||
(unless (jupyter-get client 'jupyter-widgets-initialized)
|
||||
(jupyter-set client 'jupyter-widgets-initialized t)
|
||||
(unless (get-process "httpd")
|
||||
(httpd-start))
|
||||
(browse-url
|
||||
(format jupyter-widgets-url-format
|
||||
httpd-port
|
||||
user-login-name
|
||||
(jupyter-session-id (oref client session))
|
||||
jupyter-widgets-port))))
|
||||
|
||||
(cl-defmethod jupyter-handle-comm-open ((client jupyter-widget-client) _req msg)
|
||||
(jupyter-with-message-content msg (target_name)
|
||||
(when (member target_name jupyter-widgets-supported-targets)
|
||||
(jupyter-widgets-start-websocket-server)
|
||||
(jupyter-widgets--initialize-client client)
|
||||
(jupyter-widgets-send-message client msg)))
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-handle-comm-close ((client jupyter-widget-client) _req msg)
|
||||
(jupyter-widgets-send-message client msg)
|
||||
(cl-call-next-method))
|
||||
|
||||
(cl-defmethod jupyter-handle-comm-msg ((client jupyter-widget-client) _req msg)
|
||||
(jupyter-widgets-send-message client msg)
|
||||
(cl-call-next-method))
|
||||
|
||||
;;; `httpd' interface
|
||||
|
||||
(defun httpd/jupyter (proc path _query &rest _args)
|
||||
"Serve the javascript required for Jupyter widget support.
|
||||
PROC is the httpd process and PATH is the requested resource
|
||||
path. Currently no resources are accessible at any PATH other
|
||||
than the root, which will serve the necessary Javascript to
|
||||
load."
|
||||
(let ((split-path (split-string (substring path 1) "/")))
|
||||
(if (= (length split-path) 1)
|
||||
(with-httpd-buffer proc "text/javascript; charset=UTF-8"
|
||||
(insert-file-contents
|
||||
(expand-file-name "js/built/index.built.js" jupyter-root)))
|
||||
(error "Not found"))))
|
||||
|
||||
(defun httpd/jupyter/widgets/built (proc path _query &rest _args)
|
||||
"Serve the resources required by the widgets in the browser.
|
||||
PROC is the httpd process and PATH is the requested resource
|
||||
path. Currently this will only serve a file from the js/built
|
||||
directory if it has one of the extensions woff, woff2, ttf, svg,
|
||||
or eot. These are used by Jupyter."
|
||||
(let* ((split-path (split-string (substring path 1) "/"))
|
||||
(file (car (last split-path)))
|
||||
(mime (pcase (file-name-extension file)
|
||||
((or "woff" "woff2")
|
||||
"application/font-woff")
|
||||
("ttf"
|
||||
"application/octet-stream")
|
||||
("svg"
|
||||
"image/svg+xml")
|
||||
("eot"
|
||||
"application/vnd.ms-fontobject"))))
|
||||
(unless mime
|
||||
(error "Unsupported file type"))
|
||||
(setq file (expand-file-name (concat "js/built/" file) jupyter-root))
|
||||
;; TODO: Fix this, when loading the files through httpd, font awesome
|
||||
;; doesnt work
|
||||
(when (file-exists-p file)
|
||||
(error "File nonexistent (%s)" (file-name-nondirectory file)))
|
||||
(with-temp-buffer
|
||||
(insert-file-contents file)
|
||||
(httpd-send-header proc mime 200
|
||||
:Access-Control-Allow-Origin "*"))))
|
||||
|
||||
;; TODO: Since the path when we instantiate widgets is jupyter/widgets, all
|
||||
;; files that are trying to be loaded locally in the javascript will be
|
||||
;; referenced to this path. If we encounter a javascript file requesting to be
|
||||
;; loaded we can automatically search the jupyter --paths for notebook
|
||||
;; extension modules matching it.
|
||||
(defun httpd/jupyter/widgets (proc &rest _args)
|
||||
"Serve the HTML page to display widgets.
|
||||
PROC is the httpd process."
|
||||
(with-temp-buffer
|
||||
(insert-file-contents (expand-file-name "widget.html" jupyter-root))
|
||||
(httpd-send-header
|
||||
proc "text/html; charset=UTF-8" 200
|
||||
:Access-Control-Allow-Origin "*")))
|
||||
|
||||
(provide 'jupyter-widget-client)
|
||||
|
||||
;;; jupyter-widget-client.el ends here
|
||||
82
lisp/jupyter/jupyter-zmq-channel-ioloop.el
Normal file
82
lisp/jupyter/jupyter-zmq-channel-ioloop.el
Normal file
@@ -0,0 +1,82 @@
|
||||
;;; jupyter-zmq-channel-ioloop.el --- IOLoop functions for Jupyter channels -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 08 Nov 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; A `jupyter-channel-ioloop' using `jupyter-zmq-channel' to send and receive
|
||||
;; messages. Whenever a message is received on a channel an event that looks
|
||||
;; like the following will be sent back to the parent process
|
||||
;;
|
||||
;; (message CHANNEL-TYPE IDENTS . MSG)
|
||||
;;
|
||||
;; where CHANNEL-TYPE is the channel on which the message was received (one of
|
||||
;; `jupyter-socket-types'), IDENTS are ZMQ identities, typically ignored, and
|
||||
;; MSG is the message plist.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-base)
|
||||
(require 'jupyter-channel-ioloop)
|
||||
(require 'jupyter-zmq-channel)
|
||||
|
||||
(defclass jupyter-zmq-channel-ioloop (jupyter-channel-ioloop)
|
||||
()
|
||||
:documentation "A `jupyter-ioloop' configured for Jupyter channels.")
|
||||
|
||||
(cl-defmethod initialize-instance ((ioloop jupyter-zmq-channel-ioloop) &optional _slots)
|
||||
(cl-call-next-method)
|
||||
(jupyter-ioloop-add-setup ioloop
|
||||
(require 'jupyter-zmq-channel-ioloop)
|
||||
(push 'jupyter-zmq-channel-ioloop--recv-messages jupyter-ioloop-post-hook)
|
||||
(cl-loop
|
||||
for channel in '(:shell :stdin :iopub :control)
|
||||
unless (object-assoc channel :type jupyter-channel-ioloop-channels)
|
||||
do (push (jupyter-zmq-channel
|
||||
:session jupyter-channel-ioloop-session
|
||||
:type channel)
|
||||
jupyter-channel-ioloop-channels))))
|
||||
|
||||
(defun jupyter-zmq-channel-ioloop--recv-messages (events)
|
||||
"Print the received messages described in EVENTS.
|
||||
EVENTS is a list of socket events as returned by
|
||||
`zmq-poller-wait-all'. If any of the sockets in EVENTS matches
|
||||
one of the sockets in `jupyter-channel-ioloop-channels', receive a
|
||||
message on the channel and print a list with the form
|
||||
|
||||
(message CHANNEL-TYPE . MSG...)
|
||||
|
||||
to stdout. CHANNEL-TYPE is the channel on which MSG was
|
||||
received, either :shell, :stdin, :iopub, or :control. MSG is a
|
||||
list as returned by `jupyter-recv'."
|
||||
(let (messages)
|
||||
(dolist (channel jupyter-channel-ioloop-channels)
|
||||
(with-slots (type socket) channel
|
||||
(when (zmq-assoc socket events)
|
||||
(push (cons type (jupyter-recv channel)) messages))))
|
||||
(when messages
|
||||
;; Send messages
|
||||
(mapc (lambda (msg) (prin1 (cons 'message msg))) (nreverse messages))
|
||||
(zmq-flush 'stdout))))
|
||||
|
||||
(provide 'jupyter-zmq-channel-ioloop)
|
||||
|
||||
;;; jupyter-zmq-channel-ioloop.el ends here
|
||||
252
lisp/jupyter/jupyter-zmq-channel.el
Normal file
252
lisp/jupyter/jupyter-zmq-channel.el
Normal file
@@ -0,0 +1,252 @@
|
||||
;;; jupyter-zmq-channel.el --- A Jupyter channel implementation using ZMQ sockets -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 27 Jun 2019
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Implements synchronous channel types using ZMQ sockets. Each channel is
|
||||
;; essentially a wrapper around a `zmq-socket' constrained to a socket type by
|
||||
;; the type of the channel and with an associated `zmq-IDENTITY' obtained from
|
||||
;; the `jupyter-session' that must be associated with the channel. A heartbeat
|
||||
;; channel is distinct from the other channels in that it is implemented using
|
||||
;; a timer which periodically pings the kernel depending on how its configured.
|
||||
;; In order for communication to occur on the other channels, one of
|
||||
;; `jupyter-send' or `jupyter-recv' must be called after starting the channel
|
||||
;; with `jupyter-start'.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-messages)
|
||||
(require 'zmq)
|
||||
(require 'jupyter-channel)
|
||||
(eval-when-compile (require 'subr-x))
|
||||
|
||||
(declare-function jupyter-ioloop-poller-remove "jupyter-ioloop")
|
||||
(declare-function jupyter-ioloop-poller-add "jupyter-ioloop")
|
||||
|
||||
(defconst jupyter-socket-types
|
||||
(list :hb zmq-REQ
|
||||
:shell zmq-DEALER
|
||||
:iopub zmq-SUB
|
||||
:stdin zmq-DEALER
|
||||
:control zmq-DEALER)
|
||||
"The socket types for the various channels used by `jupyter'.")
|
||||
|
||||
(cl-deftype zmq-socket () '(satisfies zmq-socket-p))
|
||||
|
||||
(defclass jupyter-zmq-channel (jupyter-channel)
|
||||
((socket
|
||||
:type (or null zmq-socket)
|
||||
:initform nil
|
||||
:documentation "The socket used for communicating with the kernel.")))
|
||||
|
||||
(defun jupyter-connect-endpoint (type endpoint &optional identity)
|
||||
"Create socket with TYPE and connect to ENDPOINT.
|
||||
If IDENTITY is non-nil, it will be set as the ROUTING-ID of the
|
||||
socket. Return the created socket."
|
||||
(let ((sock (zmq-socket (zmq-current-context) type)))
|
||||
(prog1 sock
|
||||
(zmq-socket-set sock zmq-LINGER 1000)
|
||||
(when identity
|
||||
(zmq-socket-set sock zmq-ROUTING-ID identity))
|
||||
(zmq-connect sock endpoint))))
|
||||
|
||||
(defun jupyter-connect-channel (ctype endpoint &optional identity)
|
||||
"Create a socket based on a Jupyter channel type.
|
||||
CTYPE is one of the symbols `:hb', `:stdin', `:shell',
|
||||
`:control', or `:iopub' and represents the type of channel to
|
||||
connect to ENDPOINT. If IDENTITY is non-nil, it will be set as
|
||||
the ROUTING-ID of the socket. Return the created socket."
|
||||
(let ((sock-type (plist-get jupyter-socket-types ctype)))
|
||||
(unless sock-type
|
||||
(error "Invalid channel type (%s)" ctype))
|
||||
(jupyter-connect-endpoint sock-type endpoint identity)))
|
||||
|
||||
(cl-defmethod jupyter-start ((channel jupyter-zmq-channel)
|
||||
&key (identity (jupyter-session-id
|
||||
(oref channel session))))
|
||||
(unless (jupyter-alive-p channel)
|
||||
(let ((socket (jupyter-connect-channel
|
||||
(oref channel type) (oref channel endpoint) identity)))
|
||||
(oset channel socket socket)
|
||||
(cl-case (oref channel type)
|
||||
(:iopub
|
||||
(zmq-socket-set socket zmq-SUBSCRIBE ""))))
|
||||
(when (and (functionp 'jupyter-ioloop-environment-p)
|
||||
(jupyter-ioloop-environment-p))
|
||||
(jupyter-ioloop-poller-add (oref channel socket) zmq-POLLIN))))
|
||||
|
||||
(cl-defmethod jupyter-stop ((channel jupyter-zmq-channel))
|
||||
(when (jupyter-alive-p channel)
|
||||
(when (and (functionp 'jupyter-ioloop-environment-p)
|
||||
(jupyter-ioloop-environment-p))
|
||||
(jupyter-ioloop-poller-remove (oref channel socket)))
|
||||
(with-slots (socket) channel
|
||||
(zmq-disconnect socket (zmq-socket-get socket zmq-LAST-ENDPOINT)))
|
||||
(oset channel socket nil)))
|
||||
|
||||
(cl-defmethod jupyter-alive-p ((channel jupyter-zmq-channel))
|
||||
(not (null (oref channel socket))))
|
||||
|
||||
(cl-defmethod jupyter-send ((channel jupyter-zmq-channel) type message &optional msg-id)
|
||||
"Send a message on a ZMQ based Jupyter channel.
|
||||
CHANNEL is the channel to send MESSAGE on. TYPE is a Jupyter
|
||||
message type, like :kernel-info-request. Return the message ID
|
||||
of the sent message."
|
||||
(cl-destructuring-bind (id . msg)
|
||||
(jupyter-encode-message (oref channel session) type
|
||||
:msg-id msg-id
|
||||
:content message)
|
||||
(prog1 id
|
||||
(zmq-send-multipart (oref channel socket) msg))))
|
||||
|
||||
(cl-defmethod jupyter-recv ((channel jupyter-zmq-channel) &optional dont-wait)
|
||||
"Receive a message on CHANNEL.
|
||||
Return a cons cell (IDENTS . MSG) where IDENTS are the ZMQ
|
||||
message identities, as a list, and MSG is the received message.
|
||||
|
||||
If DONT-WAIT is non-nil, return immediately without waiting for a
|
||||
message if one isn't already available."
|
||||
(condition-case nil
|
||||
(let ((session (oref channel session))
|
||||
(msg (zmq-recv-multipart (oref channel socket)
|
||||
(and dont-wait zmq-DONTWAIT))))
|
||||
(when msg
|
||||
(cl-destructuring-bind (idents . parts)
|
||||
(jupyter--split-identities msg)
|
||||
(cons idents (jupyter-decode-message session parts)))))
|
||||
(zmq-EAGAIN nil)))
|
||||
|
||||
;;; Heartbeat channel
|
||||
|
||||
(defvar jupyter-hb-max-failures 3
|
||||
"Number of heartbeat failures until the kernel is considered unreachable.
|
||||
A ping is sent to the kernel on a heartbeat channel and waits
|
||||
until `time-to-dead' seconds to see if the kernel sent a ping
|
||||
back. If the kernel doesn't send a ping back after
|
||||
`jupyter-hb-max-failures', the callback associated with the
|
||||
heartbeat channel is called. See `jupyter-hb-on-kernel-dead'.")
|
||||
|
||||
(defclass jupyter-hb-channel (jupyter-zmq-channel)
|
||||
((type
|
||||
:type keyword
|
||||
:initform :hb
|
||||
:documentation "The type of this channel is `:hb'.")
|
||||
(time-to-dead
|
||||
:type number
|
||||
:initform 10
|
||||
:documentation "The time in seconds to wait for a response
|
||||
from the kernel until the connection is assumed to be dead. Note
|
||||
that this slot only takes effect when starting the channel.")
|
||||
(dead-cb
|
||||
:type function
|
||||
:initform #'ignore
|
||||
:documentation "A callback function that takes 0 arguments
|
||||
and is called when the kernel has not responded for
|
||||
\(* `jupyter-hb-max-failures' `time-to-dead'\) seconds.")
|
||||
(beating
|
||||
:type (or boolean symbol)
|
||||
:initform t
|
||||
:documentation "A flag variable indicating that the heartbeat
|
||||
channel is communicating with the kernel.")
|
||||
(paused
|
||||
:type boolean
|
||||
:initform t
|
||||
:documentation "A flag variable indicating that the heartbeat
|
||||
channel is paused and not communicating with the kernel. To
|
||||
pause the heartbeat channel use `jupyter-hb-pause', to unpause
|
||||
use `jupyter-hb-unpause'."))
|
||||
:documentation "A base class for heartbeat channels.")
|
||||
|
||||
(cl-defmethod jupyter-alive-p ((channel jupyter-hb-channel))
|
||||
"Return non-nil if CHANNEL is alive."
|
||||
(zmq-socket-p (oref channel socket)))
|
||||
|
||||
(defun jupyter-hb--pingable-p (channel)
|
||||
(and (not (oref channel paused))
|
||||
(jupyter-alive-p channel)))
|
||||
|
||||
(cl-defmethod jupyter-hb-beating-p ((channel jupyter-hb-channel))
|
||||
"Return non-nil if CHANNEL is reachable."
|
||||
(and (jupyter-hb--pingable-p channel)
|
||||
(oref channel beating)))
|
||||
|
||||
(cl-defmethod jupyter-hb-pause ((channel jupyter-hb-channel))
|
||||
"Pause checking for heartbeat events on CHANNEL."
|
||||
(oset channel paused t))
|
||||
|
||||
(cl-defmethod jupyter-hb-unpause ((channel jupyter-hb-channel))
|
||||
"Un-pause checking for heatbeat events on CHANNEL."
|
||||
(when (oref channel paused)
|
||||
(if (jupyter-alive-p channel)
|
||||
;; Consume a pending message from the kernel if there is one. We send a
|
||||
;; ping and then schedule a timer which fires TIME-TO-DEAD seconds
|
||||
;; later to receive the ping back from the kernel and start the process
|
||||
;; all over again. If the channel is paused before TIME-TO-DEAD
|
||||
;; seconds, there may still be a ping from the kernel waiting.
|
||||
(ignore-errors (zmq-recv (oref channel socket) zmq-DONTWAIT))
|
||||
(jupyter-start channel))
|
||||
(oset channel paused nil)
|
||||
(jupyter-hb--send-ping channel)))
|
||||
|
||||
(cl-defgeneric jupyter-hb-on-kernel-dead (channel fun)
|
||||
(declare (indent 1)))
|
||||
|
||||
(cl-defmethod jupyter-hb-on-kernel-dead ((channel jupyter-hb-channel) fun)
|
||||
"When the kernel connected to CHANNEL dies, call FUN.
|
||||
A kernel is considered dead when CHANNEL does not receive a
|
||||
response after \(* `jupyter-hb-max-failures' `time-to-dead'\)
|
||||
seconds has elapsed without the kernel sending a ping back."
|
||||
(oset channel dead-cb fun))
|
||||
|
||||
(defun jupyter-hb--send-ping (channel &optional failed-count)
|
||||
(when (jupyter-hb--pingable-p channel)
|
||||
(condition-case nil
|
||||
(progn
|
||||
(zmq-send (oref channel socket) "ping")
|
||||
(run-with-timer
|
||||
(oref channel time-to-dead) nil
|
||||
(lambda ()
|
||||
(when-let* ((sock (and (jupyter-hb--pingable-p channel)
|
||||
(oref channel socket))))
|
||||
(oset channel beating
|
||||
(condition-case nil
|
||||
(and (zmq-recv sock zmq-DONTWAIT) t)
|
||||
((zmq-EINTR zmq-EAGAIN) nil)))
|
||||
(if (oref channel beating)
|
||||
(jupyter-hb--send-ping channel)
|
||||
;; Reset the socket
|
||||
(jupyter-stop channel)
|
||||
(jupyter-start channel)
|
||||
(or failed-count (setq failed-count 0))
|
||||
(if (< failed-count jupyter-hb-max-failures)
|
||||
(jupyter-hb--send-ping channel (1+ failed-count))
|
||||
(oset channel paused t)
|
||||
(when (functionp (oref channel dead-cb))
|
||||
(funcall (oref channel dead-cb)))))))))
|
||||
;; FIXME: Should be a part of `jupyter-hb--pingable-p'
|
||||
(zmq-ENOTSOCK
|
||||
(jupyter-hb-pause channel)
|
||||
(oset channel socket nil)))))
|
||||
|
||||
(provide 'jupyter-zmq-channel)
|
||||
|
||||
;;; jupyter-zmq-channel.el ends here
|
||||
44
lisp/jupyter/jupyter.el
Normal file
44
lisp/jupyter/jupyter.el
Normal file
@@ -0,0 +1,44 @@
|
||||
;;; jupyter.el --- Jupyter -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 11 Jan 2018
|
||||
;; Version: 1.0
|
||||
;; Package-Requires: ((emacs "26") (cl-lib "0.5") (org "9.1.6") (zmq "0.10.10") (simple-httpd "1.5.0") (websocket "1.9"))
|
||||
;; URL: https://github.com/emacs-jupyter/jupyter
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; An interface for communicating with Jupyter kernels.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(defgroup jupyter nil
|
||||
"Jupyter"
|
||||
:group 'processes)
|
||||
|
||||
(require 'jupyter-base)
|
||||
(require 'jupyter-client)
|
||||
(require 'jupyter-kernelspec)
|
||||
(require 'jupyter-server)
|
||||
(require 'jupyter-repl)
|
||||
|
||||
(provide 'jupyter)
|
||||
|
||||
;;; jupyter.el ends here
|
||||
836
lisp/jupyter/ob-jupyter.el
Normal file
836
lisp/jupyter/ob-jupyter.el
Normal file
@@ -0,0 +1,836 @@
|
||||
;;; ob-jupyter.el --- Jupyter integration with org-mode -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2018-2024 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 21 Jan 2018
|
||||
|
||||
;; This program is free software; you can redistribute it and/or
|
||||
;; modify it under the terms of the GNU General Public License as
|
||||
;; published by the Free Software Foundation; either version 3, or (at
|
||||
;; your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful, but
|
||||
;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
;; General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with GNU Emacs; see the file COPYING. If not, write to the
|
||||
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
;; Boston, MA 02111-1307, USA.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Interact with a Jupyter kernel via `org-mode' src-block's.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(defgroup ob-jupyter nil
|
||||
"Jupyter integration with org-mode"
|
||||
:group 'org-babel)
|
||||
|
||||
(require 'jupyter-env)
|
||||
(require 'jupyter-kernelspec)
|
||||
(require 'jupyter-org-client)
|
||||
(require 'jupyter-org-extensions)
|
||||
(eval-when-compile
|
||||
(require 'jupyter-repl) ; For `jupyter-with-repl-buffer'
|
||||
(require 'subr-x))
|
||||
|
||||
(declare-function org-in-src-block-p "org" (&optional inside))
|
||||
(declare-function org-element-at-point "org-element")
|
||||
(declare-function org-element-property "org-element" (property element))
|
||||
(declare-function org-element-context "org-element" (&optional element))
|
||||
(declare-function org-babel-execute-src-block "ob-core" (&optional arg info params executor-type))
|
||||
(declare-function org-babel-variable-assignments:python "ob-python" (params))
|
||||
(declare-function org-babel-expand-body:generic "ob-core" (body params &optional var-lines))
|
||||
(declare-function org-export-derived-backend-p "ox" (backend &rest backends))
|
||||
|
||||
(declare-function jupyter-run-server-repl "jupyter-server")
|
||||
(declare-function jupyter-connect-server-repl "jupyter-server")
|
||||
(declare-function jupyter-kernelspecs "jupyter-server")
|
||||
(declare-function jupyter-server-kernel-id-from-name "jupyter-server")
|
||||
(declare-function jupyter-server-name-client-kernel "jupyter-server")
|
||||
(declare-function jupyter-api-get-kernel "jupyter-rest-api")
|
||||
|
||||
(declare-function jupyter-tramp-url-from-file-name "jupyter-tramp")
|
||||
(declare-function jupyter-tramp-server-from-file-name "jupyter-tramp")
|
||||
(declare-function jupyter-tramp-file-name-p "jupyter-tramp")
|
||||
|
||||
(defcustom org-babel-jupyter-language-aliases '(("python3" "python"))
|
||||
"An alist mapping kernel language names to another name.
|
||||
If a kernel has a language name matching the CAR of an element of
|
||||
this list, the associated name will be used for the names of the
|
||||
source blocks instead.
|
||||
|
||||
So if this variable has an entry like \\='(\"python3\" \"python\")
|
||||
then instead of jupyter-python3 source blocks, you can use
|
||||
jupyter-python source blocks for the associated kernel."
|
||||
:type '(alist :key-type string :value-type string))
|
||||
|
||||
(defvaralias 'org-babel-jupyter-resource-directory
|
||||
'jupyter-org-resource-directory)
|
||||
|
||||
(defvar org-babel-jupyter-session-clients (make-hash-table :test #'equal)
|
||||
"A hash table mapping session names to Jupyter clients.
|
||||
`org-babel-jupyter-src-block-session' returns a key into this
|
||||
table for the source block at `point'.")
|
||||
|
||||
(defvar org-babel-header-args:jupyter '((kernel . :any)
|
||||
(async . ((yes no))))
|
||||
"Available header arguments for Jupyter src-blocks.")
|
||||
|
||||
(defvar org-babel-default-header-args:jupyter '((:kernel . "python")
|
||||
(:async . "no"))
|
||||
"Default header arguments for Jupyter src-blocks.")
|
||||
|
||||
;;; Helper functions
|
||||
|
||||
(defun org-babel-jupyter--src-block-kernel-language ()
|
||||
(when (org-in-src-block-p)
|
||||
(let ((info (org-babel-get-src-block-info)))
|
||||
(save-match-data
|
||||
(string-match "^jupyter-\\(.+\\)$" (car info))
|
||||
(match-string 1 (car info))))))
|
||||
|
||||
(defun org-babel-jupyter-language-p (lang)
|
||||
"Return non-nil if LANG src-blocks are executed using Jupyter."
|
||||
(or (string-prefix-p "jupyter-" lang)
|
||||
;; Check if the language has been overridden, see
|
||||
;; `org-babel-jupyter-override-src-block'
|
||||
(advice-member-p
|
||||
'ob-jupyter (intern (concat "org-babel-execute:" lang)))))
|
||||
|
||||
(defun org-babel-jupyter-session-key (params)
|
||||
"Return a string that is the concatenation of the :session and :kernel PARAMS.
|
||||
PARAMS is the arguments alist as returned by
|
||||
`org-babel-get-src-block-info'. The returned string can then be
|
||||
used to identify unique Jupyter Org babel sessions."
|
||||
;; Take into account a Lisp expression as a session name.
|
||||
(let ((session (org-babel-read (alist-get :session params)))
|
||||
(kernel (alist-get :kernel params)))
|
||||
(unless (and session kernel
|
||||
(not (equal session "none")))
|
||||
(error "Need a valid session and a kernel to form a key"))
|
||||
(concat session "-" kernel)))
|
||||
|
||||
(defun org-babel-jupyter-src-block-session ()
|
||||
"Return the session key for the current Jupyter source block.
|
||||
Return nil if the current source block is not a Jupyter block or
|
||||
if there is no source block at point."
|
||||
(let ((info (or (and (org-in-src-block-p)
|
||||
(org-babel-get-src-block-info 'light))
|
||||
(org-babel-lob-get-info))))
|
||||
(when info
|
||||
(cl-destructuring-bind (lang _ params . rest) info
|
||||
(when (org-babel-jupyter-language-p lang)
|
||||
(org-babel-jupyter-session-key params))))))
|
||||
|
||||
;;; `ob' integration
|
||||
|
||||
(defun org-babel-variable-assignments:jupyter (params &optional lang)
|
||||
"Assign variables in PARAMS according to the Jupyter kernel language.
|
||||
LANG is the kernel language of the source block. If LANG is nil,
|
||||
get the kernel language from the current source block.
|
||||
|
||||
The variables are assigned by looking for the function
|
||||
`org-babel-variable-assignments:LANG'. If this function does not
|
||||
exist or if LANG cannot be determined, assign variables using
|
||||
`org-babel-variable-assignments:python'."
|
||||
(or lang (setq lang (org-babel-jupyter--src-block-kernel-language)))
|
||||
(let ((fun (when lang
|
||||
(intern (format "org-babel-variable-assignments:%s" lang)))))
|
||||
(if (functionp fun) (funcall fun params)
|
||||
(require 'ob-python)
|
||||
(org-babel-variable-assignments:python params))))
|
||||
|
||||
(cl-defgeneric org-babel-jupyter-transform-code (code _changelist)
|
||||
"Transform CODE according to CHANGELIST, return the transformed CODE.
|
||||
CHANGELIST is a property list containing the requested changes. The default
|
||||
implementation returns CODE unchanged.
|
||||
|
||||
This is useful for kernel languages to extend using the
|
||||
jupyter-lang method specializer, e.g. to return new code to change
|
||||
directories before evaluating CODE.
|
||||
|
||||
See `org-babel-expand-body:jupyter' for possible changes that can
|
||||
be in CHANGELIST."
|
||||
code)
|
||||
|
||||
(defun org-babel-expand-body:jupyter (body params &optional var-lines lang)
|
||||
"Expand BODY according to PARAMS.
|
||||
|
||||
BODY is the code to expand, PARAMS should be the header arguments
|
||||
of the src block with BODY as its code, and VAR-LINES should be
|
||||
the list of strings containing the variables to evaluate before
|
||||
executing body. LANG is the kernel language of the source block.
|
||||
|
||||
This function is similar to
|
||||
`org-babel-variable-assignments:jupyter' in that it attempts to
|
||||
find the kernel language of the source block if LANG is not
|
||||
provided.
|
||||
|
||||
BODY is expanded by calling the function
|
||||
`org-babel-expand-body:LANG'. If this function doesn't exist or
|
||||
if LANG cannot be determined, fall back to
|
||||
`org-babel-expand-body:generic'.
|
||||
|
||||
If PARAMS has a :dir parameter, the expanded code is passed to
|
||||
`org-babel-jupyter-transform-code' with a changelist that
|
||||
includes the :dir parameter with the directory being an absolute
|
||||
path."
|
||||
(or lang (setq lang (org-babel-jupyter--src-block-kernel-language)))
|
||||
(let* ((expander (when lang
|
||||
(intern (format "org-babel-expand-body:%s" lang))))
|
||||
(expanded (if (functionp expander)
|
||||
(funcall expander body params)
|
||||
(org-babel-expand-body:generic body params var-lines)))
|
||||
(changelist nil))
|
||||
(when-let* ((dir (alist-get :dir params)))
|
||||
(setq changelist (plist-put changelist :dir (expand-file-name dir))))
|
||||
(if changelist (org-babel-jupyter-transform-code expanded changelist)
|
||||
expanded)))
|
||||
|
||||
(defun org-babel-edit-prep:jupyter (info)
|
||||
"Prepare the edit buffer according to INFO.
|
||||
Enable `jupyter-repl-interaction-mode' in the edit buffer
|
||||
associated with the session found in INFO.
|
||||
|
||||
If the session is a Jupyter TRAMP file name, the
|
||||
`default-directory' of the edit buffer is set to the root
|
||||
directory the notebook serves.
|
||||
|
||||
If `jupyter-org-auto-connect' is nil, this function does nothing
|
||||
if the session has not been initiated yet."
|
||||
(let* ((params (nth 2 info))
|
||||
(session (alist-get :session params))
|
||||
(client-buffer
|
||||
(when (or jupyter-org-auto-connect
|
||||
(org-babel-jupyter-session-initiated-p params))
|
||||
(org-babel-jupyter-initiate-session session params))))
|
||||
(when client-buffer
|
||||
(jupyter-repl-associate-buffer client-buffer)
|
||||
(when (jupyter-tramp-file-name-p session)
|
||||
(setq default-directory (concat (file-remote-p session) "/"))))))
|
||||
|
||||
(defun org-babel-jupyter--insert-variable-assignments (params)
|
||||
"Insert variable assignment lines from PARAMS into the `current-buffer'.
|
||||
Return non-nil if there are variable assignments, otherwise
|
||||
return nil."
|
||||
(let ((var-lines (org-babel-variable-assignments:jupyter params)))
|
||||
(prog1 var-lines
|
||||
(jupyter-repl-replace-cell-code (mapconcat #'identity var-lines "\n")))))
|
||||
|
||||
(defun org-babel-prep-session:jupyter (session params)
|
||||
"Prepare a Jupyter SESSION according to PARAMS."
|
||||
(with-current-buffer (org-babel-jupyter-initiate-session session params)
|
||||
(goto-char (point-max))
|
||||
(and (org-babel-jupyter--insert-variable-assignments params)
|
||||
(jupyter-repl-execute-cell jupyter-current-client))
|
||||
(current-buffer)))
|
||||
|
||||
(defun org-babel-load-session:jupyter (session body params)
|
||||
"In a Jupyter SESSION, load BODY according to PARAMS."
|
||||
(save-window-excursion
|
||||
(with-current-buffer (org-babel-jupyter-initiate-session session params)
|
||||
(goto-char (point-max))
|
||||
(when (org-babel-jupyter--insert-variable-assignments params)
|
||||
(insert "\n"))
|
||||
(insert (org-babel-expand-body:jupyter (org-babel-chomp body) params))
|
||||
(current-buffer))))
|
||||
|
||||
(defvar org-babel-jupyter-resolving-reference-p nil
|
||||
"Non-nil if a reference is being resolved.")
|
||||
|
||||
(defun org-babel-jupyter--indicate-resolve (&rest args)
|
||||
"Set `org-babel-jupyter-resolving-referece-p', apply ARGS."
|
||||
(let ((org-babel-jupyter-resolving-reference-p t))
|
||||
(apply args)))
|
||||
|
||||
(advice-add #'org-babel-ref-resolve :around #'org-babel-jupyter--indicate-resolve)
|
||||
|
||||
;;;; Initializing session clients
|
||||
|
||||
(cl-defstruct (org-babel-jupyter-session
|
||||
(:constructor org-babel-jupyter-session))
|
||||
name)
|
||||
|
||||
(cl-defstruct (org-babel-jupyter-remote-session
|
||||
(:include org-babel-jupyter-session)
|
||||
(:constructor org-babel-jupyter-remote-session))
|
||||
connect-repl-p)
|
||||
|
||||
(cl-defmethod org-babel-jupyter-parse-session ((session string))
|
||||
"Return a parsed representation of SESSION."
|
||||
(org-babel-jupyter-session :name session))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-initiate-client ((_session org-babel-jupyter-session) kernel)
|
||||
"Launch SESSION's KERNEL, return a `jupyter-org-client' connected to it.
|
||||
SESSION is the :session header argument of a source block and
|
||||
KERNEL is the name of the kernel to launch."
|
||||
(jupyter-run-repl kernel nil nil 'jupyter-org-client))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-initiate-client :around (session _kernel)
|
||||
"Rename the returned client's REPL buffer to include SESSION's name.
|
||||
Also set `jupyter-include-other-output' to nil for the session so
|
||||
that output produced by other clients do not get handled by the
|
||||
client."
|
||||
(let ((client (cl-call-next-method)))
|
||||
(prog1 client
|
||||
(jupyter-set client 'jupyter-include-other-output nil)
|
||||
;; Append the name of SESSION to the initiated client REPL's
|
||||
;; `buffer-name'.
|
||||
(jupyter-with-repl-buffer client
|
||||
(let ((name (buffer-name)))
|
||||
(when (string-match "^\\*\\(.+\\)\\*" name)
|
||||
(rename-buffer
|
||||
(concat "*" (match-string 1 name) "-"
|
||||
(org-babel-jupyter-session-name session)
|
||||
"*")
|
||||
'unique)))))))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-parse-session :extra "remote" ((session string))
|
||||
"If SESSION is a remote file name, return a `org-babel-jupyter-remote-session'.
|
||||
A `org-babel-jupyter-remote-session' is also returned if SESSION
|
||||
ends in \".json\", regardless of SESSION being a remote file
|
||||
name, with `org-babel-jupyter-remote-session-connect-repl-p' set
|
||||
to nil. The CONNECT-REPL-P slot indicates that a connection file
|
||||
is read to connect to the session, as opposed to launching a
|
||||
kernel."
|
||||
(if jupyter-use-zmq
|
||||
(let ((json-p (string-suffix-p ".json" session)))
|
||||
(if (or json-p (file-remote-p session))
|
||||
(org-babel-jupyter-remote-session
|
||||
:name session
|
||||
:connect-repl-p json-p)
|
||||
(cl-call-next-method)))
|
||||
(when (file-remote-p session)
|
||||
(error "ZMQ is required for remote sessions (%s)" session))
|
||||
(cl-call-next-method)))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-initiate-client :before ((session org-babel-jupyter-remote-session) _kernel)
|
||||
"Raise an error if SESSION's name is a remote file name without a local name.
|
||||
The local name is used as a unique identifier of a remote
|
||||
session."
|
||||
(unless (not (zerop (length (file-local-name
|
||||
(org-babel-jupyter-session-name session)))))
|
||||
(error "No remote session name")))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-initiate-client ((session org-babel-jupyter-remote-session) kernel)
|
||||
"Initiate a client connected to a remote kernel process."
|
||||
(pcase-let (((cl-struct org-babel-jupyter-remote-session name connect-repl-p) session))
|
||||
(if connect-repl-p
|
||||
(jupyter-connect-repl name nil nil 'jupyter-org-client)
|
||||
(let ((default-directory (file-remote-p name)))
|
||||
(org-babel-jupyter-aliases-from-kernelspecs)
|
||||
(jupyter-run-repl kernel nil nil 'jupyter-org-client)))))
|
||||
|
||||
(require 'jupyter-server)
|
||||
(require 'jupyter-tramp)
|
||||
|
||||
(cl-defstruct (org-babel-jupyter-server-session
|
||||
(:include org-babel-jupyter-remote-session)
|
||||
(:constructor org-babel-jupyter-server-session)))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-parse-session :extra "server" ((session string))
|
||||
"If SESSION is a Jupyter TRAMP file name return a
|
||||
`org-babel-jupyter-server-session'."
|
||||
(if (jupyter-tramp-file-name-p session)
|
||||
(org-babel-jupyter-server-session :name session)
|
||||
(cl-call-next-method)))
|
||||
|
||||
(cl-defmethod org-babel-jupyter-initiate-client ((session org-babel-jupyter-server-session) kernel)
|
||||
(let* ((rsession (org-babel-jupyter-session-name session))
|
||||
(server (with-parsed-tramp-file-name rsession nil
|
||||
(when (member host '("127.0.0.1" "localhost"))
|
||||
(setq port (tramp-file-name-port-or-default v))
|
||||
(when (jupyter-port-available-p port)
|
||||
(if (y-or-n-p (format "Notebook not started on port %s. Launch one? "
|
||||
port))
|
||||
;; TODO: Specify authentication? But then
|
||||
;; how would you get the token for the
|
||||
;; login that happens in
|
||||
;; `jupyter-tramp-server-from-file-name'.
|
||||
(jupyter-launch-notebook port)
|
||||
(user-error "Launch a notebook on port %s first." port))))
|
||||
(jupyter-tramp-server-from-file-name rsession))))
|
||||
(unless (jupyter-server-has-kernelspec-p server kernel)
|
||||
(error "No kernelspec matching \"%s\" exists at %s"
|
||||
kernel (oref server url)))
|
||||
;; Language aliases may not exist for the kernels that are accessible on
|
||||
;; the server so ensure they do.
|
||||
(org-babel-jupyter-aliases-from-kernelspecs
|
||||
nil (jupyter-kernelspecs server))
|
||||
(let ((sname (file-local-name rsession)))
|
||||
(if-let ((id (jupyter-server-kernel-id-from-name server sname)))
|
||||
;; Connecting to an existing kernel
|
||||
(cl-destructuring-bind (&key name id &allow-other-keys)
|
||||
(or (ignore-errors (jupyter-api-get-kernel server id))
|
||||
(error "Kernel ID, %s, no longer references a kernel at %s"
|
||||
id (oref server url)))
|
||||
(unless (string-match-p kernel name)
|
||||
(error "\":kernel %s\" doesn't match \"%s\"" kernel name))
|
||||
(jupyter-connect-server-repl server id nil nil 'jupyter-org-client))
|
||||
;; Start a new kernel
|
||||
(let ((client (jupyter-run-server-repl
|
||||
server kernel nil nil 'jupyter-org-client)))
|
||||
(prog1 client
|
||||
;; TODO: If a kernel gets renamed in the future it doesn't affect
|
||||
;; any source block :session associations because the hash of the
|
||||
;; session name used here is already stored in the
|
||||
;; `org-babel-jupyter-session-clients' variable. Should that
|
||||
;; variable be updated on a kernel rename?
|
||||
;;
|
||||
;; TODO: Would we always want to do this?
|
||||
(jupyter-server-name-client-kernel client sname)))))))
|
||||
|
||||
(defun org-babel-jupyter-session-initiated-p (params)
|
||||
"Return non-nil if the session corresponding to PARAMS is initiated."
|
||||
(let ((key (org-babel-jupyter-session-key params)))
|
||||
(gethash key org-babel-jupyter-session-clients)))
|
||||
|
||||
(defun org-babel-jupyter-initiate-session-by-key (session params)
|
||||
"Return the Jupyter REPL buffer for SESSION.
|
||||
If SESSION does not have a client already, one is created based
|
||||
on SESSION and PARAMS. If SESSION ends with \".json\" then
|
||||
SESSION is interpreted as a kernel connection file and a new
|
||||
kernel connected to SESSION is created.
|
||||
|
||||
Otherwise a kernel is started based on the `:kernel' parameter in
|
||||
PARAMS which should be either a valid kernel name or a prefix of
|
||||
one, in which case the first kernel that matches the prefix will
|
||||
be used.
|
||||
|
||||
If SESSION is a remote file name, like /ssh:ec2:jl, then the
|
||||
kernel starts on the remote host /ssh:ec2: with a session name of
|
||||
jl. The remote host must have jupyter installed since the
|
||||
\"jupyter kernel\" command will be used to start the kernel on
|
||||
the host."
|
||||
(let* ((key (org-babel-jupyter-session-key params))
|
||||
(client (gethash key org-babel-jupyter-session-clients)))
|
||||
(unless client
|
||||
(setq client (org-babel-jupyter-initiate-client
|
||||
(org-babel-jupyter-parse-session session)
|
||||
(alist-get :kernel params)))
|
||||
(puthash key client org-babel-jupyter-session-clients)
|
||||
(jupyter-with-repl-buffer client
|
||||
(let ((forget-client (lambda () (remhash key org-babel-jupyter-session-clients))))
|
||||
(add-hook 'kill-buffer-hook forget-client nil t))))
|
||||
(oref client buffer)))
|
||||
|
||||
(defun org-babel-jupyter-initiate-session (&optional session params)
|
||||
"Initialize a Jupyter SESSION according to PARAMS."
|
||||
(if (equal session "none") (error "Need a session to run")
|
||||
(when session
|
||||
;; Take into account a Lisp expression as a session name.
|
||||
(setq session (org-babel-read session)))
|
||||
(org-babel-jupyter-initiate-session-by-key session params)))
|
||||
|
||||
;;;; Helper functions
|
||||
|
||||
;;;###autoload
|
||||
(defun org-babel-jupyter-scratch-buffer ()
|
||||
"Display a scratch buffer connected to the current block's session."
|
||||
(interactive)
|
||||
(let (buffer)
|
||||
(org-babel-do-in-edit-buffer
|
||||
(setq buffer (save-window-excursion
|
||||
(jupyter-repl-scratch-buffer))))
|
||||
(if buffer (pop-to-buffer buffer)
|
||||
(user-error "No source block at point"))))
|
||||
|
||||
(cl-defmethod jupyter-do-refresh-kernelspecs (&context (major-mode org-mode))
|
||||
(or (jupyter-org-when-in-src-block
|
||||
(let* ((info (org-babel-get-src-block-info 'light))
|
||||
(params (nth 2 info))
|
||||
(session (org-babel-read (alist-get :session params))))
|
||||
(when (file-remote-p session)
|
||||
(jupyter-kernelspecs session 'refresh))))
|
||||
(cl-call-next-method)))
|
||||
|
||||
;;;; `org-babel-execute:jupyter'
|
||||
|
||||
(defvar org-link-bracket-re)
|
||||
|
||||
(defun org-babel-jupyter-cleanup-file-links ()
|
||||
"Delete the files of image links for the current source block result.
|
||||
Do this only if the file exists in
|
||||
`org-babel-jupyter-resource-directory'."
|
||||
(when-let*
|
||||
((pos (org-babel-where-is-src-block-result))
|
||||
(link-re (format "^[ \t]*%s[ \t]*$" org-link-bracket-re))
|
||||
(resource-dir (expand-file-name org-babel-jupyter-resource-directory)))
|
||||
(save-excursion
|
||||
(goto-char pos)
|
||||
(forward-line)
|
||||
(let ((bound (org-babel-result-end)))
|
||||
;; This assumes that `jupyter-org-client' only emits bracketed links as
|
||||
;; images
|
||||
(while (re-search-forward link-re bound t)
|
||||
(when-let*
|
||||
((path (org-element-property :path (org-element-context)))
|
||||
(dir (when (file-name-directory path)
|
||||
(expand-file-name (file-name-directory path)))))
|
||||
(when (and (equal dir resource-dir)
|
||||
(file-exists-p path))
|
||||
(delete-file path))))))))
|
||||
|
||||
;; TODO: What is a better way to handle discrepancies between how `org-mode'
|
||||
;; views header arguments and how `emacs-jupyter' views them? Should the
|
||||
;; strategy be to always try to emulate the `org-mode' behavior?
|
||||
(defun org-babel-jupyter--remove-file-param (params)
|
||||
"Destructively remove the file result parameter from PARAMS.
|
||||
These parameters are handled internally."
|
||||
(let* ((result-params (assq :result-params params))
|
||||
(fresult (member "file" result-params))
|
||||
(fparam (assq :file params)))
|
||||
(setcar fresult "")
|
||||
(delq fparam params)))
|
||||
|
||||
(defconst org-babel-jupyter-async-inline-results-pending-indicator "???"
|
||||
"A string to disambiguate pending inline results from empty results.")
|
||||
|
||||
(defun org-babel-jupyter--execute (code async-p)
|
||||
(jupyter-run-with-client jupyter-current-client
|
||||
(let ((dreq (jupyter-execute-request :code code)))
|
||||
(jupyter-mlet* ((req (jupyter-org-maybe-queued dreq)))
|
||||
(jupyter-return
|
||||
`(,req
|
||||
,(cond
|
||||
(async-p
|
||||
(when (bound-and-true-p org-export-current-backend)
|
||||
(jupyter-add-idle-sync-hook
|
||||
'org-babel-after-execute-hook req 'append))
|
||||
(if (jupyter-org-request-inline-block-p req)
|
||||
org-babel-jupyter-async-inline-results-pending-indicator
|
||||
;; This returns the message ID of REQ as an indicator
|
||||
;; for the pending results.
|
||||
(jupyter-org-pending-async-results req)))
|
||||
(t
|
||||
(jupyter-idle-sync req)
|
||||
(if (jupyter-org-request-inline-block-p req)
|
||||
;; When evaluating a source block synchronously, only the
|
||||
;; :execute-result will be in `jupyter-org-request-results' since
|
||||
;; stream results and any displayed data will be placed in a separate
|
||||
;; buffer.
|
||||
(let ((el (jupyter-org-result
|
||||
req (car (jupyter-org-request-results req)))))
|
||||
(if (stringp el) el
|
||||
(org-element-property :value el)))
|
||||
;; This returns an Org formatted string of the collected
|
||||
;; results.
|
||||
(jupyter-org-sync-results req))))))))))
|
||||
|
||||
(defvar org-babel-jupyter-current-src-block-params nil
|
||||
"The block parameters of the most recently executed Jupyter source block.")
|
||||
|
||||
(defun org-babel-execute:jupyter (body params)
|
||||
"Execute BODY according to PARAMS.
|
||||
BODY is the code to execute for the current Jupyter `:session' in
|
||||
the PARAMS alist."
|
||||
(when org-babel-current-src-block-location
|
||||
(save-excursion
|
||||
(goto-char org-babel-current-src-block-location)
|
||||
(when (jupyter-org-request-at-point)
|
||||
(user-error "Source block currently being executed"))))
|
||||
(let* ((result-params (assq :result-params params))
|
||||
(async-p (jupyter-org-execute-async-p params)))
|
||||
(when (member "replace" result-params)
|
||||
(org-babel-jupyter-cleanup-file-links))
|
||||
(let* ((org-babel-jupyter-current-src-block-params params)
|
||||
(session (alist-get :session params))
|
||||
(buf (org-babel-jupyter-initiate-session session params))
|
||||
(jupyter-current-client (buffer-local-value 'jupyter-current-client buf))
|
||||
(lang (jupyter-kernel-language jupyter-current-client))
|
||||
(vars (org-babel-variable-assignments:jupyter params lang))
|
||||
(code (progn
|
||||
(when-let* ((dir (alist-get :dir params)))
|
||||
;; `default-directory' is already set according
|
||||
;; to :dir when executing a source block. Set
|
||||
;; :dir to the absolute path so that
|
||||
;; `org-babel-expand-body:jupyter' does not try
|
||||
;; to re-expand the path. See #302.
|
||||
(setf (alist-get :dir params) default-directory))
|
||||
(org-babel-expand-body:jupyter body params vars lang))))
|
||||
(pcase-let ((`(,req ,maybe-result)
|
||||
(org-babel-jupyter--execute code async-p)))
|
||||
;; KLUDGE: Remove the file result-parameter so that
|
||||
;; `org-babel-insert-result' doesn't attempt to handle it while
|
||||
;; async results are pending. Do the same in the synchronous
|
||||
;; case, but not if link or graphics are also result-parameters,
|
||||
;; only in Org >= 9.2, since those in combination with file mean
|
||||
;; to interpret the result as a file link, a useful meaning that
|
||||
;; doesn't interfere with Jupyter style result insertion.
|
||||
;;
|
||||
;; Do this after sending the request since
|
||||
;; `jupyter-generate-request' still needs access to the :file
|
||||
;; parameter.
|
||||
(when (and (member "file" result-params)
|
||||
(or async-p
|
||||
(not (or (member "link" result-params)
|
||||
(member "graphics" result-params)))))
|
||||
(org-babel-jupyter--remove-file-param params))
|
||||
(prog1 maybe-result
|
||||
;; KLUDGE: Add the "raw" result parameter for non-inline
|
||||
;; synchronous results because an Org formatted string is
|
||||
;; already returned in that case and
|
||||
;; `org-babel-insert-result' should not process it.
|
||||
(unless (or async-p
|
||||
(jupyter-org-request-inline-block-p req))
|
||||
(nconc (alist-get :result-params params) (list "raw"))))))))
|
||||
|
||||
;;; Overriding source block languages, language aliases
|
||||
|
||||
(defvar org-babel-jupyter--babel-ops
|
||||
'(execute expand-body prep-session edit-prep
|
||||
variable-assignments load-session
|
||||
initiate))
|
||||
|
||||
(defvar org-babel-jupyter--babel-vars
|
||||
'(header-args default-header-args))
|
||||
|
||||
(defun org-babel-jupyter--babel-op-symbol (op lang)
|
||||
(if (eq op 'initiate)
|
||||
(intern (format "org-babel-%s-initiate-session" lang))
|
||||
(intern (format (format "org-babel-%s:%s" op lang)))))
|
||||
|
||||
(defun org-babel-jupyter--babel-var-symbol (var lang)
|
||||
(intern (format "org-babel-%s:%s" var lang)))
|
||||
|
||||
(defun org-babel-jupyter--babel-map (alias-action
|
||||
var-action)
|
||||
"Loop over Org babel function and variable symbols.
|
||||
ALIAS-ACTION and VAR-ACTION are functions of one argument.
|
||||
|
||||
When ALIAS-ACTION is called, the argument will be a symbol that
|
||||
represents an Org Babel operation that can be defined by a
|
||||
language extension to Org Babel, e.g. \\='execute.
|
||||
|
||||
Similarly VAR-ACTION is called with a symbol representing an Org
|
||||
Babel variable that can be defined for a language,
|
||||
e.g. \\='header-args."
|
||||
(declare (indent 0))
|
||||
(dolist (op org-babel-jupyter--babel-ops)
|
||||
(funcall alias-action op))
|
||||
(dolist (var org-babel-jupyter--babel-vars)
|
||||
(funcall var-action var)))
|
||||
|
||||
(defun org-babel-jupyter-override-src-block (lang)
|
||||
"Override the built-in `org-babel' functions for LANG.
|
||||
This overrides functions like `org-babel-execute:LANG' and
|
||||
`org-babel-LANG-initiate-session' to use the machinery of
|
||||
jupyter-LANG source blocks.
|
||||
|
||||
Also, set `org-babel-header-args:LANG' to the value of
|
||||
`org-babel-header-args:jupyter-LANG', if the latter exists. If
|
||||
`org-babel-header-args:LANG' had a value, save it as a symbol
|
||||
property of `org-babel-header-args:LANG' for restoring it later.
|
||||
Do the same for `org-babel-default-header-args:LANG'."
|
||||
(org-babel-jupyter--babel-map
|
||||
(lambda (op)
|
||||
;; Only override operations that are not related to a particular
|
||||
;; language.
|
||||
(unless (memq op '(variable-assignments expand-body))
|
||||
(let ((lang-op
|
||||
(org-babel-jupyter--babel-op-symbol
|
||||
op lang))
|
||||
(jupyter-lang-op
|
||||
(org-babel-jupyter--babel-op-symbol
|
||||
op (format "jupyter-%s" lang))))
|
||||
;; If a language doesn't have a function assigned, set one so it can
|
||||
;; be overridden
|
||||
(unless (fboundp lang-op)
|
||||
(fset lang-op #'ignore))
|
||||
(advice-add lang-op :override jupyter-lang-op
|
||||
'((name . ob-jupyter))))))
|
||||
(lambda (var)
|
||||
(let ((lang-var
|
||||
(org-babel-jupyter--babel-var-symbol
|
||||
var lang))
|
||||
(jupyter-lang-var
|
||||
(org-babel-jupyter--babel-var-symbol
|
||||
var (format "jupyter-%s" lang))))
|
||||
(when (boundp jupyter-lang-var)
|
||||
(when (boundp lang-var)
|
||||
(put lang-var 'jupyter-restore-value (symbol-value lang-var)))
|
||||
(set lang-var (copy-tree (symbol-value jupyter-lang-var))))))))
|
||||
|
||||
(defun org-babel-jupyter-restore-src-block (lang)
|
||||
"Restore the overridden `org-babel' functions for LANG.
|
||||
This undoes everything that
|
||||
`org-babel-jupyter-override-src-block' did."
|
||||
(org-babel-jupyter--babel-map
|
||||
(lambda (op)
|
||||
;; Only override operations that are not related to a particular
|
||||
;; language.
|
||||
(unless (memq op '(variable-assignments expand-body))
|
||||
(let ((lang-op
|
||||
(org-babel-jupyter--babel-op-symbol
|
||||
op lang))
|
||||
(jupyter-lang-op
|
||||
(org-babel-jupyter--babel-op-symbol
|
||||
op (format "jupyter-%s" lang))))
|
||||
(advice-remove lang-op jupyter-lang-op)
|
||||
;; The function didn't have a definition, so
|
||||
;; ensure that we restore that fact.
|
||||
(when (eq (symbol-function lang-op) #'ignore)
|
||||
(fmakunbound lang-op)))))
|
||||
(lambda (var)
|
||||
(let ((lang-var
|
||||
(org-babel-jupyter--babel-var-symbol
|
||||
var lang)))
|
||||
(when (boundp lang-var)
|
||||
(set lang-var (get lang-var 'jupyter-restore-value)))))))
|
||||
|
||||
(defun org-babel-jupyter-make-language-alias (kernel lang)
|
||||
"Similar to `org-babel-make-language-alias' but for Jupyter src-blocks.
|
||||
KERNEL should be the name of the default kernel to use for kernel
|
||||
LANG, the language of the kernel.
|
||||
|
||||
The Org Babel functions `org-babel-FN:jupyter-LANG', where FN is
|
||||
one of execute, expand-body, prep-session, edit-prep,
|
||||
variable-assignments, or load-session, are aliased to
|
||||
`org-babel-FN:jupyter'. Similarly,
|
||||
`org-babel-jupyter-LANG-initiate-session' is aliased to
|
||||
`org-babel-jupyter-initiate-session'.
|
||||
|
||||
If not already defined, the variable
|
||||
`org-babel-default-header-args:jupyter-LANG' is set to the same
|
||||
value as `org-babel-header-args:jupyter', which see. The
|
||||
variable `org-babel-default-header-args:jupyter-LANG' is also set
|
||||
to
|
||||
|
||||
\((:async . \"no\")
|
||||
\(:kernel . KERNEL))
|
||||
|
||||
if that variable does not already have a value.
|
||||
|
||||
If LANG has an association in `org-babel-tangle-lang-exts',
|
||||
associate the same value with jupyter-LANG, if needed.
|
||||
Similarly, associate the same value for LANG in
|
||||
`org-src-lang-modes'."
|
||||
(org-babel-jupyter--babel-map
|
||||
(lambda (op)
|
||||
(defalias (org-babel-jupyter--babel-op-symbol
|
||||
op (format "jupyter-%s" lang))
|
||||
(org-babel-jupyter--babel-op-symbol
|
||||
op "jupyter")))
|
||||
(lambda (var)
|
||||
(let ((jupyter-var
|
||||
(org-babel-jupyter--babel-var-symbol
|
||||
var "jupyter"))
|
||||
(jupyter-lang-var
|
||||
(org-babel-jupyter--babel-var-symbol
|
||||
var (format "jupyter-%s" lang))))
|
||||
(unless (boundp jupyter-lang-var)
|
||||
(set jupyter-lang-var (copy-tree (symbol-value jupyter-var)))
|
||||
(cond
|
||||
((eq var 'default-header-args)
|
||||
;; Needed since the default kernel is not language
|
||||
;; specific and it needs to be.
|
||||
(setf (alist-get :kernel (symbol-value jupyter-lang-var)) kernel)
|
||||
(put jupyter-lang-var 'variable-documentation
|
||||
(format
|
||||
"Default header arguments for Jupyter %s src-blocks"
|
||||
lang)))
|
||||
(t
|
||||
(put jupyter-lang-var 'variable-documentation
|
||||
(get jupyter-var 'variable-documentation))))))))
|
||||
(when (assoc lang org-babel-tangle-lang-exts)
|
||||
(add-to-list 'org-babel-tangle-lang-exts
|
||||
(cons (concat "jupyter-" lang)
|
||||
(cdr (assoc lang org-babel-tangle-lang-exts)))))
|
||||
(add-to-list 'org-src-lang-modes
|
||||
(cons (concat "jupyter-" lang)
|
||||
(or (cdr (assoc lang org-src-lang-modes))
|
||||
(intern (downcase (replace-regexp-in-string
|
||||
"[0-9]*" "" lang)))))))
|
||||
|
||||
(defun org-babel-jupyter-aliases-from-kernelspecs (&optional refresh specs)
|
||||
"Make language aliases based on the available kernelspecs.
|
||||
For all kernel SPECS, make a language alias for the kernel
|
||||
language if one does not already exist. The alias is created with
|
||||
`org-babel-jupyter-make-language-alias'.
|
||||
|
||||
SPECS defaults to those associated with the `default-directory'.
|
||||
Optional argument REFRESH has the same meaning as in
|
||||
`jupyter-kernelspecs'.
|
||||
|
||||
Note, spaces in the kernel language name are converted into
|
||||
dashes in the language alias, e.g.
|
||||
|
||||
Wolfram Language -> jupyter-Wolfram-Language
|
||||
|
||||
For convenience, after creating a language alias for a kernel
|
||||
language LANG, set the :kernel default header argument if not
|
||||
present in `org-babel-default-header-args:jupyter-LANG', see
|
||||
`org-babel-header-args:jupyter'. This allows users to set that
|
||||
variable in their configurations without having to also set the
|
||||
:kernel header argument since it is common for only one per
|
||||
language to exist on someone's system."
|
||||
(cl-loop
|
||||
for spec in (or specs
|
||||
(with-demoted-errors "Error retrieving kernelspecs: %S"
|
||||
(jupyter-kernelspecs default-directory refresh)))
|
||||
for kernel = (jupyter-kernelspec-name spec)
|
||||
for lang = (let ((lang (jupyter-canonicalize-language-string
|
||||
(plist-get (jupyter-kernelspec-plist spec) :language))))
|
||||
(or (cadr (assoc lang org-babel-jupyter-language-aliases))
|
||||
lang))
|
||||
unless (member lang languages) collect lang into languages and
|
||||
do (org-babel-jupyter-make-language-alias kernel lang)
|
||||
;; KLUDGE: The :kernel header argument is always set, even when we aren't
|
||||
;; the ones who originally set the defaults. This is here for convenience
|
||||
;; since usually a user does not set :kernel directly.
|
||||
(let ((var (intern (concat "org-babel-default-header-args:jupyter-" lang))))
|
||||
(unless (alist-get :kernel (symbol-value var))
|
||||
(setf (alist-get :kernel (symbol-value var)) kernel)))))
|
||||
|
||||
;;; `ox' integration
|
||||
|
||||
(defvar org-latex-minted-langs)
|
||||
|
||||
(defun org-babel-jupyter-setup-export (backend)
|
||||
"Ensure that Jupyter src-blocks are integrated with BACKEND.
|
||||
Currently this makes sure that Jupyter src-block languages are
|
||||
mapped to their appropriate minted language in
|
||||
`org-latex-minted-langs' if BACKEND is latex."
|
||||
(cond
|
||||
((org-export-derived-backend-p backend 'latex)
|
||||
(cl-loop
|
||||
for spec in (jupyter-kernelspecs default-directory)
|
||||
for lang = (plist-get (jupyter-kernelspec-plist spec) :language)
|
||||
do (cl-pushnew (list (intern (concat "jupyter-" lang)) lang)
|
||||
org-latex-minted-langs :test #'equal)))))
|
||||
|
||||
(defun org-babel-jupyter-strip-ansi-escapes (_backend)
|
||||
"Remove ANSI escapes from Jupyter src-block results in the current buffer."
|
||||
(org-babel-map-src-blocks nil
|
||||
(when (org-babel-jupyter-language-p lang)
|
||||
(when-let* ((pos (org-babel-where-is-src-block-result))
|
||||
(ansi-color-apply-face-function
|
||||
(lambda (beg end face)
|
||||
;; Could be useful for export backends
|
||||
(when face
|
||||
(put-text-property beg end 'face face)))))
|
||||
(goto-char pos)
|
||||
(ansi-color-apply-on-region (point) (org-babel-result-end))))))
|
||||
|
||||
;;; Hook into `org'
|
||||
|
||||
;; Defer generation of the aliases until Org is enabled in a buffer to
|
||||
;; avoid generating them at top-level when loading ob-jupyter. Some
|
||||
;; users, e.g. those who use conda environments, may not have a
|
||||
;; jupyter command available at load time.
|
||||
(defun org-babel-jupyter-make-local-aliases ()
|
||||
(let ((default-directory user-emacs-directory))
|
||||
(org-babel-jupyter-aliases-from-kernelspecs)))
|
||||
(add-hook 'org-mode-hook #'org-babel-jupyter-make-local-aliases 10)
|
||||
|
||||
(add-hook 'org-export-before-processing-functions #'org-babel-jupyter-setup-export)
|
||||
(add-hook 'org-export-before-parsing-functions #'org-babel-jupyter-strip-ansi-escapes)
|
||||
|
||||
(provide 'ob-jupyter)
|
||||
|
||||
;;; ob-jupyter.el ends here
|
||||
33
lisp/jupyter/widget.html
Normal file
33
lisp/jupyter/widget.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Jupyter Client</title>
|
||||
<script type="application/javascript" src="/jupyter"></script>
|
||||
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.min.js"></script>
|
||||
<style type="text/css">
|
||||
* {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<script type="application/javascript">
|
||||
var kernel;
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
// TODO: May not be available everywhere
|
||||
var p = new URLSearchParams(window.location.search);
|
||||
var kernel = new EmacsJupyter({username: p.get('username'),
|
||||
clientId: p.get('clientId')},
|
||||
p.get('port'));
|
||||
var commManager = new CommManager(kernel);
|
||||
var widgetManager = new WidgetManager(kernel, document.getElementById("widget"));
|
||||
commManager.register_target(widgetManager.comm_target_name, function(comm, msg) {
|
||||
widgetManager.handle_comm_open(comm, msg);
|
||||
});
|
||||
kernel.widgetManager = widgetManager;
|
||||
kernel.commManager = commManager;
|
||||
window.kernel = kernel;
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
1108
lisp/ox-ipynb.el
Normal file
1108
lisp/ox-ipynb.el
Normal file
File diff suppressed because it is too large
Load Diff
12
lisp/request/request-pkg.el
Normal file
12
lisp/request/request-pkg.el
Normal file
@@ -0,0 +1,12 @@
|
||||
(define-package "request" "20230127.417" "Compatible layer for URL request"
|
||||
'((emacs "24.4"))
|
||||
:commit "01e338c335c07e4407239619e57361944a82cb8a" :authors
|
||||
'(("Takafumi Arakaki <aka.tkf at gmail.com>"))
|
||||
:maintainers
|
||||
'(("Takafumi Arakaki <aka.tkf at gmail.com>"))
|
||||
:maintainer
|
||||
'("Takafumi Arakaki <aka.tkf at gmail.com>")
|
||||
:url "https://github.com/tkf/emacs-request")
|
||||
;; Local Variables:
|
||||
;; no-byte-compile: t
|
||||
;; End:
|
||||
1234
lisp/request/request.el
Normal file
1234
lisp/request/request.el
Normal file
File diff suppressed because it is too large
Load Diff
6
lisp/zmq/.ipynb_checkpoints/Untitled-checkpoint.ipynb
Normal file
6
lisp/zmq/.ipynb_checkpoints/Untitled-checkpoint.ipynb
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"cells": [],
|
||||
"metadata": {},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
93
lisp/zmq/Makefile
Normal file
93
lisp/zmq/Makefile
Normal file
@@ -0,0 +1,93 @@
|
||||
ROOT = .
|
||||
SHELL = bash
|
||||
EMACS ?= emacs
|
||||
EFILES := zmq.el
|
||||
# CPPFLAGS = -DEZMQ_DEBUG=0
|
||||
ELCFILES = $(EFILES:.el=.elc)
|
||||
|
||||
export ZMQ_GIT_REPO ?= https://github.com/zeromq/libzmq
|
||||
# The version of ZMQ to build
|
||||
export ZMQ_VERSION ?= 4.3.1
|
||||
# Directory in which the emacs-zmq module will be written
|
||||
EZMQ_LIBDIR ?= $(CURDIR)
|
||||
# NOTE: The ZMQ_LIBS and ZMQ_CFLAGS can be set before configuring the project
|
||||
# to point to the ZMQ to build with.
|
||||
|
||||
MODULE_EXT := $(shell $(EMACS) -Q --batch --eval "(princ (and (boundp 'module-file-suffix) module-file-suffix))")
|
||||
ifeq ($(MODULE_EXT), nil)
|
||||
$(error No module support in $(EMACS))
|
||||
endif
|
||||
EZMQ_MODULE := emacs-zmq$(MODULE_EXT)
|
||||
|
||||
.PHONY: all
|
||||
all: $(EZMQ_LIBDIR)/$(EZMQ_MODULE) compile
|
||||
|
||||
.PHONY: configure
|
||||
configure: src/configure
|
||||
cd src && ./configure CPPFLAGS="$(CPPFLAGS)" \
|
||||
--prefix=$(CURDIR) \
|
||||
--enable-shared=emacs-zmq --enable-static=zeromq \
|
||||
--without-docs --enable-drafts=yes --enable-libunwind=no \
|
||||
--disable-curve-keygen --disable-perf --disable-eventfd
|
||||
|
||||
$(EZMQ_LIBDIR)/$(EZMQ_MODULE): src/Makefile
|
||||
$(MAKE) -C src
|
||||
mkdir -p $(EZMQ_LIBDIR)
|
||||
cp src/.libs/$(EZMQ_MODULE) $(EZMQ_LIBDIR)/$(EZMQ_MODULE)
|
||||
|
||||
src/Makefile: src/configure
|
||||
$(MAKE) configure
|
||||
|
||||
# Needed for static Windows builds of libzmq, see libzmq/INSTALL
|
||||
ifeq ($(MODULE_EXT),.dll)
|
||||
CPPFLAGS += -DZMQ_STATIC
|
||||
endif
|
||||
|
||||
src/configure: src/configure.ac src/Makefile.am
|
||||
cd src && autoreconf -i
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(EMACS) -nw -Q -batch -L . -l ert -l zmq-tests.el \
|
||||
--eval "(ert-run-tests-batch-and-exit)"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(MAKE) -C src clean
|
||||
$(RM) emacs-zmq.* $(ELCFILES)
|
||||
|
||||
.PHONY: clean-zmq-build
|
||||
clean-zmq-build:
|
||||
$(RM) -r src/libzmq-build
|
||||
$(MAKE) -C src clean-libzmq
|
||||
|
||||
.PHONY: compile
|
||||
compile: $(ELCFILES)
|
||||
|
||||
$(ELCFILES): %.elc: %.el
|
||||
$(EMACS) --batch -Q -L . -f batch-byte-compile $<
|
||||
|
||||
ifneq (,$(filter products,$(MAKECMDGOALS)))
|
||||
ifeq (,$(shell which $(CC)))
|
||||
$(error "Compiler $(CC) not found.")
|
||||
endif
|
||||
PRODUCT := emacs-zmq-$(shell $(CC) -dumpmachine)
|
||||
ifneq ($(shell command -v shasum),)
|
||||
SHA256SUM := shasum -a 256
|
||||
else
|
||||
SHA256SUM := sha256sum
|
||||
endif
|
||||
endif
|
||||
|
||||
.PHONY: products
|
||||
products: products/$(PRODUCT).tar.gz.sha256
|
||||
|
||||
products/$(PRODUCT).tar.gz: $(EZMQ_LIBDIR)/$(EZMQ_MODULE)
|
||||
mkdir -p products/$(PRODUCT)
|
||||
cp $(EZMQ_LIBDIR)/*$(EZMQ_MODULE) products/$(PRODUCT)
|
||||
cd products && \
|
||||
tar -czf $(CURDIR)/products/$(PRODUCT).tar.gz $(PRODUCT)
|
||||
|
||||
products/$(PRODUCT).tar.gz.sha256: products/$(PRODUCT).tar.gz
|
||||
cd products && \
|
||||
$(SHA256SUM) $(PRODUCT).tar.gz > $(PRODUCT).tar.gz.sha256
|
||||
BIN
lisp/zmq/emacs-zmq.so
Executable file
BIN
lisp/zmq/emacs-zmq.so
Executable file
Binary file not shown.
148
lisp/zmq/src/.deps/emacs_zmq_la-constants.Plo
Normal file
148
lisp/zmq/src/.deps/emacs_zmq_la-constants.Plo
Normal file
@@ -0,0 +1,148 @@
|
||||
emacs_zmq_la-constants.lo: constants.c /usr/include/stdc-predef.h core.h \
|
||||
/usr/include/stdlib.h /usr/include/bits/libc-header-start.h \
|
||||
/usr/include/features.h /usr/include/features-time64.h \
|
||||
/usr/include/bits/wordsize.h /usr/include/bits/timesize.h \
|
||||
/usr/include/sys/cdefs.h /usr/include/bits/long-double.h \
|
||||
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h \
|
||||
/usr/include/bits/waitflags.h /usr/include/bits/waitstatus.h \
|
||||
/usr/include/bits/floatn.h /usr/include/bits/floatn-common.h \
|
||||
/usr/include/sys/types.h /usr/include/bits/types.h \
|
||||
/usr/include/bits/typesizes.h /usr/include/bits/time64.h \
|
||||
/usr/include/bits/types/clock_t.h /usr/include/bits/types/clockid_t.h \
|
||||
/usr/include/bits/types/time_t.h /usr/include/bits/types/timer_t.h \
|
||||
/usr/include/bits/stdint-intn.h /usr/include/endian.h \
|
||||
/usr/include/bits/endian.h /usr/include/bits/endianness.h \
|
||||
/usr/include/bits/byteswap.h /usr/include/bits/uintn-identity.h \
|
||||
/usr/include/sys/select.h /usr/include/bits/select.h \
|
||||
/usr/include/bits/types/sigset_t.h /usr/include/bits/types/__sigset_t.h \
|
||||
/usr/include/bits/types/struct_timeval.h \
|
||||
/usr/include/bits/types/struct_timespec.h \
|
||||
/usr/include/bits/pthreadtypes.h /usr/include/bits/thread-shared-types.h \
|
||||
/usr/include/bits/pthreadtypes-arch.h \
|
||||
/usr/include/bits/atomic_wide_counter.h /usr/include/bits/struct_mutex.h \
|
||||
/usr/include/bits/struct_rwlock.h /usr/include/alloca.h \
|
||||
/usr/include/bits/stdlib-bsearch.h /usr/include/bits/stdlib-float.h \
|
||||
/usr/include/string.h /usr/include/bits/types/locale_t.h \
|
||||
/usr/include/bits/types/__locale_t.h /usr/include/strings.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h \
|
||||
/usr/include/zmq.h /usr/include/errno.h /usr/include/bits/errno.h \
|
||||
/usr/include/linux/errno.h /usr/include/asm/errno.h \
|
||||
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h \
|
||||
/usr/include/stdio.h /usr/include/bits/types/__fpos_t.h \
|
||||
/usr/include/bits/types/__mbstate_t.h \
|
||||
/usr/include/bits/types/__fpos64_t.h /usr/include/bits/types/__FILE.h \
|
||||
/usr/include/bits/types/FILE.h /usr/include/bits/types/struct_FILE.h \
|
||||
/usr/include/bits/types/cookie_io_functions_t.h \
|
||||
/usr/include/bits/stdio_lim.h /usr/include/bits/stdio.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h \
|
||||
/usr/include/stdint.h /usr/include/bits/wchar.h \
|
||||
/usr/include/bits/stdint-uintn.h /usr/include/bits/stdint-least.h \
|
||||
/usr/include/signal.h /usr/include/bits/signum-generic.h \
|
||||
/usr/include/bits/signum-arch.h /usr/include/bits/types/sig_atomic_t.h \
|
||||
/usr/include/bits/types/siginfo_t.h /usr/include/bits/types/__sigval_t.h \
|
||||
/usr/include/bits/siginfo-arch.h /usr/include/bits/siginfo-consts.h \
|
||||
/usr/include/bits/types/sigval_t.h /usr/include/bits/types/sigevent_t.h \
|
||||
/usr/include/bits/sigevent-consts.h /usr/include/bits/sigaction.h \
|
||||
/usr/include/bits/sigcontext.h /usr/include/bits/types/stack_t.h \
|
||||
/usr/include/sys/ucontext.h /usr/include/bits/sigstack.h \
|
||||
/usr/include/bits/sigstksz.h /usr/include/bits/ss_flags.h \
|
||||
/usr/include/bits/types/struct_sigstack.h /usr/include/bits/sigthread.h \
|
||||
/usr/include/bits/signal_ext.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h emacs-module.h
|
||||
/usr/include/stdc-predef.h:
|
||||
core.h:
|
||||
/usr/include/stdlib.h:
|
||||
/usr/include/bits/libc-header-start.h:
|
||||
/usr/include/features.h:
|
||||
/usr/include/features-time64.h:
|
||||
/usr/include/bits/wordsize.h:
|
||||
/usr/include/bits/timesize.h:
|
||||
/usr/include/sys/cdefs.h:
|
||||
/usr/include/bits/long-double.h:
|
||||
/usr/include/gnu/stubs.h:
|
||||
/usr/include/gnu/stubs-64.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h:
|
||||
/usr/include/bits/waitflags.h:
|
||||
/usr/include/bits/waitstatus.h:
|
||||
/usr/include/bits/floatn.h:
|
||||
/usr/include/bits/floatn-common.h:
|
||||
/usr/include/sys/types.h:
|
||||
/usr/include/bits/types.h:
|
||||
/usr/include/bits/typesizes.h:
|
||||
/usr/include/bits/time64.h:
|
||||
/usr/include/bits/types/clock_t.h:
|
||||
/usr/include/bits/types/clockid_t.h:
|
||||
/usr/include/bits/types/time_t.h:
|
||||
/usr/include/bits/types/timer_t.h:
|
||||
/usr/include/bits/stdint-intn.h:
|
||||
/usr/include/endian.h:
|
||||
/usr/include/bits/endian.h:
|
||||
/usr/include/bits/endianness.h:
|
||||
/usr/include/bits/byteswap.h:
|
||||
/usr/include/bits/uintn-identity.h:
|
||||
/usr/include/sys/select.h:
|
||||
/usr/include/bits/select.h:
|
||||
/usr/include/bits/types/sigset_t.h:
|
||||
/usr/include/bits/types/__sigset_t.h:
|
||||
/usr/include/bits/types/struct_timeval.h:
|
||||
/usr/include/bits/types/struct_timespec.h:
|
||||
/usr/include/bits/pthreadtypes.h:
|
||||
/usr/include/bits/thread-shared-types.h:
|
||||
/usr/include/bits/pthreadtypes-arch.h:
|
||||
/usr/include/bits/atomic_wide_counter.h:
|
||||
/usr/include/bits/struct_mutex.h:
|
||||
/usr/include/bits/struct_rwlock.h:
|
||||
/usr/include/alloca.h:
|
||||
/usr/include/bits/stdlib-bsearch.h:
|
||||
/usr/include/bits/stdlib-float.h:
|
||||
/usr/include/string.h:
|
||||
/usr/include/bits/types/locale_t.h:
|
||||
/usr/include/bits/types/__locale_t.h:
|
||||
/usr/include/strings.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h:
|
||||
/usr/include/zmq.h:
|
||||
/usr/include/errno.h:
|
||||
/usr/include/bits/errno.h:
|
||||
/usr/include/linux/errno.h:
|
||||
/usr/include/asm/errno.h:
|
||||
/usr/include/asm-generic/errno.h:
|
||||
/usr/include/asm-generic/errno-base.h:
|
||||
/usr/include/stdio.h:
|
||||
/usr/include/bits/types/__fpos_t.h:
|
||||
/usr/include/bits/types/__mbstate_t.h:
|
||||
/usr/include/bits/types/__fpos64_t.h:
|
||||
/usr/include/bits/types/__FILE.h:
|
||||
/usr/include/bits/types/FILE.h:
|
||||
/usr/include/bits/types/struct_FILE.h:
|
||||
/usr/include/bits/types/cookie_io_functions_t.h:
|
||||
/usr/include/bits/stdio_lim.h:
|
||||
/usr/include/bits/stdio.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h:
|
||||
/usr/include/stdint.h:
|
||||
/usr/include/bits/wchar.h:
|
||||
/usr/include/bits/stdint-uintn.h:
|
||||
/usr/include/bits/stdint-least.h:
|
||||
/usr/include/signal.h:
|
||||
/usr/include/bits/signum-generic.h:
|
||||
/usr/include/bits/signum-arch.h:
|
||||
/usr/include/bits/types/sig_atomic_t.h:
|
||||
/usr/include/bits/types/siginfo_t.h:
|
||||
/usr/include/bits/types/__sigval_t.h:
|
||||
/usr/include/bits/siginfo-arch.h:
|
||||
/usr/include/bits/siginfo-consts.h:
|
||||
/usr/include/bits/types/sigval_t.h:
|
||||
/usr/include/bits/types/sigevent_t.h:
|
||||
/usr/include/bits/sigevent-consts.h:
|
||||
/usr/include/bits/sigaction.h:
|
||||
/usr/include/bits/sigcontext.h:
|
||||
/usr/include/bits/types/stack_t.h:
|
||||
/usr/include/sys/ucontext.h:
|
||||
/usr/include/bits/sigstack.h:
|
||||
/usr/include/bits/sigstksz.h:
|
||||
/usr/include/bits/ss_flags.h:
|
||||
/usr/include/bits/types/struct_sigstack.h:
|
||||
/usr/include/bits/sigthread.h:
|
||||
/usr/include/bits/signal_ext.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h:
|
||||
emacs-module.h:
|
||||
149
lisp/zmq/src/.deps/emacs_zmq_la-context.Plo
Normal file
149
lisp/zmq/src/.deps/emacs_zmq_la-context.Plo
Normal file
@@ -0,0 +1,149 @@
|
||||
emacs_zmq_la-context.lo: context.c /usr/include/stdc-predef.h context.h \
|
||||
core.h /usr/include/stdlib.h /usr/include/bits/libc-header-start.h \
|
||||
/usr/include/features.h /usr/include/features-time64.h \
|
||||
/usr/include/bits/wordsize.h /usr/include/bits/timesize.h \
|
||||
/usr/include/sys/cdefs.h /usr/include/bits/long-double.h \
|
||||
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h \
|
||||
/usr/include/bits/waitflags.h /usr/include/bits/waitstatus.h \
|
||||
/usr/include/bits/floatn.h /usr/include/bits/floatn-common.h \
|
||||
/usr/include/sys/types.h /usr/include/bits/types.h \
|
||||
/usr/include/bits/typesizes.h /usr/include/bits/time64.h \
|
||||
/usr/include/bits/types/clock_t.h /usr/include/bits/types/clockid_t.h \
|
||||
/usr/include/bits/types/time_t.h /usr/include/bits/types/timer_t.h \
|
||||
/usr/include/bits/stdint-intn.h /usr/include/endian.h \
|
||||
/usr/include/bits/endian.h /usr/include/bits/endianness.h \
|
||||
/usr/include/bits/byteswap.h /usr/include/bits/uintn-identity.h \
|
||||
/usr/include/sys/select.h /usr/include/bits/select.h \
|
||||
/usr/include/bits/types/sigset_t.h /usr/include/bits/types/__sigset_t.h \
|
||||
/usr/include/bits/types/struct_timeval.h \
|
||||
/usr/include/bits/types/struct_timespec.h \
|
||||
/usr/include/bits/pthreadtypes.h /usr/include/bits/thread-shared-types.h \
|
||||
/usr/include/bits/pthreadtypes-arch.h \
|
||||
/usr/include/bits/atomic_wide_counter.h /usr/include/bits/struct_mutex.h \
|
||||
/usr/include/bits/struct_rwlock.h /usr/include/alloca.h \
|
||||
/usr/include/bits/stdlib-bsearch.h /usr/include/bits/stdlib-float.h \
|
||||
/usr/include/string.h /usr/include/bits/types/locale_t.h \
|
||||
/usr/include/bits/types/__locale_t.h /usr/include/strings.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h \
|
||||
/usr/include/zmq.h /usr/include/errno.h /usr/include/bits/errno.h \
|
||||
/usr/include/linux/errno.h /usr/include/asm/errno.h \
|
||||
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h \
|
||||
/usr/include/stdio.h /usr/include/bits/types/__fpos_t.h \
|
||||
/usr/include/bits/types/__mbstate_t.h \
|
||||
/usr/include/bits/types/__fpos64_t.h /usr/include/bits/types/__FILE.h \
|
||||
/usr/include/bits/types/FILE.h /usr/include/bits/types/struct_FILE.h \
|
||||
/usr/include/bits/types/cookie_io_functions_t.h \
|
||||
/usr/include/bits/stdio_lim.h /usr/include/bits/stdio.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h \
|
||||
/usr/include/stdint.h /usr/include/bits/wchar.h \
|
||||
/usr/include/bits/stdint-uintn.h /usr/include/bits/stdint-least.h \
|
||||
/usr/include/signal.h /usr/include/bits/signum-generic.h \
|
||||
/usr/include/bits/signum-arch.h /usr/include/bits/types/sig_atomic_t.h \
|
||||
/usr/include/bits/types/siginfo_t.h /usr/include/bits/types/__sigval_t.h \
|
||||
/usr/include/bits/siginfo-arch.h /usr/include/bits/siginfo-consts.h \
|
||||
/usr/include/bits/types/sigval_t.h /usr/include/bits/types/sigevent_t.h \
|
||||
/usr/include/bits/sigevent-consts.h /usr/include/bits/sigaction.h \
|
||||
/usr/include/bits/sigcontext.h /usr/include/bits/types/stack_t.h \
|
||||
/usr/include/sys/ucontext.h /usr/include/bits/sigstack.h \
|
||||
/usr/include/bits/sigstksz.h /usr/include/bits/ss_flags.h \
|
||||
/usr/include/bits/types/struct_sigstack.h /usr/include/bits/sigthread.h \
|
||||
/usr/include/bits/signal_ext.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h emacs-module.h
|
||||
/usr/include/stdc-predef.h:
|
||||
context.h:
|
||||
core.h:
|
||||
/usr/include/stdlib.h:
|
||||
/usr/include/bits/libc-header-start.h:
|
||||
/usr/include/features.h:
|
||||
/usr/include/features-time64.h:
|
||||
/usr/include/bits/wordsize.h:
|
||||
/usr/include/bits/timesize.h:
|
||||
/usr/include/sys/cdefs.h:
|
||||
/usr/include/bits/long-double.h:
|
||||
/usr/include/gnu/stubs.h:
|
||||
/usr/include/gnu/stubs-64.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h:
|
||||
/usr/include/bits/waitflags.h:
|
||||
/usr/include/bits/waitstatus.h:
|
||||
/usr/include/bits/floatn.h:
|
||||
/usr/include/bits/floatn-common.h:
|
||||
/usr/include/sys/types.h:
|
||||
/usr/include/bits/types.h:
|
||||
/usr/include/bits/typesizes.h:
|
||||
/usr/include/bits/time64.h:
|
||||
/usr/include/bits/types/clock_t.h:
|
||||
/usr/include/bits/types/clockid_t.h:
|
||||
/usr/include/bits/types/time_t.h:
|
||||
/usr/include/bits/types/timer_t.h:
|
||||
/usr/include/bits/stdint-intn.h:
|
||||
/usr/include/endian.h:
|
||||
/usr/include/bits/endian.h:
|
||||
/usr/include/bits/endianness.h:
|
||||
/usr/include/bits/byteswap.h:
|
||||
/usr/include/bits/uintn-identity.h:
|
||||
/usr/include/sys/select.h:
|
||||
/usr/include/bits/select.h:
|
||||
/usr/include/bits/types/sigset_t.h:
|
||||
/usr/include/bits/types/__sigset_t.h:
|
||||
/usr/include/bits/types/struct_timeval.h:
|
||||
/usr/include/bits/types/struct_timespec.h:
|
||||
/usr/include/bits/pthreadtypes.h:
|
||||
/usr/include/bits/thread-shared-types.h:
|
||||
/usr/include/bits/pthreadtypes-arch.h:
|
||||
/usr/include/bits/atomic_wide_counter.h:
|
||||
/usr/include/bits/struct_mutex.h:
|
||||
/usr/include/bits/struct_rwlock.h:
|
||||
/usr/include/alloca.h:
|
||||
/usr/include/bits/stdlib-bsearch.h:
|
||||
/usr/include/bits/stdlib-float.h:
|
||||
/usr/include/string.h:
|
||||
/usr/include/bits/types/locale_t.h:
|
||||
/usr/include/bits/types/__locale_t.h:
|
||||
/usr/include/strings.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h:
|
||||
/usr/include/zmq.h:
|
||||
/usr/include/errno.h:
|
||||
/usr/include/bits/errno.h:
|
||||
/usr/include/linux/errno.h:
|
||||
/usr/include/asm/errno.h:
|
||||
/usr/include/asm-generic/errno.h:
|
||||
/usr/include/asm-generic/errno-base.h:
|
||||
/usr/include/stdio.h:
|
||||
/usr/include/bits/types/__fpos_t.h:
|
||||
/usr/include/bits/types/__mbstate_t.h:
|
||||
/usr/include/bits/types/__fpos64_t.h:
|
||||
/usr/include/bits/types/__FILE.h:
|
||||
/usr/include/bits/types/FILE.h:
|
||||
/usr/include/bits/types/struct_FILE.h:
|
||||
/usr/include/bits/types/cookie_io_functions_t.h:
|
||||
/usr/include/bits/stdio_lim.h:
|
||||
/usr/include/bits/stdio.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h:
|
||||
/usr/include/stdint.h:
|
||||
/usr/include/bits/wchar.h:
|
||||
/usr/include/bits/stdint-uintn.h:
|
||||
/usr/include/bits/stdint-least.h:
|
||||
/usr/include/signal.h:
|
||||
/usr/include/bits/signum-generic.h:
|
||||
/usr/include/bits/signum-arch.h:
|
||||
/usr/include/bits/types/sig_atomic_t.h:
|
||||
/usr/include/bits/types/siginfo_t.h:
|
||||
/usr/include/bits/types/__sigval_t.h:
|
||||
/usr/include/bits/siginfo-arch.h:
|
||||
/usr/include/bits/siginfo-consts.h:
|
||||
/usr/include/bits/types/sigval_t.h:
|
||||
/usr/include/bits/types/sigevent_t.h:
|
||||
/usr/include/bits/sigevent-consts.h:
|
||||
/usr/include/bits/sigaction.h:
|
||||
/usr/include/bits/sigcontext.h:
|
||||
/usr/include/bits/types/stack_t.h:
|
||||
/usr/include/sys/ucontext.h:
|
||||
/usr/include/bits/sigstack.h:
|
||||
/usr/include/bits/sigstksz.h:
|
||||
/usr/include/bits/ss_flags.h:
|
||||
/usr/include/bits/types/struct_sigstack.h:
|
||||
/usr/include/bits/sigthread.h:
|
||||
/usr/include/bits/signal_ext.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h:
|
||||
emacs-module.h:
|
||||
150
lisp/zmq/src/.deps/emacs_zmq_la-core.Plo
Normal file
150
lisp/zmq/src/.deps/emacs_zmq_la-core.Plo
Normal file
@@ -0,0 +1,150 @@
|
||||
emacs_zmq_la-core.lo: core.c /usr/include/stdc-predef.h core.h \
|
||||
/usr/include/stdlib.h /usr/include/bits/libc-header-start.h \
|
||||
/usr/include/features.h /usr/include/features-time64.h \
|
||||
/usr/include/bits/wordsize.h /usr/include/bits/timesize.h \
|
||||
/usr/include/sys/cdefs.h /usr/include/bits/long-double.h \
|
||||
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h \
|
||||
/usr/include/bits/waitflags.h /usr/include/bits/waitstatus.h \
|
||||
/usr/include/bits/floatn.h /usr/include/bits/floatn-common.h \
|
||||
/usr/include/sys/types.h /usr/include/bits/types.h \
|
||||
/usr/include/bits/typesizes.h /usr/include/bits/time64.h \
|
||||
/usr/include/bits/types/clock_t.h /usr/include/bits/types/clockid_t.h \
|
||||
/usr/include/bits/types/time_t.h /usr/include/bits/types/timer_t.h \
|
||||
/usr/include/bits/stdint-intn.h /usr/include/endian.h \
|
||||
/usr/include/bits/endian.h /usr/include/bits/endianness.h \
|
||||
/usr/include/bits/byteswap.h /usr/include/bits/uintn-identity.h \
|
||||
/usr/include/sys/select.h /usr/include/bits/select.h \
|
||||
/usr/include/bits/types/sigset_t.h /usr/include/bits/types/__sigset_t.h \
|
||||
/usr/include/bits/types/struct_timeval.h \
|
||||
/usr/include/bits/types/struct_timespec.h \
|
||||
/usr/include/bits/pthreadtypes.h /usr/include/bits/thread-shared-types.h \
|
||||
/usr/include/bits/pthreadtypes-arch.h \
|
||||
/usr/include/bits/atomic_wide_counter.h /usr/include/bits/struct_mutex.h \
|
||||
/usr/include/bits/struct_rwlock.h /usr/include/alloca.h \
|
||||
/usr/include/bits/stdlib-bsearch.h /usr/include/bits/stdlib-float.h \
|
||||
/usr/include/string.h /usr/include/bits/types/locale_t.h \
|
||||
/usr/include/bits/types/__locale_t.h /usr/include/strings.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h \
|
||||
/usr/include/zmq.h /usr/include/errno.h /usr/include/bits/errno.h \
|
||||
/usr/include/linux/errno.h /usr/include/asm/errno.h \
|
||||
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h \
|
||||
/usr/include/stdio.h /usr/include/bits/types/__fpos_t.h \
|
||||
/usr/include/bits/types/__mbstate_t.h \
|
||||
/usr/include/bits/types/__fpos64_t.h /usr/include/bits/types/__FILE.h \
|
||||
/usr/include/bits/types/FILE.h /usr/include/bits/types/struct_FILE.h \
|
||||
/usr/include/bits/types/cookie_io_functions_t.h \
|
||||
/usr/include/bits/stdio_lim.h /usr/include/bits/stdio.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h \
|
||||
/usr/include/stdint.h /usr/include/bits/wchar.h \
|
||||
/usr/include/bits/stdint-uintn.h /usr/include/bits/stdint-least.h \
|
||||
/usr/include/signal.h /usr/include/bits/signum-generic.h \
|
||||
/usr/include/bits/signum-arch.h /usr/include/bits/types/sig_atomic_t.h \
|
||||
/usr/include/bits/types/siginfo_t.h /usr/include/bits/types/__sigval_t.h \
|
||||
/usr/include/bits/siginfo-arch.h /usr/include/bits/siginfo-consts.h \
|
||||
/usr/include/bits/types/sigval_t.h /usr/include/bits/types/sigevent_t.h \
|
||||
/usr/include/bits/sigevent-consts.h /usr/include/bits/sigaction.h \
|
||||
/usr/include/bits/sigcontext.h /usr/include/bits/types/stack_t.h \
|
||||
/usr/include/sys/ucontext.h /usr/include/bits/sigstack.h \
|
||||
/usr/include/bits/sigstksz.h /usr/include/bits/ss_flags.h \
|
||||
/usr/include/bits/types/struct_sigstack.h /usr/include/bits/sigthread.h \
|
||||
/usr/include/bits/signal_ext.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h emacs-module.h \
|
||||
/usr/include/assert.h
|
||||
/usr/include/stdc-predef.h:
|
||||
core.h:
|
||||
/usr/include/stdlib.h:
|
||||
/usr/include/bits/libc-header-start.h:
|
||||
/usr/include/features.h:
|
||||
/usr/include/features-time64.h:
|
||||
/usr/include/bits/wordsize.h:
|
||||
/usr/include/bits/timesize.h:
|
||||
/usr/include/sys/cdefs.h:
|
||||
/usr/include/bits/long-double.h:
|
||||
/usr/include/gnu/stubs.h:
|
||||
/usr/include/gnu/stubs-64.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h:
|
||||
/usr/include/bits/waitflags.h:
|
||||
/usr/include/bits/waitstatus.h:
|
||||
/usr/include/bits/floatn.h:
|
||||
/usr/include/bits/floatn-common.h:
|
||||
/usr/include/sys/types.h:
|
||||
/usr/include/bits/types.h:
|
||||
/usr/include/bits/typesizes.h:
|
||||
/usr/include/bits/time64.h:
|
||||
/usr/include/bits/types/clock_t.h:
|
||||
/usr/include/bits/types/clockid_t.h:
|
||||
/usr/include/bits/types/time_t.h:
|
||||
/usr/include/bits/types/timer_t.h:
|
||||
/usr/include/bits/stdint-intn.h:
|
||||
/usr/include/endian.h:
|
||||
/usr/include/bits/endian.h:
|
||||
/usr/include/bits/endianness.h:
|
||||
/usr/include/bits/byteswap.h:
|
||||
/usr/include/bits/uintn-identity.h:
|
||||
/usr/include/sys/select.h:
|
||||
/usr/include/bits/select.h:
|
||||
/usr/include/bits/types/sigset_t.h:
|
||||
/usr/include/bits/types/__sigset_t.h:
|
||||
/usr/include/bits/types/struct_timeval.h:
|
||||
/usr/include/bits/types/struct_timespec.h:
|
||||
/usr/include/bits/pthreadtypes.h:
|
||||
/usr/include/bits/thread-shared-types.h:
|
||||
/usr/include/bits/pthreadtypes-arch.h:
|
||||
/usr/include/bits/atomic_wide_counter.h:
|
||||
/usr/include/bits/struct_mutex.h:
|
||||
/usr/include/bits/struct_rwlock.h:
|
||||
/usr/include/alloca.h:
|
||||
/usr/include/bits/stdlib-bsearch.h:
|
||||
/usr/include/bits/stdlib-float.h:
|
||||
/usr/include/string.h:
|
||||
/usr/include/bits/types/locale_t.h:
|
||||
/usr/include/bits/types/__locale_t.h:
|
||||
/usr/include/strings.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h:
|
||||
/usr/include/zmq.h:
|
||||
/usr/include/errno.h:
|
||||
/usr/include/bits/errno.h:
|
||||
/usr/include/linux/errno.h:
|
||||
/usr/include/asm/errno.h:
|
||||
/usr/include/asm-generic/errno.h:
|
||||
/usr/include/asm-generic/errno-base.h:
|
||||
/usr/include/stdio.h:
|
||||
/usr/include/bits/types/__fpos_t.h:
|
||||
/usr/include/bits/types/__mbstate_t.h:
|
||||
/usr/include/bits/types/__fpos64_t.h:
|
||||
/usr/include/bits/types/__FILE.h:
|
||||
/usr/include/bits/types/FILE.h:
|
||||
/usr/include/bits/types/struct_FILE.h:
|
||||
/usr/include/bits/types/cookie_io_functions_t.h:
|
||||
/usr/include/bits/stdio_lim.h:
|
||||
/usr/include/bits/stdio.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h:
|
||||
/usr/include/stdint.h:
|
||||
/usr/include/bits/wchar.h:
|
||||
/usr/include/bits/stdint-uintn.h:
|
||||
/usr/include/bits/stdint-least.h:
|
||||
/usr/include/signal.h:
|
||||
/usr/include/bits/signum-generic.h:
|
||||
/usr/include/bits/signum-arch.h:
|
||||
/usr/include/bits/types/sig_atomic_t.h:
|
||||
/usr/include/bits/types/siginfo_t.h:
|
||||
/usr/include/bits/types/__sigval_t.h:
|
||||
/usr/include/bits/siginfo-arch.h:
|
||||
/usr/include/bits/siginfo-consts.h:
|
||||
/usr/include/bits/types/sigval_t.h:
|
||||
/usr/include/bits/types/sigevent_t.h:
|
||||
/usr/include/bits/sigevent-consts.h:
|
||||
/usr/include/bits/sigaction.h:
|
||||
/usr/include/bits/sigcontext.h:
|
||||
/usr/include/bits/types/stack_t.h:
|
||||
/usr/include/sys/ucontext.h:
|
||||
/usr/include/bits/sigstack.h:
|
||||
/usr/include/bits/sigstksz.h:
|
||||
/usr/include/bits/ss_flags.h:
|
||||
/usr/include/bits/types/struct_sigstack.h:
|
||||
/usr/include/bits/sigthread.h:
|
||||
/usr/include/bits/signal_ext.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h:
|
||||
emacs-module.h:
|
||||
/usr/include/assert.h:
|
||||
157
lisp/zmq/src/.deps/emacs_zmq_la-emacs-zmq.Plo
Normal file
157
lisp/zmq/src/.deps/emacs_zmq_la-emacs-zmq.Plo
Normal file
@@ -0,0 +1,157 @@
|
||||
emacs_zmq_la-emacs-zmq.lo: emacs-zmq.c /usr/include/stdc-predef.h \
|
||||
emacs-zmq.h core.h /usr/include/stdlib.h \
|
||||
/usr/include/bits/libc-header-start.h /usr/include/features.h \
|
||||
/usr/include/features-time64.h /usr/include/bits/wordsize.h \
|
||||
/usr/include/bits/timesize.h /usr/include/sys/cdefs.h \
|
||||
/usr/include/bits/long-double.h /usr/include/gnu/stubs.h \
|
||||
/usr/include/gnu/stubs-64.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h \
|
||||
/usr/include/bits/waitflags.h /usr/include/bits/waitstatus.h \
|
||||
/usr/include/bits/floatn.h /usr/include/bits/floatn-common.h \
|
||||
/usr/include/sys/types.h /usr/include/bits/types.h \
|
||||
/usr/include/bits/typesizes.h /usr/include/bits/time64.h \
|
||||
/usr/include/bits/types/clock_t.h /usr/include/bits/types/clockid_t.h \
|
||||
/usr/include/bits/types/time_t.h /usr/include/bits/types/timer_t.h \
|
||||
/usr/include/bits/stdint-intn.h /usr/include/endian.h \
|
||||
/usr/include/bits/endian.h /usr/include/bits/endianness.h \
|
||||
/usr/include/bits/byteswap.h /usr/include/bits/uintn-identity.h \
|
||||
/usr/include/sys/select.h /usr/include/bits/select.h \
|
||||
/usr/include/bits/types/sigset_t.h /usr/include/bits/types/__sigset_t.h \
|
||||
/usr/include/bits/types/struct_timeval.h \
|
||||
/usr/include/bits/types/struct_timespec.h \
|
||||
/usr/include/bits/pthreadtypes.h /usr/include/bits/thread-shared-types.h \
|
||||
/usr/include/bits/pthreadtypes-arch.h \
|
||||
/usr/include/bits/atomic_wide_counter.h /usr/include/bits/struct_mutex.h \
|
||||
/usr/include/bits/struct_rwlock.h /usr/include/alloca.h \
|
||||
/usr/include/bits/stdlib-bsearch.h /usr/include/bits/stdlib-float.h \
|
||||
/usr/include/string.h /usr/include/bits/types/locale_t.h \
|
||||
/usr/include/bits/types/__locale_t.h /usr/include/strings.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h \
|
||||
/usr/include/zmq.h /usr/include/errno.h /usr/include/bits/errno.h \
|
||||
/usr/include/linux/errno.h /usr/include/asm/errno.h \
|
||||
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h \
|
||||
/usr/include/stdio.h /usr/include/bits/types/__fpos_t.h \
|
||||
/usr/include/bits/types/__mbstate_t.h \
|
||||
/usr/include/bits/types/__fpos64_t.h /usr/include/bits/types/__FILE.h \
|
||||
/usr/include/bits/types/FILE.h /usr/include/bits/types/struct_FILE.h \
|
||||
/usr/include/bits/types/cookie_io_functions_t.h \
|
||||
/usr/include/bits/stdio_lim.h /usr/include/bits/stdio.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h \
|
||||
/usr/include/stdint.h /usr/include/bits/wchar.h \
|
||||
/usr/include/bits/stdint-uintn.h /usr/include/bits/stdint-least.h \
|
||||
/usr/include/signal.h /usr/include/bits/signum-generic.h \
|
||||
/usr/include/bits/signum-arch.h /usr/include/bits/types/sig_atomic_t.h \
|
||||
/usr/include/bits/types/siginfo_t.h /usr/include/bits/types/__sigval_t.h \
|
||||
/usr/include/bits/siginfo-arch.h /usr/include/bits/siginfo-consts.h \
|
||||
/usr/include/bits/types/sigval_t.h /usr/include/bits/types/sigevent_t.h \
|
||||
/usr/include/bits/sigevent-consts.h /usr/include/bits/sigaction.h \
|
||||
/usr/include/bits/sigcontext.h /usr/include/bits/types/stack_t.h \
|
||||
/usr/include/sys/ucontext.h /usr/include/bits/sigstack.h \
|
||||
/usr/include/bits/sigstksz.h /usr/include/bits/ss_flags.h \
|
||||
/usr/include/bits/types/struct_sigstack.h /usr/include/bits/sigthread.h \
|
||||
/usr/include/bits/signal_ext.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h emacs-module.h \
|
||||
util.h msg.h socket.h context.h poll.h /usr/include/assert.h
|
||||
/usr/include/stdc-predef.h:
|
||||
emacs-zmq.h:
|
||||
core.h:
|
||||
/usr/include/stdlib.h:
|
||||
/usr/include/bits/libc-header-start.h:
|
||||
/usr/include/features.h:
|
||||
/usr/include/features-time64.h:
|
||||
/usr/include/bits/wordsize.h:
|
||||
/usr/include/bits/timesize.h:
|
||||
/usr/include/sys/cdefs.h:
|
||||
/usr/include/bits/long-double.h:
|
||||
/usr/include/gnu/stubs.h:
|
||||
/usr/include/gnu/stubs-64.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h:
|
||||
/usr/include/bits/waitflags.h:
|
||||
/usr/include/bits/waitstatus.h:
|
||||
/usr/include/bits/floatn.h:
|
||||
/usr/include/bits/floatn-common.h:
|
||||
/usr/include/sys/types.h:
|
||||
/usr/include/bits/types.h:
|
||||
/usr/include/bits/typesizes.h:
|
||||
/usr/include/bits/time64.h:
|
||||
/usr/include/bits/types/clock_t.h:
|
||||
/usr/include/bits/types/clockid_t.h:
|
||||
/usr/include/bits/types/time_t.h:
|
||||
/usr/include/bits/types/timer_t.h:
|
||||
/usr/include/bits/stdint-intn.h:
|
||||
/usr/include/endian.h:
|
||||
/usr/include/bits/endian.h:
|
||||
/usr/include/bits/endianness.h:
|
||||
/usr/include/bits/byteswap.h:
|
||||
/usr/include/bits/uintn-identity.h:
|
||||
/usr/include/sys/select.h:
|
||||
/usr/include/bits/select.h:
|
||||
/usr/include/bits/types/sigset_t.h:
|
||||
/usr/include/bits/types/__sigset_t.h:
|
||||
/usr/include/bits/types/struct_timeval.h:
|
||||
/usr/include/bits/types/struct_timespec.h:
|
||||
/usr/include/bits/pthreadtypes.h:
|
||||
/usr/include/bits/thread-shared-types.h:
|
||||
/usr/include/bits/pthreadtypes-arch.h:
|
||||
/usr/include/bits/atomic_wide_counter.h:
|
||||
/usr/include/bits/struct_mutex.h:
|
||||
/usr/include/bits/struct_rwlock.h:
|
||||
/usr/include/alloca.h:
|
||||
/usr/include/bits/stdlib-bsearch.h:
|
||||
/usr/include/bits/stdlib-float.h:
|
||||
/usr/include/string.h:
|
||||
/usr/include/bits/types/locale_t.h:
|
||||
/usr/include/bits/types/__locale_t.h:
|
||||
/usr/include/strings.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h:
|
||||
/usr/include/zmq.h:
|
||||
/usr/include/errno.h:
|
||||
/usr/include/bits/errno.h:
|
||||
/usr/include/linux/errno.h:
|
||||
/usr/include/asm/errno.h:
|
||||
/usr/include/asm-generic/errno.h:
|
||||
/usr/include/asm-generic/errno-base.h:
|
||||
/usr/include/stdio.h:
|
||||
/usr/include/bits/types/__fpos_t.h:
|
||||
/usr/include/bits/types/__mbstate_t.h:
|
||||
/usr/include/bits/types/__fpos64_t.h:
|
||||
/usr/include/bits/types/__FILE.h:
|
||||
/usr/include/bits/types/FILE.h:
|
||||
/usr/include/bits/types/struct_FILE.h:
|
||||
/usr/include/bits/types/cookie_io_functions_t.h:
|
||||
/usr/include/bits/stdio_lim.h:
|
||||
/usr/include/bits/stdio.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h:
|
||||
/usr/include/stdint.h:
|
||||
/usr/include/bits/wchar.h:
|
||||
/usr/include/bits/stdint-uintn.h:
|
||||
/usr/include/bits/stdint-least.h:
|
||||
/usr/include/signal.h:
|
||||
/usr/include/bits/signum-generic.h:
|
||||
/usr/include/bits/signum-arch.h:
|
||||
/usr/include/bits/types/sig_atomic_t.h:
|
||||
/usr/include/bits/types/siginfo_t.h:
|
||||
/usr/include/bits/types/__sigval_t.h:
|
||||
/usr/include/bits/siginfo-arch.h:
|
||||
/usr/include/bits/siginfo-consts.h:
|
||||
/usr/include/bits/types/sigval_t.h:
|
||||
/usr/include/bits/types/sigevent_t.h:
|
||||
/usr/include/bits/sigevent-consts.h:
|
||||
/usr/include/bits/sigaction.h:
|
||||
/usr/include/bits/sigcontext.h:
|
||||
/usr/include/bits/types/stack_t.h:
|
||||
/usr/include/sys/ucontext.h:
|
||||
/usr/include/bits/sigstack.h:
|
||||
/usr/include/bits/sigstksz.h:
|
||||
/usr/include/bits/ss_flags.h:
|
||||
/usr/include/bits/types/struct_sigstack.h:
|
||||
/usr/include/bits/sigthread.h:
|
||||
/usr/include/bits/signal_ext.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h:
|
||||
emacs-module.h:
|
||||
util.h:
|
||||
msg.h:
|
||||
socket.h:
|
||||
context.h:
|
||||
poll.h:
|
||||
/usr/include/assert.h:
|
||||
149
lisp/zmq/src/.deps/emacs_zmq_la-msg.Plo
Normal file
149
lisp/zmq/src/.deps/emacs_zmq_la-msg.Plo
Normal file
@@ -0,0 +1,149 @@
|
||||
emacs_zmq_la-msg.lo: msg.c /usr/include/stdc-predef.h msg.h core.h \
|
||||
/usr/include/stdlib.h /usr/include/bits/libc-header-start.h \
|
||||
/usr/include/features.h /usr/include/features-time64.h \
|
||||
/usr/include/bits/wordsize.h /usr/include/bits/timesize.h \
|
||||
/usr/include/sys/cdefs.h /usr/include/bits/long-double.h \
|
||||
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h \
|
||||
/usr/include/bits/waitflags.h /usr/include/bits/waitstatus.h \
|
||||
/usr/include/bits/floatn.h /usr/include/bits/floatn-common.h \
|
||||
/usr/include/sys/types.h /usr/include/bits/types.h \
|
||||
/usr/include/bits/typesizes.h /usr/include/bits/time64.h \
|
||||
/usr/include/bits/types/clock_t.h /usr/include/bits/types/clockid_t.h \
|
||||
/usr/include/bits/types/time_t.h /usr/include/bits/types/timer_t.h \
|
||||
/usr/include/bits/stdint-intn.h /usr/include/endian.h \
|
||||
/usr/include/bits/endian.h /usr/include/bits/endianness.h \
|
||||
/usr/include/bits/byteswap.h /usr/include/bits/uintn-identity.h \
|
||||
/usr/include/sys/select.h /usr/include/bits/select.h \
|
||||
/usr/include/bits/types/sigset_t.h /usr/include/bits/types/__sigset_t.h \
|
||||
/usr/include/bits/types/struct_timeval.h \
|
||||
/usr/include/bits/types/struct_timespec.h \
|
||||
/usr/include/bits/pthreadtypes.h /usr/include/bits/thread-shared-types.h \
|
||||
/usr/include/bits/pthreadtypes-arch.h \
|
||||
/usr/include/bits/atomic_wide_counter.h /usr/include/bits/struct_mutex.h \
|
||||
/usr/include/bits/struct_rwlock.h /usr/include/alloca.h \
|
||||
/usr/include/bits/stdlib-bsearch.h /usr/include/bits/stdlib-float.h \
|
||||
/usr/include/string.h /usr/include/bits/types/locale_t.h \
|
||||
/usr/include/bits/types/__locale_t.h /usr/include/strings.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h \
|
||||
/usr/include/zmq.h /usr/include/errno.h /usr/include/bits/errno.h \
|
||||
/usr/include/linux/errno.h /usr/include/asm/errno.h \
|
||||
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h \
|
||||
/usr/include/stdio.h /usr/include/bits/types/__fpos_t.h \
|
||||
/usr/include/bits/types/__mbstate_t.h \
|
||||
/usr/include/bits/types/__fpos64_t.h /usr/include/bits/types/__FILE.h \
|
||||
/usr/include/bits/types/FILE.h /usr/include/bits/types/struct_FILE.h \
|
||||
/usr/include/bits/types/cookie_io_functions_t.h \
|
||||
/usr/include/bits/stdio_lim.h /usr/include/bits/stdio.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h \
|
||||
/usr/include/stdint.h /usr/include/bits/wchar.h \
|
||||
/usr/include/bits/stdint-uintn.h /usr/include/bits/stdint-least.h \
|
||||
/usr/include/signal.h /usr/include/bits/signum-generic.h \
|
||||
/usr/include/bits/signum-arch.h /usr/include/bits/types/sig_atomic_t.h \
|
||||
/usr/include/bits/types/siginfo_t.h /usr/include/bits/types/__sigval_t.h \
|
||||
/usr/include/bits/siginfo-arch.h /usr/include/bits/siginfo-consts.h \
|
||||
/usr/include/bits/types/sigval_t.h /usr/include/bits/types/sigevent_t.h \
|
||||
/usr/include/bits/sigevent-consts.h /usr/include/bits/sigaction.h \
|
||||
/usr/include/bits/sigcontext.h /usr/include/bits/types/stack_t.h \
|
||||
/usr/include/sys/ucontext.h /usr/include/bits/sigstack.h \
|
||||
/usr/include/bits/sigstksz.h /usr/include/bits/ss_flags.h \
|
||||
/usr/include/bits/types/struct_sigstack.h /usr/include/bits/sigthread.h \
|
||||
/usr/include/bits/signal_ext.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h emacs-module.h
|
||||
/usr/include/stdc-predef.h:
|
||||
msg.h:
|
||||
core.h:
|
||||
/usr/include/stdlib.h:
|
||||
/usr/include/bits/libc-header-start.h:
|
||||
/usr/include/features.h:
|
||||
/usr/include/features-time64.h:
|
||||
/usr/include/bits/wordsize.h:
|
||||
/usr/include/bits/timesize.h:
|
||||
/usr/include/sys/cdefs.h:
|
||||
/usr/include/bits/long-double.h:
|
||||
/usr/include/gnu/stubs.h:
|
||||
/usr/include/gnu/stubs-64.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h:
|
||||
/usr/include/bits/waitflags.h:
|
||||
/usr/include/bits/waitstatus.h:
|
||||
/usr/include/bits/floatn.h:
|
||||
/usr/include/bits/floatn-common.h:
|
||||
/usr/include/sys/types.h:
|
||||
/usr/include/bits/types.h:
|
||||
/usr/include/bits/typesizes.h:
|
||||
/usr/include/bits/time64.h:
|
||||
/usr/include/bits/types/clock_t.h:
|
||||
/usr/include/bits/types/clockid_t.h:
|
||||
/usr/include/bits/types/time_t.h:
|
||||
/usr/include/bits/types/timer_t.h:
|
||||
/usr/include/bits/stdint-intn.h:
|
||||
/usr/include/endian.h:
|
||||
/usr/include/bits/endian.h:
|
||||
/usr/include/bits/endianness.h:
|
||||
/usr/include/bits/byteswap.h:
|
||||
/usr/include/bits/uintn-identity.h:
|
||||
/usr/include/sys/select.h:
|
||||
/usr/include/bits/select.h:
|
||||
/usr/include/bits/types/sigset_t.h:
|
||||
/usr/include/bits/types/__sigset_t.h:
|
||||
/usr/include/bits/types/struct_timeval.h:
|
||||
/usr/include/bits/types/struct_timespec.h:
|
||||
/usr/include/bits/pthreadtypes.h:
|
||||
/usr/include/bits/thread-shared-types.h:
|
||||
/usr/include/bits/pthreadtypes-arch.h:
|
||||
/usr/include/bits/atomic_wide_counter.h:
|
||||
/usr/include/bits/struct_mutex.h:
|
||||
/usr/include/bits/struct_rwlock.h:
|
||||
/usr/include/alloca.h:
|
||||
/usr/include/bits/stdlib-bsearch.h:
|
||||
/usr/include/bits/stdlib-float.h:
|
||||
/usr/include/string.h:
|
||||
/usr/include/bits/types/locale_t.h:
|
||||
/usr/include/bits/types/__locale_t.h:
|
||||
/usr/include/strings.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h:
|
||||
/usr/include/zmq.h:
|
||||
/usr/include/errno.h:
|
||||
/usr/include/bits/errno.h:
|
||||
/usr/include/linux/errno.h:
|
||||
/usr/include/asm/errno.h:
|
||||
/usr/include/asm-generic/errno.h:
|
||||
/usr/include/asm-generic/errno-base.h:
|
||||
/usr/include/stdio.h:
|
||||
/usr/include/bits/types/__fpos_t.h:
|
||||
/usr/include/bits/types/__mbstate_t.h:
|
||||
/usr/include/bits/types/__fpos64_t.h:
|
||||
/usr/include/bits/types/__FILE.h:
|
||||
/usr/include/bits/types/FILE.h:
|
||||
/usr/include/bits/types/struct_FILE.h:
|
||||
/usr/include/bits/types/cookie_io_functions_t.h:
|
||||
/usr/include/bits/stdio_lim.h:
|
||||
/usr/include/bits/stdio.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h:
|
||||
/usr/include/stdint.h:
|
||||
/usr/include/bits/wchar.h:
|
||||
/usr/include/bits/stdint-uintn.h:
|
||||
/usr/include/bits/stdint-least.h:
|
||||
/usr/include/signal.h:
|
||||
/usr/include/bits/signum-generic.h:
|
||||
/usr/include/bits/signum-arch.h:
|
||||
/usr/include/bits/types/sig_atomic_t.h:
|
||||
/usr/include/bits/types/siginfo_t.h:
|
||||
/usr/include/bits/types/__sigval_t.h:
|
||||
/usr/include/bits/siginfo-arch.h:
|
||||
/usr/include/bits/siginfo-consts.h:
|
||||
/usr/include/bits/types/sigval_t.h:
|
||||
/usr/include/bits/types/sigevent_t.h:
|
||||
/usr/include/bits/sigevent-consts.h:
|
||||
/usr/include/bits/sigaction.h:
|
||||
/usr/include/bits/sigcontext.h:
|
||||
/usr/include/bits/types/stack_t.h:
|
||||
/usr/include/sys/ucontext.h:
|
||||
/usr/include/bits/sigstack.h:
|
||||
/usr/include/bits/sigstksz.h:
|
||||
/usr/include/bits/ss_flags.h:
|
||||
/usr/include/bits/types/struct_sigstack.h:
|
||||
/usr/include/bits/sigthread.h:
|
||||
/usr/include/bits/signal_ext.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h:
|
||||
emacs-module.h:
|
||||
149
lisp/zmq/src/.deps/emacs_zmq_la-poll.Plo
Normal file
149
lisp/zmq/src/.deps/emacs_zmq_la-poll.Plo
Normal file
@@ -0,0 +1,149 @@
|
||||
emacs_zmq_la-poll.lo: poll.c /usr/include/stdc-predef.h poll.h core.h \
|
||||
/usr/include/stdlib.h /usr/include/bits/libc-header-start.h \
|
||||
/usr/include/features.h /usr/include/features-time64.h \
|
||||
/usr/include/bits/wordsize.h /usr/include/bits/timesize.h \
|
||||
/usr/include/sys/cdefs.h /usr/include/bits/long-double.h \
|
||||
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h \
|
||||
/usr/include/bits/waitflags.h /usr/include/bits/waitstatus.h \
|
||||
/usr/include/bits/floatn.h /usr/include/bits/floatn-common.h \
|
||||
/usr/include/sys/types.h /usr/include/bits/types.h \
|
||||
/usr/include/bits/typesizes.h /usr/include/bits/time64.h \
|
||||
/usr/include/bits/types/clock_t.h /usr/include/bits/types/clockid_t.h \
|
||||
/usr/include/bits/types/time_t.h /usr/include/bits/types/timer_t.h \
|
||||
/usr/include/bits/stdint-intn.h /usr/include/endian.h \
|
||||
/usr/include/bits/endian.h /usr/include/bits/endianness.h \
|
||||
/usr/include/bits/byteswap.h /usr/include/bits/uintn-identity.h \
|
||||
/usr/include/sys/select.h /usr/include/bits/select.h \
|
||||
/usr/include/bits/types/sigset_t.h /usr/include/bits/types/__sigset_t.h \
|
||||
/usr/include/bits/types/struct_timeval.h \
|
||||
/usr/include/bits/types/struct_timespec.h \
|
||||
/usr/include/bits/pthreadtypes.h /usr/include/bits/thread-shared-types.h \
|
||||
/usr/include/bits/pthreadtypes-arch.h \
|
||||
/usr/include/bits/atomic_wide_counter.h /usr/include/bits/struct_mutex.h \
|
||||
/usr/include/bits/struct_rwlock.h /usr/include/alloca.h \
|
||||
/usr/include/bits/stdlib-bsearch.h /usr/include/bits/stdlib-float.h \
|
||||
/usr/include/string.h /usr/include/bits/types/locale_t.h \
|
||||
/usr/include/bits/types/__locale_t.h /usr/include/strings.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h \
|
||||
/usr/include/zmq.h /usr/include/errno.h /usr/include/bits/errno.h \
|
||||
/usr/include/linux/errno.h /usr/include/asm/errno.h \
|
||||
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h \
|
||||
/usr/include/stdio.h /usr/include/bits/types/__fpos_t.h \
|
||||
/usr/include/bits/types/__mbstate_t.h \
|
||||
/usr/include/bits/types/__fpos64_t.h /usr/include/bits/types/__FILE.h \
|
||||
/usr/include/bits/types/FILE.h /usr/include/bits/types/struct_FILE.h \
|
||||
/usr/include/bits/types/cookie_io_functions_t.h \
|
||||
/usr/include/bits/stdio_lim.h /usr/include/bits/stdio.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h \
|
||||
/usr/include/stdint.h /usr/include/bits/wchar.h \
|
||||
/usr/include/bits/stdint-uintn.h /usr/include/bits/stdint-least.h \
|
||||
/usr/include/signal.h /usr/include/bits/signum-generic.h \
|
||||
/usr/include/bits/signum-arch.h /usr/include/bits/types/sig_atomic_t.h \
|
||||
/usr/include/bits/types/siginfo_t.h /usr/include/bits/types/__sigval_t.h \
|
||||
/usr/include/bits/siginfo-arch.h /usr/include/bits/siginfo-consts.h \
|
||||
/usr/include/bits/types/sigval_t.h /usr/include/bits/types/sigevent_t.h \
|
||||
/usr/include/bits/sigevent-consts.h /usr/include/bits/sigaction.h \
|
||||
/usr/include/bits/sigcontext.h /usr/include/bits/types/stack_t.h \
|
||||
/usr/include/sys/ucontext.h /usr/include/bits/sigstack.h \
|
||||
/usr/include/bits/sigstksz.h /usr/include/bits/ss_flags.h \
|
||||
/usr/include/bits/types/struct_sigstack.h /usr/include/bits/sigthread.h \
|
||||
/usr/include/bits/signal_ext.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h emacs-module.h
|
||||
/usr/include/stdc-predef.h:
|
||||
poll.h:
|
||||
core.h:
|
||||
/usr/include/stdlib.h:
|
||||
/usr/include/bits/libc-header-start.h:
|
||||
/usr/include/features.h:
|
||||
/usr/include/features-time64.h:
|
||||
/usr/include/bits/wordsize.h:
|
||||
/usr/include/bits/timesize.h:
|
||||
/usr/include/sys/cdefs.h:
|
||||
/usr/include/bits/long-double.h:
|
||||
/usr/include/gnu/stubs.h:
|
||||
/usr/include/gnu/stubs-64.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h:
|
||||
/usr/include/bits/waitflags.h:
|
||||
/usr/include/bits/waitstatus.h:
|
||||
/usr/include/bits/floatn.h:
|
||||
/usr/include/bits/floatn-common.h:
|
||||
/usr/include/sys/types.h:
|
||||
/usr/include/bits/types.h:
|
||||
/usr/include/bits/typesizes.h:
|
||||
/usr/include/bits/time64.h:
|
||||
/usr/include/bits/types/clock_t.h:
|
||||
/usr/include/bits/types/clockid_t.h:
|
||||
/usr/include/bits/types/time_t.h:
|
||||
/usr/include/bits/types/timer_t.h:
|
||||
/usr/include/bits/stdint-intn.h:
|
||||
/usr/include/endian.h:
|
||||
/usr/include/bits/endian.h:
|
||||
/usr/include/bits/endianness.h:
|
||||
/usr/include/bits/byteswap.h:
|
||||
/usr/include/bits/uintn-identity.h:
|
||||
/usr/include/sys/select.h:
|
||||
/usr/include/bits/select.h:
|
||||
/usr/include/bits/types/sigset_t.h:
|
||||
/usr/include/bits/types/__sigset_t.h:
|
||||
/usr/include/bits/types/struct_timeval.h:
|
||||
/usr/include/bits/types/struct_timespec.h:
|
||||
/usr/include/bits/pthreadtypes.h:
|
||||
/usr/include/bits/thread-shared-types.h:
|
||||
/usr/include/bits/pthreadtypes-arch.h:
|
||||
/usr/include/bits/atomic_wide_counter.h:
|
||||
/usr/include/bits/struct_mutex.h:
|
||||
/usr/include/bits/struct_rwlock.h:
|
||||
/usr/include/alloca.h:
|
||||
/usr/include/bits/stdlib-bsearch.h:
|
||||
/usr/include/bits/stdlib-float.h:
|
||||
/usr/include/string.h:
|
||||
/usr/include/bits/types/locale_t.h:
|
||||
/usr/include/bits/types/__locale_t.h:
|
||||
/usr/include/strings.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h:
|
||||
/usr/include/zmq.h:
|
||||
/usr/include/errno.h:
|
||||
/usr/include/bits/errno.h:
|
||||
/usr/include/linux/errno.h:
|
||||
/usr/include/asm/errno.h:
|
||||
/usr/include/asm-generic/errno.h:
|
||||
/usr/include/asm-generic/errno-base.h:
|
||||
/usr/include/stdio.h:
|
||||
/usr/include/bits/types/__fpos_t.h:
|
||||
/usr/include/bits/types/__mbstate_t.h:
|
||||
/usr/include/bits/types/__fpos64_t.h:
|
||||
/usr/include/bits/types/__FILE.h:
|
||||
/usr/include/bits/types/FILE.h:
|
||||
/usr/include/bits/types/struct_FILE.h:
|
||||
/usr/include/bits/types/cookie_io_functions_t.h:
|
||||
/usr/include/bits/stdio_lim.h:
|
||||
/usr/include/bits/stdio.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h:
|
||||
/usr/include/stdint.h:
|
||||
/usr/include/bits/wchar.h:
|
||||
/usr/include/bits/stdint-uintn.h:
|
||||
/usr/include/bits/stdint-least.h:
|
||||
/usr/include/signal.h:
|
||||
/usr/include/bits/signum-generic.h:
|
||||
/usr/include/bits/signum-arch.h:
|
||||
/usr/include/bits/types/sig_atomic_t.h:
|
||||
/usr/include/bits/types/siginfo_t.h:
|
||||
/usr/include/bits/types/__sigval_t.h:
|
||||
/usr/include/bits/siginfo-arch.h:
|
||||
/usr/include/bits/siginfo-consts.h:
|
||||
/usr/include/bits/types/sigval_t.h:
|
||||
/usr/include/bits/types/sigevent_t.h:
|
||||
/usr/include/bits/sigevent-consts.h:
|
||||
/usr/include/bits/sigaction.h:
|
||||
/usr/include/bits/sigcontext.h:
|
||||
/usr/include/bits/types/stack_t.h:
|
||||
/usr/include/sys/ucontext.h:
|
||||
/usr/include/bits/sigstack.h:
|
||||
/usr/include/bits/sigstksz.h:
|
||||
/usr/include/bits/ss_flags.h:
|
||||
/usr/include/bits/types/struct_sigstack.h:
|
||||
/usr/include/bits/sigthread.h:
|
||||
/usr/include/bits/signal_ext.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h:
|
||||
emacs-module.h:
|
||||
151
lisp/zmq/src/.deps/emacs_zmq_la-socket.Plo
Normal file
151
lisp/zmq/src/.deps/emacs_zmq_la-socket.Plo
Normal file
@@ -0,0 +1,151 @@
|
||||
emacs_zmq_la-socket.lo: socket.c /usr/include/stdc-predef.h socket.h \
|
||||
core.h /usr/include/stdlib.h /usr/include/bits/libc-header-start.h \
|
||||
/usr/include/features.h /usr/include/features-time64.h \
|
||||
/usr/include/bits/wordsize.h /usr/include/bits/timesize.h \
|
||||
/usr/include/sys/cdefs.h /usr/include/bits/long-double.h \
|
||||
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h \
|
||||
/usr/include/bits/waitflags.h /usr/include/bits/waitstatus.h \
|
||||
/usr/include/bits/floatn.h /usr/include/bits/floatn-common.h \
|
||||
/usr/include/sys/types.h /usr/include/bits/types.h \
|
||||
/usr/include/bits/typesizes.h /usr/include/bits/time64.h \
|
||||
/usr/include/bits/types/clock_t.h /usr/include/bits/types/clockid_t.h \
|
||||
/usr/include/bits/types/time_t.h /usr/include/bits/types/timer_t.h \
|
||||
/usr/include/bits/stdint-intn.h /usr/include/endian.h \
|
||||
/usr/include/bits/endian.h /usr/include/bits/endianness.h \
|
||||
/usr/include/bits/byteswap.h /usr/include/bits/uintn-identity.h \
|
||||
/usr/include/sys/select.h /usr/include/bits/select.h \
|
||||
/usr/include/bits/types/sigset_t.h /usr/include/bits/types/__sigset_t.h \
|
||||
/usr/include/bits/types/struct_timeval.h \
|
||||
/usr/include/bits/types/struct_timespec.h \
|
||||
/usr/include/bits/pthreadtypes.h /usr/include/bits/thread-shared-types.h \
|
||||
/usr/include/bits/pthreadtypes-arch.h \
|
||||
/usr/include/bits/atomic_wide_counter.h /usr/include/bits/struct_mutex.h \
|
||||
/usr/include/bits/struct_rwlock.h /usr/include/alloca.h \
|
||||
/usr/include/bits/stdlib-bsearch.h /usr/include/bits/stdlib-float.h \
|
||||
/usr/include/string.h /usr/include/bits/types/locale_t.h \
|
||||
/usr/include/bits/types/__locale_t.h /usr/include/strings.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h \
|
||||
/usr/include/zmq.h /usr/include/errno.h /usr/include/bits/errno.h \
|
||||
/usr/include/linux/errno.h /usr/include/asm/errno.h \
|
||||
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h \
|
||||
/usr/include/stdio.h /usr/include/bits/types/__fpos_t.h \
|
||||
/usr/include/bits/types/__mbstate_t.h \
|
||||
/usr/include/bits/types/__fpos64_t.h /usr/include/bits/types/__FILE.h \
|
||||
/usr/include/bits/types/FILE.h /usr/include/bits/types/struct_FILE.h \
|
||||
/usr/include/bits/types/cookie_io_functions_t.h \
|
||||
/usr/include/bits/stdio_lim.h /usr/include/bits/stdio.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h \
|
||||
/usr/include/stdint.h /usr/include/bits/wchar.h \
|
||||
/usr/include/bits/stdint-uintn.h /usr/include/bits/stdint-least.h \
|
||||
/usr/include/signal.h /usr/include/bits/signum-generic.h \
|
||||
/usr/include/bits/signum-arch.h /usr/include/bits/types/sig_atomic_t.h \
|
||||
/usr/include/bits/types/siginfo_t.h /usr/include/bits/types/__sigval_t.h \
|
||||
/usr/include/bits/siginfo-arch.h /usr/include/bits/siginfo-consts.h \
|
||||
/usr/include/bits/types/sigval_t.h /usr/include/bits/types/sigevent_t.h \
|
||||
/usr/include/bits/sigevent-consts.h /usr/include/bits/sigaction.h \
|
||||
/usr/include/bits/sigcontext.h /usr/include/bits/types/stack_t.h \
|
||||
/usr/include/sys/ucontext.h /usr/include/bits/sigstack.h \
|
||||
/usr/include/bits/sigstksz.h /usr/include/bits/ss_flags.h \
|
||||
/usr/include/bits/types/struct_sigstack.h /usr/include/bits/sigthread.h \
|
||||
/usr/include/bits/signal_ext.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h emacs-module.h \
|
||||
/usr/include/assert.h
|
||||
/usr/include/stdc-predef.h:
|
||||
socket.h:
|
||||
core.h:
|
||||
/usr/include/stdlib.h:
|
||||
/usr/include/bits/libc-header-start.h:
|
||||
/usr/include/features.h:
|
||||
/usr/include/features-time64.h:
|
||||
/usr/include/bits/wordsize.h:
|
||||
/usr/include/bits/timesize.h:
|
||||
/usr/include/sys/cdefs.h:
|
||||
/usr/include/bits/long-double.h:
|
||||
/usr/include/gnu/stubs.h:
|
||||
/usr/include/gnu/stubs-64.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h:
|
||||
/usr/include/bits/waitflags.h:
|
||||
/usr/include/bits/waitstatus.h:
|
||||
/usr/include/bits/floatn.h:
|
||||
/usr/include/bits/floatn-common.h:
|
||||
/usr/include/sys/types.h:
|
||||
/usr/include/bits/types.h:
|
||||
/usr/include/bits/typesizes.h:
|
||||
/usr/include/bits/time64.h:
|
||||
/usr/include/bits/types/clock_t.h:
|
||||
/usr/include/bits/types/clockid_t.h:
|
||||
/usr/include/bits/types/time_t.h:
|
||||
/usr/include/bits/types/timer_t.h:
|
||||
/usr/include/bits/stdint-intn.h:
|
||||
/usr/include/endian.h:
|
||||
/usr/include/bits/endian.h:
|
||||
/usr/include/bits/endianness.h:
|
||||
/usr/include/bits/byteswap.h:
|
||||
/usr/include/bits/uintn-identity.h:
|
||||
/usr/include/sys/select.h:
|
||||
/usr/include/bits/select.h:
|
||||
/usr/include/bits/types/sigset_t.h:
|
||||
/usr/include/bits/types/__sigset_t.h:
|
||||
/usr/include/bits/types/struct_timeval.h:
|
||||
/usr/include/bits/types/struct_timespec.h:
|
||||
/usr/include/bits/pthreadtypes.h:
|
||||
/usr/include/bits/thread-shared-types.h:
|
||||
/usr/include/bits/pthreadtypes-arch.h:
|
||||
/usr/include/bits/atomic_wide_counter.h:
|
||||
/usr/include/bits/struct_mutex.h:
|
||||
/usr/include/bits/struct_rwlock.h:
|
||||
/usr/include/alloca.h:
|
||||
/usr/include/bits/stdlib-bsearch.h:
|
||||
/usr/include/bits/stdlib-float.h:
|
||||
/usr/include/string.h:
|
||||
/usr/include/bits/types/locale_t.h:
|
||||
/usr/include/bits/types/__locale_t.h:
|
||||
/usr/include/strings.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h:
|
||||
/usr/include/zmq.h:
|
||||
/usr/include/errno.h:
|
||||
/usr/include/bits/errno.h:
|
||||
/usr/include/linux/errno.h:
|
||||
/usr/include/asm/errno.h:
|
||||
/usr/include/asm-generic/errno.h:
|
||||
/usr/include/asm-generic/errno-base.h:
|
||||
/usr/include/stdio.h:
|
||||
/usr/include/bits/types/__fpos_t.h:
|
||||
/usr/include/bits/types/__mbstate_t.h:
|
||||
/usr/include/bits/types/__fpos64_t.h:
|
||||
/usr/include/bits/types/__FILE.h:
|
||||
/usr/include/bits/types/FILE.h:
|
||||
/usr/include/bits/types/struct_FILE.h:
|
||||
/usr/include/bits/types/cookie_io_functions_t.h:
|
||||
/usr/include/bits/stdio_lim.h:
|
||||
/usr/include/bits/stdio.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h:
|
||||
/usr/include/stdint.h:
|
||||
/usr/include/bits/wchar.h:
|
||||
/usr/include/bits/stdint-uintn.h:
|
||||
/usr/include/bits/stdint-least.h:
|
||||
/usr/include/signal.h:
|
||||
/usr/include/bits/signum-generic.h:
|
||||
/usr/include/bits/signum-arch.h:
|
||||
/usr/include/bits/types/sig_atomic_t.h:
|
||||
/usr/include/bits/types/siginfo_t.h:
|
||||
/usr/include/bits/types/__sigval_t.h:
|
||||
/usr/include/bits/siginfo-arch.h:
|
||||
/usr/include/bits/siginfo-consts.h:
|
||||
/usr/include/bits/types/sigval_t.h:
|
||||
/usr/include/bits/types/sigevent_t.h:
|
||||
/usr/include/bits/sigevent-consts.h:
|
||||
/usr/include/bits/sigaction.h:
|
||||
/usr/include/bits/sigcontext.h:
|
||||
/usr/include/bits/types/stack_t.h:
|
||||
/usr/include/sys/ucontext.h:
|
||||
/usr/include/bits/sigstack.h:
|
||||
/usr/include/bits/sigstksz.h:
|
||||
/usr/include/bits/ss_flags.h:
|
||||
/usr/include/bits/types/struct_sigstack.h:
|
||||
/usr/include/bits/sigthread.h:
|
||||
/usr/include/bits/signal_ext.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h:
|
||||
emacs-module.h:
|
||||
/usr/include/assert.h:
|
||||
149
lisp/zmq/src/.deps/emacs_zmq_la-util.Plo
Normal file
149
lisp/zmq/src/.deps/emacs_zmq_la-util.Plo
Normal file
@@ -0,0 +1,149 @@
|
||||
emacs_zmq_la-util.lo: util.c /usr/include/stdc-predef.h util.h core.h \
|
||||
/usr/include/stdlib.h /usr/include/bits/libc-header-start.h \
|
||||
/usr/include/features.h /usr/include/features-time64.h \
|
||||
/usr/include/bits/wordsize.h /usr/include/bits/timesize.h \
|
||||
/usr/include/sys/cdefs.h /usr/include/bits/long-double.h \
|
||||
/usr/include/gnu/stubs.h /usr/include/gnu/stubs-64.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h \
|
||||
/usr/include/bits/waitflags.h /usr/include/bits/waitstatus.h \
|
||||
/usr/include/bits/floatn.h /usr/include/bits/floatn-common.h \
|
||||
/usr/include/sys/types.h /usr/include/bits/types.h \
|
||||
/usr/include/bits/typesizes.h /usr/include/bits/time64.h \
|
||||
/usr/include/bits/types/clock_t.h /usr/include/bits/types/clockid_t.h \
|
||||
/usr/include/bits/types/time_t.h /usr/include/bits/types/timer_t.h \
|
||||
/usr/include/bits/stdint-intn.h /usr/include/endian.h \
|
||||
/usr/include/bits/endian.h /usr/include/bits/endianness.h \
|
||||
/usr/include/bits/byteswap.h /usr/include/bits/uintn-identity.h \
|
||||
/usr/include/sys/select.h /usr/include/bits/select.h \
|
||||
/usr/include/bits/types/sigset_t.h /usr/include/bits/types/__sigset_t.h \
|
||||
/usr/include/bits/types/struct_timeval.h \
|
||||
/usr/include/bits/types/struct_timespec.h \
|
||||
/usr/include/bits/pthreadtypes.h /usr/include/bits/thread-shared-types.h \
|
||||
/usr/include/bits/pthreadtypes-arch.h \
|
||||
/usr/include/bits/atomic_wide_counter.h /usr/include/bits/struct_mutex.h \
|
||||
/usr/include/bits/struct_rwlock.h /usr/include/alloca.h \
|
||||
/usr/include/bits/stdlib-bsearch.h /usr/include/bits/stdlib-float.h \
|
||||
/usr/include/string.h /usr/include/bits/types/locale_t.h \
|
||||
/usr/include/bits/types/__locale_t.h /usr/include/strings.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h \
|
||||
/usr/include/zmq.h /usr/include/errno.h /usr/include/bits/errno.h \
|
||||
/usr/include/linux/errno.h /usr/include/asm/errno.h \
|
||||
/usr/include/asm-generic/errno.h /usr/include/asm-generic/errno-base.h \
|
||||
/usr/include/stdio.h /usr/include/bits/types/__fpos_t.h \
|
||||
/usr/include/bits/types/__mbstate_t.h \
|
||||
/usr/include/bits/types/__fpos64_t.h /usr/include/bits/types/__FILE.h \
|
||||
/usr/include/bits/types/FILE.h /usr/include/bits/types/struct_FILE.h \
|
||||
/usr/include/bits/types/cookie_io_functions_t.h \
|
||||
/usr/include/bits/stdio_lim.h /usr/include/bits/stdio.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h \
|
||||
/usr/include/stdint.h /usr/include/bits/wchar.h \
|
||||
/usr/include/bits/stdint-uintn.h /usr/include/bits/stdint-least.h \
|
||||
/usr/include/signal.h /usr/include/bits/signum-generic.h \
|
||||
/usr/include/bits/signum-arch.h /usr/include/bits/types/sig_atomic_t.h \
|
||||
/usr/include/bits/types/siginfo_t.h /usr/include/bits/types/__sigval_t.h \
|
||||
/usr/include/bits/siginfo-arch.h /usr/include/bits/siginfo-consts.h \
|
||||
/usr/include/bits/types/sigval_t.h /usr/include/bits/types/sigevent_t.h \
|
||||
/usr/include/bits/sigevent-consts.h /usr/include/bits/sigaction.h \
|
||||
/usr/include/bits/sigcontext.h /usr/include/bits/types/stack_t.h \
|
||||
/usr/include/sys/ucontext.h /usr/include/bits/sigstack.h \
|
||||
/usr/include/bits/sigstksz.h /usr/include/bits/ss_flags.h \
|
||||
/usr/include/bits/types/struct_sigstack.h /usr/include/bits/sigthread.h \
|
||||
/usr/include/bits/signal_ext.h \
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h emacs-module.h
|
||||
/usr/include/stdc-predef.h:
|
||||
util.h:
|
||||
core.h:
|
||||
/usr/include/stdlib.h:
|
||||
/usr/include/bits/libc-header-start.h:
|
||||
/usr/include/features.h:
|
||||
/usr/include/features-time64.h:
|
||||
/usr/include/bits/wordsize.h:
|
||||
/usr/include/bits/timesize.h:
|
||||
/usr/include/sys/cdefs.h:
|
||||
/usr/include/bits/long-double.h:
|
||||
/usr/include/gnu/stubs.h:
|
||||
/usr/include/gnu/stubs-64.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stddef.h:
|
||||
/usr/include/bits/waitflags.h:
|
||||
/usr/include/bits/waitstatus.h:
|
||||
/usr/include/bits/floatn.h:
|
||||
/usr/include/bits/floatn-common.h:
|
||||
/usr/include/sys/types.h:
|
||||
/usr/include/bits/types.h:
|
||||
/usr/include/bits/typesizes.h:
|
||||
/usr/include/bits/time64.h:
|
||||
/usr/include/bits/types/clock_t.h:
|
||||
/usr/include/bits/types/clockid_t.h:
|
||||
/usr/include/bits/types/time_t.h:
|
||||
/usr/include/bits/types/timer_t.h:
|
||||
/usr/include/bits/stdint-intn.h:
|
||||
/usr/include/endian.h:
|
||||
/usr/include/bits/endian.h:
|
||||
/usr/include/bits/endianness.h:
|
||||
/usr/include/bits/byteswap.h:
|
||||
/usr/include/bits/uintn-identity.h:
|
||||
/usr/include/sys/select.h:
|
||||
/usr/include/bits/select.h:
|
||||
/usr/include/bits/types/sigset_t.h:
|
||||
/usr/include/bits/types/__sigset_t.h:
|
||||
/usr/include/bits/types/struct_timeval.h:
|
||||
/usr/include/bits/types/struct_timespec.h:
|
||||
/usr/include/bits/pthreadtypes.h:
|
||||
/usr/include/bits/thread-shared-types.h:
|
||||
/usr/include/bits/pthreadtypes-arch.h:
|
||||
/usr/include/bits/atomic_wide_counter.h:
|
||||
/usr/include/bits/struct_mutex.h:
|
||||
/usr/include/bits/struct_rwlock.h:
|
||||
/usr/include/alloca.h:
|
||||
/usr/include/bits/stdlib-bsearch.h:
|
||||
/usr/include/bits/stdlib-float.h:
|
||||
/usr/include/string.h:
|
||||
/usr/include/bits/types/locale_t.h:
|
||||
/usr/include/bits/types/__locale_t.h:
|
||||
/usr/include/strings.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdarg.h:
|
||||
/usr/include/zmq.h:
|
||||
/usr/include/errno.h:
|
||||
/usr/include/bits/errno.h:
|
||||
/usr/include/linux/errno.h:
|
||||
/usr/include/asm/errno.h:
|
||||
/usr/include/asm-generic/errno.h:
|
||||
/usr/include/asm-generic/errno-base.h:
|
||||
/usr/include/stdio.h:
|
||||
/usr/include/bits/types/__fpos_t.h:
|
||||
/usr/include/bits/types/__mbstate_t.h:
|
||||
/usr/include/bits/types/__fpos64_t.h:
|
||||
/usr/include/bits/types/__FILE.h:
|
||||
/usr/include/bits/types/FILE.h:
|
||||
/usr/include/bits/types/struct_FILE.h:
|
||||
/usr/include/bits/types/cookie_io_functions_t.h:
|
||||
/usr/include/bits/stdio_lim.h:
|
||||
/usr/include/bits/stdio.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdint.h:
|
||||
/usr/include/stdint.h:
|
||||
/usr/include/bits/wchar.h:
|
||||
/usr/include/bits/stdint-uintn.h:
|
||||
/usr/include/bits/stdint-least.h:
|
||||
/usr/include/signal.h:
|
||||
/usr/include/bits/signum-generic.h:
|
||||
/usr/include/bits/signum-arch.h:
|
||||
/usr/include/bits/types/sig_atomic_t.h:
|
||||
/usr/include/bits/types/siginfo_t.h:
|
||||
/usr/include/bits/types/__sigval_t.h:
|
||||
/usr/include/bits/siginfo-arch.h:
|
||||
/usr/include/bits/siginfo-consts.h:
|
||||
/usr/include/bits/types/sigval_t.h:
|
||||
/usr/include/bits/types/sigevent_t.h:
|
||||
/usr/include/bits/sigevent-consts.h:
|
||||
/usr/include/bits/sigaction.h:
|
||||
/usr/include/bits/sigcontext.h:
|
||||
/usr/include/bits/types/stack_t.h:
|
||||
/usr/include/sys/ucontext.h:
|
||||
/usr/include/bits/sigstack.h:
|
||||
/usr/include/bits/sigstksz.h:
|
||||
/usr/include/bits/ss_flags.h:
|
||||
/usr/include/bits/types/struct_sigstack.h:
|
||||
/usr/include/bits/sigthread.h:
|
||||
/usr/include/bits/signal_ext.h:
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include/stdbool.h:
|
||||
emacs-module.h:
|
||||
1
lisp/zmq/src/.libs/emacs-zmq.la
Symbolic link
1
lisp/zmq/src/.libs/emacs-zmq.la
Symbolic link
@@ -0,0 +1 @@
|
||||
../emacs-zmq.la
|
||||
41
lisp/zmq/src/.libs/emacs-zmq.lai
Normal file
41
lisp/zmq/src/.libs/emacs-zmq.lai
Normal file
@@ -0,0 +1,41 @@
|
||||
# emacs-zmq.la - a libtool library file
|
||||
# Generated by libtool (GNU libtool) 2.4.7.4-1ec8f-dirty
|
||||
#
|
||||
# Please DO NOT delete this file!
|
||||
# It is necessary for linking the library.
|
||||
|
||||
# The name that we can dlopen(3).
|
||||
dlname='emacs-zmq.so'
|
||||
|
||||
# Names of this library.
|
||||
library_names='emacs-zmq.so emacs-zmq.so emacs-zmq.so'
|
||||
|
||||
# The name of the static archive.
|
||||
old_library=''
|
||||
|
||||
# Linker flags that cannot go in dependency_libs.
|
||||
inherited_linker_flags=''
|
||||
|
||||
# Libraries that this one depends upon.
|
||||
dependency_libs=' -lzmq'
|
||||
|
||||
# Names of additional weak libraries provided by this library
|
||||
weak_library_names=''
|
||||
|
||||
# Version information for emacs-zmq.
|
||||
current=0
|
||||
age=0
|
||||
revision=0
|
||||
|
||||
# Is this an already installed library?
|
||||
installed=yes
|
||||
|
||||
# Should we warn about portability when linking against -modules?
|
||||
shouldnotlink=yes
|
||||
|
||||
# Files to dlopen/dlpreopen
|
||||
dlopen=''
|
||||
dlpreopen=''
|
||||
|
||||
# Directory that this library needs to be installed in:
|
||||
libdir='/home/daniel/repos/emacs-conf/lisp/zmq/lib'
|
||||
BIN
lisp/zmq/src/.libs/emacs-zmq.so
Executable file
BIN
lisp/zmq/src/.libs/emacs-zmq.so
Executable file
Binary file not shown.
BIN
lisp/zmq/src/.libs/emacs_zmq_la-constants.o
Normal file
BIN
lisp/zmq/src/.libs/emacs_zmq_la-constants.o
Normal file
Binary file not shown.
BIN
lisp/zmq/src/.libs/emacs_zmq_la-context.o
Normal file
BIN
lisp/zmq/src/.libs/emacs_zmq_la-context.o
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user