├── .envrc ├── thumbnail.png ├── test ├── testein-loader.el ├── redirecting-server.py ├── test-ein-process.el ├── testein.el ├── test-ein-core.el ├── test-ein-modes.el ├── test-ein-ob.el ├── test-ein-shared-output.el ├── test-ein-content.el ├── test-ein-notebooklist.el ├── test-ein-kill-ring.el ├── test-ein-worksheet.el ├── test-ein-output-area.el ├── test-ein-worksheet-notebook.el ├── ein-testing-cell.el ├── test-ein-kernel.el ├── test-ein-notification.el ├── test-ein-node.el ├── ein-testing-notebook.el ├── ein-testing.el ├── test-ein-utils.el └── test-ein-cell-notebook.el ├── .gitignore ├── features ├── dev.feature ├── jupyterhub.feature ├── connect.feature ├── gat.feature ├── polymode.feature ├── execute-all-cells.feature ├── ipdb.feature ├── undo.ipynb ├── notebook.feature ├── notebooklist.feature ├── ob-ein.feature ├── support │ └── env.el └── undo.feature ├── Cask ├── tools ├── readme-sed.sh ├── install-virtualenv.sh ├── install-julia.sh ├── retry.sh ├── install-R.sh └── install-cask.sh ├── lisp ├── ein-completer.el ├── ein-scratchsheet.el ├── ein-kernelinfo.el ├── ein-pytools.el ├── ein-kill-ring.el ├── ein-events.el ├── ein.el ├── ein-node.el ├── ein-file.el ├── ein-pager.el ├── ein-ipynb-mode.el ├── ein-log.el ├── ein-websocket.el ├── ein-ipdb.el ├── ein-python-send.el ├── ein-core.el ├── ein-output-area.el ├── ein-notification.el └── ein-traceback.el ├── .github └── workflows │ └── test.yml ├── README.in.rst ├── Makefile ├── .appveyor.yml └── README.rst /.envrc: -------------------------------------------------------------------------------- 1 | export VIRTUAL_ENV="venv" 2 | layout python 3 | -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/millejoh/emacs-ipython-notebook/HEAD/thumbnail.png -------------------------------------------------------------------------------- /test/testein-loader.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (load "testein.el" nil t) 3 | (dolist (file (directory-files (file-name-directory load-file-name) t "test-ein.*\\.el")) 4 | (load file nil t)) 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/build/ 2 | .cask/* 3 | *.pyc 4 | *.elc 5 | log/* 6 | *.zip 7 | .gitattributes 8 | .ecukes* 9 | dist 10 | .*ein*ipynb 11 | .Rhistory 12 | .ipynb_checkpoints 13 | ein-autoloads.el 14 | ert-profile 15 | ein-images 16 | -------------------------------------------------------------------------------- /features/dev.feature: -------------------------------------------------------------------------------- 1 | @dev 2 | Scenario: dev-bug-report-template needs to work 3 | Given I call "ein:dev-bug-report-template" 4 | And I switch to buffer "*ein:bug-report*" 5 | Then I should see ":lib" 6 | And I should see "#### *ein:log-all*" 7 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package-file "lisp/ein.el") 5 | (files "lisp/*.el") 6 | 7 | (development 8 | (depends-on "ert-runner") 9 | (depends-on "ecukes") 10 | (depends-on "espuds") 11 | (depends-on "mocker") 12 | (depends-on "julia-mode") 13 | (depends-on "magit") 14 | (depends-on "ess") 15 | (depends-on "f") 16 | (depends-on "s") 17 | (depends-on "exec-path-from-shell")) 18 | -------------------------------------------------------------------------------- /test/redirecting-server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/') 6 | def jupyter_redirect(): 7 | return redirect("http://127.0.0.1:8888/", code=302) 8 | 9 | @app.route('/api') 10 | def api_check(): 11 | return redirect("http://127.0.0.1:8888/api", code=302) 12 | 13 | if __name__=='__main__': 14 | port = int(8000) 15 | app.run(host='127.0.0.1', port=port) 16 | 17 | 18 | -------------------------------------------------------------------------------- /features/jupyterhub.feature: -------------------------------------------------------------------------------- 1 | @jupyterhub 2 | Scenario: basic spawn 3 | Given I start jupyterhub configured "c.JupyterHub.answer_yes=True\nc.JupyterHub.authenticator_class='dummyauthenticator.DummyAuthenticator'\nc.JupyterHub.cookie_secret_file = '/var/tmp/jupyterhub_cookie_secret'\nc.JupyterHub.spawner_class = 'simplespawner.SimpleLocalProcessSpawner'\n" 4 | And I switch to log expr "ein:log-all-buffer-name" 5 | Then I should not see "[warn]" 6 | And I should not see ": [error]" 7 | Then I login to jupyterhub 8 | -------------------------------------------------------------------------------- /test/test-ein-process.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | (require 'ein-process) 4 | 5 | (ert-deftest ein:process-check-suitable () 6 | (should-not (equal (ein:process-suitable-notebook-dir (concat default-directory "features/support")) (concat default-directory "features/support")))) 7 | 8 | (ert-deftest ein:process-divine () 9 | (with-current-buffer "*scratch*" 10 | (erase-buffer)) 11 | (ein:process-divine-dir 1 "" "*scratch*") 12 | (ein:process-divine-port 1 "" "*scratch*")) 13 | -------------------------------------------------------------------------------- /test/testein.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (custom-set-variables '(python-indent-guess-indent-offset-verbose nil) 3 | '(ein:jupyter-use-containers nil) 4 | '(ein:output-area-inlined-images t)) 5 | 6 | (require 'python) 7 | (require 'ein-dev) 8 | (require 'ein-testing) 9 | 10 | (ein:setq-if-not ein:testing-dump-file-log "./log/testein.log") 11 | (ein:setq-if-not ein:testing-dump-file-messages "./log/testein.messages") 12 | (setq ert-runner-profile nil) 13 | 14 | (setq message-log-max t) 15 | -------------------------------------------------------------------------------- /tools/readme-sed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # by mklement0 https://stackoverflow.com/a/29613573/5132008 4 | 5 | # Define sample multi-line literal. 6 | input=`cat` 7 | replace="$input" 8 | if [ ! -z "$3" ]; then 9 | replace=$(awk "BEGIN{IGNORECASE=1} /$3/,EOF { print \" \" \$0 }" <<<"$input") 10 | fi 11 | 12 | # Escape it for use as a Sed replacement string. 13 | IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$replace") 14 | replaceEscaped=${REPLY%$'\n'} 15 | 16 | # If ok, outputs $replace as is. 17 | sed "/$1/c\\$replaceEscaped" $2 18 | -------------------------------------------------------------------------------- /tools/install-virtualenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create virtualenvs for python{2,3} for Travis CI on OSX 4 | 5 | set -x 6 | 7 | WORKDIR=${HOME}/local 8 | 9 | . tools/retry.sh 10 | 11 | if [ "x$TRAVIS_OS_NAME" = "xosx" ]; then 12 | brew list pyenv-virtualenv &>/dev/null || HOMEBREW_NO_AUTO_UPDATE=1 brew install pyenv-virtualenv 13 | 14 | case "${TOXENV}" in 15 | py27) 16 | pyenv install -s 2.7.13 17 | pyenv virtualenv -f 2.7.13 py27 18 | ;; 19 | py35) 20 | pyenv install -s 3.5.2 21 | pyenv virtualenv -f 3.5.2 py35 22 | ;; 23 | esac 24 | fi 25 | -------------------------------------------------------------------------------- /test/test-ein-core.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'tramp) 5 | (require 'ein-core) 6 | 7 | ;; Generic getter 8 | 9 | (defmacro eintest:generic-getter-should-return-nil (func) 10 | "In an \"empty\" context, generic getter should return nil." 11 | `(ert-deftest ,(intern (format "%s--nil name" func)) () 12 | (with-temp-buffer 13 | (should (not (,func)))))) 14 | 15 | (eintest:generic-getter-should-return-nil ein:get-url-or-port) 16 | (eintest:generic-getter-should-return-nil ein:get-notebook) 17 | (eintest:generic-getter-should-return-nil ein:get-kernel) 18 | (eintest:generic-getter-should-return-nil ein:get-cell-at-point) 19 | (eintest:generic-getter-should-return-nil ein:get-traceback-data) 20 | -------------------------------------------------------------------------------- /tools/install-julia.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install julia for Actions CI 4 | 5 | set -ex 6 | 7 | WORKDIR=${HOME}/local 8 | UNAME=$(uname -s) 9 | cd $WORKDIR 10 | if [ "x$UNAME" = "xLinux" ] ; then 11 | if [ ! -d ${WORKDIR}/julia-1.3.1 ]; then 12 | wget https://julialang-s3.julialang.org/bin/linux/x64/1.3/julia-1.3.1-linux-x86_64.tar.gz 13 | tar zxvf julia-1.3.1-linux-x86_64.tar.gz 14 | rm -f julia-1.3.1-linux-x86_64.tar.gz 15 | fi 16 | hash 17 | julia --version 18 | julia -e 'import Pkg; Pkg.add("IJulia")' 19 | elif [ "x$UNAME" = "xDarwin" ]; then 20 | brew update 21 | brew cask list julia &>/dev/null || HOMEBREW_NO_AUTO_UPDATE=1 brew cask install julia 22 | julia --version 23 | julia -e 'import Pkg; Pkg.add("IJulia")' 24 | fi 25 | -------------------------------------------------------------------------------- /tools/retry.sh: -------------------------------------------------------------------------------- 1 | # Copied retry logic from Travis CI [http://bit.ly/2jPDCtV] 2 | # Author: gonewest818 https://github.com/clojure-emacs/cider/pull/2139 3 | 4 | ANSI_RED="\033[31;1m" 5 | ANSI_GREEN="\033[32;1m" 6 | ANSI_RESET="\033[0m" 7 | ANSI_CLEAR="\033[0K" 8 | 9 | travis_retry() { 10 | local result=0 11 | local count=1 12 | while [ $count -le 3 ]; do 13 | [ $result -ne 0 ] && { 14 | echo -e "\n${ANSI_RED}The command \"$@\" failed. Retrying, $count of 3.${ANSI_RESET}\n" >&2 15 | } 16 | "$@" 17 | result=$? 18 | [ $result -eq 0 ] && break 19 | count=$(($count + 1)) 20 | sleep 1 21 | done 22 | 23 | [ $count -gt 3 ] && { 24 | echo -e "\n${ANSI_RED}The command \"$@\" failed 3 times.${ANSI_RESET}\n" >&2 25 | } 26 | 27 | return $result 28 | } 29 | -------------------------------------------------------------------------------- /tools/install-R.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install R for Actions CI 4 | 5 | set -ex 6 | 7 | WORKDIR=${HOME}/local 8 | UNAME=$(uname -s) 9 | cd $WORKDIR 10 | if [ "x$UNAME" = "xLinux" ] ; then 11 | if [ ! -d ${WORKDIR}/R ]; then 12 | wget http://cran.mirrors.hoobly.com/src/base/R-3/R-3.6.3.tar.gz 13 | tar xvf R-3.6.3.tar.gz 14 | cd R-3.6.3 15 | ./configure --prefix=${WORKDIR}/R 16 | make && make install 17 | find ${WORKDIR}/R -name R -print 18 | fi 19 | elif [ "x$UNAME" = "xDarwin" ]; then 20 | brew list r &>/dev/null || HOMEBREW_NO_AUTO_UPDATE=1 brew install r 21 | fi 22 | mkdir -p ${WORKDIR}/R 23 | echo ".libPaths( c( '${WORKDIR}/R' , .libPaths() ) )" > ${HOME}/.Rprofile 24 | R -e "install.packages(c('pbdZMQ', 'repr', 'IRdisplay', 'IRkernel'), type='source', repos='http://cran.mirrors.hoobly.com', lib='${WORKDIR}/R')" 25 | R -e "IRkernel::installspec()" 26 | R --version 27 | -------------------------------------------------------------------------------- /features/connect.feature: -------------------------------------------------------------------------------- 1 | @connect 2 | Scenario: Connect buffer to an open notebook 3 | Given new python notebook 4 | And eval "(get-buffer-create "connect.feature.py")" 5 | And I switch to buffer "connect.feature.py" 6 | And I call "python-mode" 7 | And I type "a = 10" 8 | And I press "C-c C-/ e" 9 | And I wait for the smoke to clear 10 | And I switch to buffer like "Untitled" 11 | And I type "a" 12 | And I wait for cell to execute 13 | Then I should see "10" 14 | And I switch to buffer "connect.feature.py" 15 | And I press "RET" 16 | And I press "RET" 17 | And I type "for i in range(10):" 18 | And I press "RET" 19 | And I type " j = i" 20 | And I press "C-p" 21 | And I press "C-a" 22 | And I press "C-SPC" 23 | And I press "C-x ]" 24 | And I press "C-c C-/ r" 25 | And I switch to buffer like "Untitled" 26 | And I press "C-c C-b" 27 | And I type "j" 28 | And I wait for cell to execute 29 | Then I should see "9" 30 | -------------------------------------------------------------------------------- /test/test-ein-modes.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'ein-dev) 5 | 6 | (defun eintest:assert-keymap-fboundp (keymap) 7 | (cl-labels ((assert-fboundp (event value) 8 | (cond 9 | ((keymapp value) 10 | (map-keymap #'assert-fboundp value)) 11 | ((and (listp value) (eq (car value) 'menu-item)) 12 | (funcall #'assert-fboundp (cadr value) (cl-caddr value))) 13 | ((not (equal (car-safe value) "--")) 14 | (if (consp value) 15 | (should (functionp (cdr value))) 16 | (when value ; nil is also valid in keymap 17 | (should (commandp value)))))))) 18 | (map-keymap #'assert-fboundp keymap))) 19 | 20 | (defmacro eintest:test-keymap (keymap) 21 | `(ert-deftest ,(intern (format "%s--assert-fboundp" keymap)) () 22 | (eintest:assert-keymap-fboundp ,keymap))) 23 | 24 | (eintest:test-keymap ein:notebooklist-mode-map) 25 | (eintest:test-keymap ein:notebook-mode-map) 26 | (eintest:test-keymap ein:traceback-mode-map) 27 | (eintest:test-keymap ein:shared-output-mode-map) 28 | -------------------------------------------------------------------------------- /test/test-ein-ob.el: -------------------------------------------------------------------------------- 1 | ; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | (require 'ob-ein) 4 | (require 'org) 5 | 6 | (ert-deftest ein:ob-anonymous-p () 7 | (should (ob-ein-anonymous-p "ob-ein-python.ipynb")) 8 | (should (ob-ein-anonymous-p "ob-ein.ipynb")) 9 | (should-not (ob-ein-anonymous-p "ein-python.ipynb")) 10 | (should-not (ob-ein-anonymous-p "Untitled.ipynb"))) 11 | 12 | ;;; This is the content portion of a response from the content API. 13 | (defvar eintest:ob-src-block 14 | "#+BEGIN_SRC ein :session 8888/Untitled.ipynb 15 | import sys 16 | 17 | a = 14500 18 | b = a+1000 19 | sys.version 20 | #+END_SRC 21 | ") 22 | 23 | (ert-deftest ein:ob-aware () 24 | (let ((org-babel-load-languages (quote ((ein . t))))) 25 | (with-temp-buffer 26 | (save-excursion 27 | (org-mode) 28 | (insert eintest:ob-src-block) 29 | (search-backward "SRC") 30 | (cl-letf (((symbol-function 'ob-ein--initiate-session) 31 | (lambda (&rest _args) (make-ein:$notebook)))) 32 | (should (call-interactively #'org-edit-special))))))) 33 | -------------------------------------------------------------------------------- /test/test-ein-shared-output.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ein-shared-output) 3 | 4 | (defmacro eintest:shared-output-with-buffer (&rest body) 5 | (declare (indent 0)) 6 | `(with-current-buffer (ein:shared-output-create-buffer) 7 | (ein:shared-output-get-or-create) 8 | ,@body)) 9 | 10 | (defmacro eintest:shared-output-is-empty-context-of (func) 11 | `(ert-deftest ,(intern (format "%s--shared-output" func)) () 12 | (eintest:shared-output-with-buffer 13 | (should-not (,func))))) 14 | 15 | 16 | ;; Generic getter 17 | 18 | (ert-deftest ein:get-cell-at-point--shared-output () 19 | (eintest:shared-output-with-buffer 20 | (should (eq (ein:get-cell-at-point) 21 | (ein:shared-output-get-cell)))) 22 | (with-temp-buffer 23 | (should-not (ein:get-cell-at-point--shared-output)))) 24 | 25 | ;; FIXME: Add tests with non-empty shared output buffer. 26 | (eintest:shared-output-is-empty-context-of ein:get-url-or-port) 27 | (eintest:shared-output-is-empty-context-of ein:get-notebook) 28 | (eintest:shared-output-is-empty-context-of ein:get-kernel) 29 | (eintest:shared-output-is-empty-context-of ein:get-traceback-data) 30 | -------------------------------------------------------------------------------- /test/test-ein-content.el: -------------------------------------------------------------------------------- 1 | ;;; test-ein-content.el --- Testing content interface -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2015 John Miller 4 | 5 | ;; Authors: Takafumi Arakaki 6 | ;; John M. Miller 7 | 8 | ;; This file is NOT part of GNU Emacs. 9 | 10 | ;; test-ein-content.el is free software: you can redistribute it 11 | ;; and/or modify it under the terms of the GNU General Public License 12 | ;; as published by the Free Software Foundation, either version 3 of 13 | ;; the License, or (at your option) any later version. 14 | 15 | ;; test-ein-content.el is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; You should have received a copy of the GNU General Public License 21 | ;; along with ein-testing-notebook.el. 22 | ;; If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; 27 | 28 | ;;; Code: 29 | 30 | (require 'ein-contents-api) 31 | 32 | (defvar *list-content-result* nil) 33 | 34 | (defun ein-test-list-contents-1 () 35 | (ein:content-list-contents "" ) 36 | ) 37 | -------------------------------------------------------------------------------- /lisp/ein-completer.el: -------------------------------------------------------------------------------- 1 | ;;; -*- mode: emacs-lisp; lexical-binding: t -*- 2 | ;;; ein-completer.el --- Completion module 3 | 4 | ;; Copyright (C) 2018- Takafumi Arakaki / John Miller 5 | 6 | ;; Author: Takafumi Arakaki / John Miller 7 | 8 | ;; This file is NOT part of GNU Emacs. 9 | 10 | ;; ein-completer.el is free software: you can redistribute it and/or modify 11 | ;; it under the terms of the GNU General Public License as published by 12 | ;; the Free Software Foundation, either version 3 of the License, or 13 | ;; (at your option) any later version. 14 | 15 | ;; ein-completer.el is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; You should have received a copy of the GNU General Public License 21 | ;; along with ein-completer.el. If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; This needs to get re-written. 26 | 27 | ;;; Code: 28 | 29 | (make-obsolete-variable 'ein:complete-on-dot nil "0.15.0") 30 | (make-obsolete-variable 'ein:completion-backend nil "0.17.0") 31 | 32 | (provide 'ein-completer) 33 | 34 | ;;; ein-completer.el ends here 35 | -------------------------------------------------------------------------------- /features/gat.feature: -------------------------------------------------------------------------------- 1 | @gat 2 | Scenario: gat create from repo, then create from worktree, then run-local 3 | Given new git repo "test-repo" 4 | Given new python notebook in "test-repo" 5 | And I type "99" 6 | And I wait for cell to execute 7 | And I clear log expr "ein:log-all-buffer-name" 8 | And I press "C-x C-s" 9 | And I switch to log expr "ein:log-all-buffer-name" 10 | And I wait for buffer to say "Notebook is saved" 11 | And I switch to buffer like "Untitled" 12 | When I gat create "foo" in "test-repo" 13 | Then I possibly wait for gat install 14 | Then I switch to buffer like ".gat/foo/Untitled" 15 | When I gat create "baz" in "test-repo" 16 | Then I switch to buffer like ".gat/baz/Untitled" 17 | When I call "ein:gat-run-local" 18 | And I switch to buffer like "Dockerfile.Untitled" 19 | And I should see "FROM dickmao" 20 | And I press "C-c C-c" 21 | And I switch to buffer like "ein-gat: test-repo" 22 | Then I wait for buffer to say "Building image" 23 | And I go to word "run" 24 | And eval "(ignore-errors (kill-process (magit-section-value-if 'process)))" 25 | And I wait for buffer to not say "run . gat" 26 | And I am in notebooklist buffer 27 | And I click on "Home" 28 | Then I wait for buffer to say "support" 29 | And remove git repo "test-repo" 30 | And eval "(kill-buffer)" 31 | -------------------------------------------------------------------------------- /test/test-ein-notebooklist.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ein-notebooklist) 3 | (require 'ein-testing-notebook) 4 | 5 | (defun eintest:notebooklist-make-empty (&optional url-or-port) 6 | "Make empty notebook list buffer." 7 | (let ((url-or-port (or url-or-port ein:testing-notebook-dummy-url))) 8 | (cl-letf (((symbol-function 'ein:need-kernelspecs) #'ignore) 9 | ((symbol-function 'ein:content-query-sessions) #'ignore)) 10 | (ein:notebooklist-open--finish url-or-port #'ignore 11 | (make-ein:$content :url-or-port url-or-port 12 | :notebook-api-version "5" 13 | :path ""))))) 14 | 15 | (defmacro eintest:notebooklist-is-empty-context-of (func) 16 | `(ert-deftest ,(intern (format "%s--notebooklist" func)) () 17 | (with-current-buffer (eintest:notebooklist-make-empty) 18 | (should-not (,func))))) 19 | 20 | (ert-deftest ein:get-url-or-port--notebooklist () 21 | (with-current-buffer (eintest:notebooklist-make-empty) 22 | (should (equal (ein:get-url-or-port) ein:testing-notebook-dummy-url)))) 23 | 24 | (eintest:notebooklist-is-empty-context-of ein:get-notebook) 25 | (eintest:notebooklist-is-empty-context-of ein:get-kernel) 26 | (eintest:notebooklist-is-empty-context-of ein:get-cell-at-point) 27 | (eintest:notebooklist-is-empty-context-of ein:get-traceback-data) 28 | -------------------------------------------------------------------------------- /test/test-ein-kill-ring.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'ein-kill-ring) 5 | 6 | (ert-deftest ein:kill-ring-simple () 7 | (let (ein:kill-ring 8 | ein:kill-ring-yank-pointer) 9 | (ein:kill-new 1) 10 | (should (equal (ein:current-kill 0) 1)))) 11 | 12 | (defun eintest:kill-ring-simple-repeat-setup () 13 | (cl-loop for i from 0 below 5 14 | do (ein:kill-new i) 15 | do (should (equal (ein:current-kill 0) i)))) 16 | 17 | (ert-deftest ein:kill-ring-simple-repeat () 18 | (let (ein:kill-ring 19 | ein:kill-ring-yank-pointer) 20 | (eintest:kill-ring-simple-repeat-setup) 21 | (should (equal ein:kill-ring ein:kill-ring-yank-pointer)) 22 | (should (equal ein:kill-ring '(4 3 2 1 0))))) 23 | 24 | (ert-deftest ein:kill-ring-repeat-n-1 () 25 | (let (ein:kill-ring 26 | ein:kill-ring-yank-pointer) 27 | (eintest:kill-ring-simple-repeat-setup) 28 | (cl-loop for i in '(3 2 1 0 4 3 2) 29 | do (should (equal (ein:current-kill 1) i))) 30 | (should-not (equal ein:kill-ring ein:kill-ring-yank-pointer)) 31 | (should (equal ein:kill-ring '(4 3 2 1 0))) 32 | (should (equal ein:kill-ring-yank-pointer '(2 1 0))))) 33 | 34 | (ert-deftest ein:kill-ring-exceeds-max () 35 | (let (ein:kill-ring 36 | ein:kill-ring-yank-pointer 37 | (ein:kill-ring-max 3)) 38 | (eintest:kill-ring-simple-repeat-setup) 39 | (should (equal ein:kill-ring ein:kill-ring-yank-pointer)) 40 | (should (equal (length ein:kill-ring) ein:kill-ring-max)) 41 | (should (equal ein:kill-ring '(4 3 2))))) 42 | -------------------------------------------------------------------------------- /test/test-ein-worksheet.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'ein-worksheet) 5 | (require 'ein-testing-cell) 6 | 7 | (defvar ein:testing-worksheet-example-data 8 | (list (ein:testing-codecell-data "code example input") 9 | (ein:testing-markdowncell-data "markdown example input") 10 | (ein:testing-rawcell-data "raw example input") 11 | (ein:testing-htmlcell-data "html example input"))) 12 | 13 | (defun ein:testing-worksheet-new () 14 | (make-instance 'ein:worksheet)) 15 | 16 | (defun ein:testing-worksheet-to-json (cells &optional metadata) 17 | (let* ((ws-0 (ein:worksheet-from-json (ein:testing-worksheet-new) 18 | (list :cells cells 19 | :metadata metadata))) 20 | (ws-1 (ein:testing-worksheet-new)) 21 | (json-0 (ein:worksheet-to-json ws-0)) 22 | (json-1 (ein:worksheet-to-json 23 | (ein:worksheet-from-json ws-1 24 | (ein:json-read-from-string 25 | (ein:json-encode json-0)))))) 26 | (let* ((found (assoc 'metadata json-0))) 27 | (when found 28 | (should (cdr found)))) 29 | (should (equal json-0 json-1)))) 30 | 31 | (ert-deftest ein:worksheet-to-json/empty () 32 | (ein:testing-worksheet-to-json nil)) 33 | 34 | (ert-deftest ein:worksheet-to-json/example-data () 35 | (ein:testing-worksheet-to-json ein:testing-worksheet-example-data)) 36 | 37 | (ert-deftest ein:worksheet-to-json/example-data-with-metadata () 38 | (ein:testing-worksheet-to-json ein:testing-worksheet-example-data 39 | '(:name "Worksheet name"))) 40 | -------------------------------------------------------------------------------- /test/test-ein-output-area.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'ein-output-area) 5 | 6 | (defun ein:testing-insert-html--fix-urls-do-test (source desired) 7 | (let ((s (ein:xml-parse-html-string source)) 8 | (d (ein:xml-parse-html-string desired))) 9 | (ein:insert-html--fix-urls s 8888) 10 | (should (equal s d)))) 11 | 12 | (defmacro ein:testing-insert-html--fix-urls-deftests (args-list) 13 | `(progn 14 | ,@(cl-loop for i from 0 15 | for args in args-list 16 | for test = (intern (format "ein:insert-html--fix-urls/%s" i)) 17 | collect 18 | `(ert-deftest ,test () 19 | (ein:testing-insert-html--fix-urls-do-test ,@args))))) 20 | 21 | (ein:testing-insert-html--fix-urls-deftests 22 | (;; Simple replaces 23 | ("text" 24 | "text") 25 | ("text" 26 | "text") 27 | ("" 28 | "") 29 | ("" 30 | "") 31 | ;; Do not modify dom in these cases: 32 | ("text" 33 | "text") 34 | ("text" 35 | "text") 36 | ("" 37 | "") 38 | ;; Bit more complicated cases: 39 | ("

link normal

" 40 | "

link normal

") 41 | (" normal

" 42 | " normal

"))) 43 | -------------------------------------------------------------------------------- /lisp/ein-scratchsheet.el: -------------------------------------------------------------------------------- 1 | ;;; ein-scratchsheet.el --- Worksheet without needs for saving -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012 Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-scratchsheet.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-scratchsheet.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-scratchsheet.el. 21 | ;; If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; 26 | 27 | ;;; Code: 28 | 29 | (require 'ein-worksheet) 30 | 31 | (defclass ein:scratchsheet (ein:worksheet) 32 | ((data :initarg :data :initform nil)) 33 | :documentation "Worksheet without needs for saving.") 34 | 35 | (defun ein:scratchsheet-new (nbformat notebook-path kernel events &rest args) 36 | (apply #'make-instance 'ein:scratchsheet 37 | :nbformat nbformat 38 | :notebook-path notebook-path 39 | :kernel kernel 40 | :events events 41 | args)) 42 | 43 | (cl-defmethod ein:worksheet--buffer-name ((ws ein:scratchsheet)) 44 | (format "*ein:scratch %s/%s*" 45 | (ein:worksheet-url-or-port ws) 46 | (ein:worksheet-notebook-path ws))) 47 | 48 | (provide 'ein-scratchsheet) 49 | 50 | ;;; ein-scratchsheet.el ends here 51 | -------------------------------------------------------------------------------- /features/polymode.feature: -------------------------------------------------------------------------------- 1 | @poly 2 | Scenario: selection spans cells 3 | Given new python notebook 4 | And I press "C-c C-b" 5 | And I press "C-" 6 | And I press "C-p" 7 | And I press "C-SPC" 8 | And I press "C-n" 9 | And I press "C-n" 10 | And I press "C-n" 11 | And I press "C-n" 12 | Then newlined region should be "In [ ]:\n\n\nIn [ ]:\n" 13 | And I press "C-g" 14 | Then the region should not be active 15 | 16 | @poly 17 | Scenario: markdown often erroneously fontifies the whole buffer 18 | Given new python notebook 19 | And I press "C-c C-t" 20 | And I type "# Header" 21 | And I press "RET" 22 | And I press "C-p" 23 | And I press "C-p" 24 | And I press "C-e" 25 | Then text property at point includes "rear-nonsticky" 26 | 27 | @poly 28 | Scenario: moving cells requires refontification 29 | Given new python notebook 30 | And I press "C-c C-t" 31 | And I type "# Header" 32 | And I press "RET" 33 | And I press "C-c C-b" 34 | And I type "import math" 35 | And I press "C-c C-c" 36 | And I press "M-" 37 | And I press "C-" 38 | And I go to word "Header" 39 | 40 | @poly 41 | Scenario: Indirect buffers confuse buffer switching without hacks 42 | Given new python notebook 43 | And I type "print("hello")" 44 | And I switch to buffer "*Messages*" 45 | And I switch to buffer like "Untitled" 46 | And I press "C-x b" 47 | Then I should be in buffer "*Messages*" 48 | And I press "C-x b" 49 | Then eval "(string-prefix-p " *ein:" (buffer-name))" 50 | 51 | @poly 52 | Scenario: ido-mode spits back the indirect buffer we're already in 53 | Given eval "(ido-mode t)" 54 | Given new python notebook 55 | And I type "print("hello")" 56 | And I press "C-x b" 57 | And I press "C-x b" 58 | Then eval "(string-prefix-p " *ein:" (buffer-name))" 59 | And eval "(ido-mode -1)" -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**.md' 7 | - '**.rst' 8 | push: 9 | paths-ignore: 10 | - '**.md' 11 | - '**.rst' 12 | branches-ignore: 13 | - 'master' 14 | 15 | jobs: 16 | build: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest] 21 | emacs_version: [27.2, 28.2, 29.1] 22 | python-version: [3.8] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: nix 33 | uses: purcell/setup-emacs@master 34 | with: 35 | version: ${{ matrix.emacs_version }} 36 | 37 | - uses: actions/cache@v4 38 | id: cache-cask-packages 39 | with: 40 | path: .cask 41 | key: cache-cask-packages-001 42 | 43 | - uses: actions/cache@v4 44 | id: cache-cask-executable 45 | with: 46 | path: ~/.cask 47 | key: cache-cask-executable-001 48 | 49 | - uses: cask/setup-cask@master 50 | if: steps.cache-cask-executable.outputs.cache-hit != 'true' 51 | with: 52 | version: snapshot 53 | 54 | - name: paths 55 | run: | 56 | echo "$HOME/.cask/bin" >> $GITHUB_PATH 57 | 58 | - name: apt-get 59 | if: startsWith(runner.os, 'disable') 60 | run: | 61 | sudo apt-get -yq update 62 | DEBIAN_FRONTEND=noninteractive sudo apt-get -yq install gnutls-bin sharutils nodejs gfortran gnupg2 dirmngr libreadline-dev libcurl4-openssl-dev texlive-latex-base libfuse-dev libxml2-dev libssl-dev libzmq3-dev jupyter-core jupyter-client 63 | 64 | - name: test-mem-constrained 65 | run: | 66 | pip install yq 67 | make quick 68 | continue-on-error: ${{ matrix.emacs_version == 'snapshot' }} 69 | -------------------------------------------------------------------------------- /tools/install-cask.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install cask for Travis CI 4 | # or if already installed, then check for updates 5 | # Author: gonewest818 https://github.com/clojure-emacs/cider/pull/2139 6 | 7 | set -x 8 | 9 | WORKDIR=${HOME}/local 10 | CASKDIR=$WORKDIR/cask 11 | 12 | . tools/retry.sh 13 | 14 | update_elpa_keys() { 15 | mkdir -p $HOME/.emacs.d/elpa/gnupg || true 16 | chmod 700 $HOME/.emacs.d/elpa/gnupg 17 | GPG=gpg 18 | if which gpg2 ; then 19 | GPG=gpg2 20 | fi 21 | for i in 1 2 3 ; do 22 | if ${GPG} -q --homedir $HOME/.emacs.d/elpa/gnupg -k | grep 81E42C40 ; then 23 | return 0 24 | fi 25 | if [ $i -gt 1 ] ; then 26 | sleep 5 27 | fi 28 | ${GPG} --keyserver hkp://ipv4.pool.sks-keyservers.net --homedir $HOME/.emacs.d/elpa/gnupg --recv-keys 066DAFCB81E42C40 29 | done 30 | return 1 31 | } 32 | 33 | copy_keys() { 34 | mkdir -p $(cask package-directory) || true 35 | mkdir -p $HOME/.cask || true 36 | rsync -azSHe ssh $HOME/.cask $(dirname $(dirname $(dirname $(cask package-directory)))) 37 | rsync -azSHe ssh $HOME/.emacs.d/elpa/gnupg $(cask package-directory) 38 | } 39 | 40 | cask_upgrade_cask_or_reset() { 41 | cask upgrade-cask || { rm -rf $HOME/.emacs.d/.cask && false; } 42 | } 43 | 44 | cask_install_or_reset() { 45 | cask install 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-kernelinfo.el is free software: you can redistribute it and/or 10 | ;; modify it under the terms of the GNU General Public License as 11 | ;; published by the Free Software Foundation, either version 3 of the 12 | ;; License, or (at your option) any later version. 13 | 14 | ;; ein-kernelinfo.el is distributed in the hope that it will be 15 | ;; useful, but WITHOUT ANY WARRANTY; without even the implied warranty 16 | ;; of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-kernelinfo.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'eieio) 29 | (require 'ein-kernel) 30 | 31 | (defclass ein:kernelinfo () 32 | ((kernel 33 | :initarg :kernel :type ein:$kernel 34 | :documentation "Kernel instance.") 35 | (get-buffers 36 | :initarg :get-buffers 37 | :documentation "A packed function to get buffers associated 38 | with the kernel. The buffer local `default-directory' variable 39 | in these buffer will be synced with the kernel's cwd.") 40 | (hostname 41 | :initarg :hostname :type string 42 | :documentation "Host name of the machine where the kernel is running on.") 43 | (ccwd 44 | :initarg :ccwd :type string 45 | :documentation "cached CWD (last time checked CWD).")) 46 | :documentation "Info related (but unimportant) to kernel") 47 | 48 | (defun ein:kernelinfo-new (kernel get-buffers) 49 | "Make a new `ein:kernelinfo' instance based on KERNEL and GET-BUFFERS." 50 | (let ((kerinfo (make-instance 'ein:kernelinfo))) 51 | (setf (slot-value kerinfo 'kernel) kernel) 52 | (setf (slot-value kerinfo 'get-buffers) get-buffers) 53 | kerinfo)) 54 | (provide 'ein-kernelinfo) 55 | 56 | ;;; ein-kernelinfo.el ends here 57 | -------------------------------------------------------------------------------- /test/test-ein-worksheet-notebook.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'ein-notebook) 5 | (require 'ein-testing-notebook) 6 | 7 | 8 | ;;; Event handler 9 | 10 | (defun ein:testing-worksheet-set-dirty 11 | (pre-dirty value post-dirty fired-in) 12 | (with-current-buffer (ein:testing-make-notebook-with-outputs '(nil)) 13 | (when pre-dirty 14 | (ein:cell-goto (ein:worksheet-get-current-cell)) 15 | (insert "something")) 16 | (should (equal (ein:worksheet-modified-p ein:%worksheet%) pre-dirty)) 17 | (with-current-buffer (funcall fired-in) 18 | (let ((events (oref ein:%worksheet% :events)) 19 | (cell (ein:worksheet-get-current-cell))) 20 | (ein:events-trigger events 'set_dirty.Worksheet 21 | (list :cell cell :value value)))) 22 | (should (equal (ein:worksheet-modified-p ein:%worksheet%) post-dirty)))) 23 | 24 | (defun ein:testing-scratchsheet-buffer () 25 | (ein:worksheet-buffer (ein:notebook-scratchsheet-open ein:%notebook%))) 26 | 27 | (defmacro ein:testing-worksheet-set-dirty-deftest 28 | (pre-dirty value post-dirty &optional fired-in) 29 | (let ((name (intern (format "ein:worksheet-set-dirty/%s-to-%s-fired-in-%s" 30 | pre-dirty value 31 | (or fired-in "current-buffer")))) 32 | (fired-in-defun 33 | (cl-case fired-in 34 | (scratchsheet 'ein:testing-scratchsheet-buffer) 35 | (t 'current-buffer)))) 36 | `(ert-deftest ,name () 37 | (ein:testing-worksheet-set-dirty ,pre-dirty ,value ,post-dirty 38 | #',fired-in-defun)))) 39 | 40 | (ein:testing-worksheet-set-dirty-deftest t nil nil) 41 | (ein:testing-worksheet-set-dirty-deftest t t t ) 42 | (ein:testing-worksheet-set-dirty-deftest nil nil nil) 43 | (ein:testing-worksheet-set-dirty-deftest nil t t ) 44 | (ein:testing-worksheet-set-dirty-deftest t nil t scratchsheet) 45 | (ein:testing-worksheet-set-dirty-deftest t t t scratchsheet) 46 | (ein:testing-worksheet-set-dirty-deftest nil nil nil scratchsheet) 47 | (ein:testing-worksheet-set-dirty-deftest nil t nil scratchsheet) 48 | -------------------------------------------------------------------------------- /features/ipdb.feature: -------------------------------------------------------------------------------- 1 | @ipdb 2 | Scenario: ein-ipdb works... poorly 3 | Given new python notebook 4 | And I type "from IPython.core.debugger import set_trace" 5 | And I press "RET" 6 | And I type "for i in range(2):" 7 | And I press "RET" 8 | And I type " set_trace()" 9 | And I press "RET" 10 | And I type " print(i)" 11 | And I press "RET" 12 | And I press "C-c C-c" 13 | And I switch to buffer like "*ipdb: " 14 | And I wait for buffer process 15 | And I type "c" 16 | And I press "RET" 17 | And I wait for buffer to say "0\n" 18 | And I type "c" 19 | And I press "RET" 20 | And I wait for buffer to say "Finished" 21 | And eval "(kill-buffer)" 22 | 23 | @ipdb 24 | Scenario: ein-ipdb needs to not flail on C-d 25 | Given new python notebook 26 | And I type "from IPython.core.debugger import set_trace" 27 | And I press "RET" 28 | And I type "for i in range(2):" 29 | And I press "RET" 30 | And I type " set_trace()" 31 | And I press "RET" 32 | And I type " print(i)" 33 | And I press "RET" 34 | And I press "C-c C-c" 35 | And I switch to buffer like "*ipdb: " 36 | And I wait for buffer process 37 | And I type "c" 38 | And I press "RET" 39 | And I wait for buffer to say "0\n" 40 | And I press "C-d" 41 | And I wait for buffer to say "Finished" 42 | And eval "(kill-buffer)" 43 | 44 | @ipdb 45 | Scenario: kill buffer 46 | Given new python notebook 47 | And I type "from IPython.core.debugger import set_trace" 48 | And I press "RET" 49 | And I type "for i in range(2):" 50 | And I press "RET" 51 | And I type " set_trace()" 52 | And I press "RET" 53 | And I type " print(i)" 54 | And I press "RET" 55 | And I press "C-c C-c" 56 | And I switch to buffer like "*ipdb: " 57 | And I wait for buffer process 58 | And I type "c" 59 | And I press "RET" 60 | And I wait for buffer to say "0\n" 61 | And I switch to buffer like "Untitled" 62 | Then I should see "In [*]" 63 | And header says "Kernel busy..." 64 | And I switch to buffer like "*ipdb: " 65 | And I press "C-x k" 66 | And I switch to buffer like "Untitled" 67 | And I wait for buffer to not say "In [*]" 68 | And header does not say "Kernel busy..." 69 | -------------------------------------------------------------------------------- /lisp/ein-pytools.el: -------------------------------------------------------------------------------- 1 | ;;; ein-pytools.el --- Python tools build on top of kernel -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-pytools.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-pytools.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-pytools.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'ein-kernel) 29 | 30 | (defun ein:pytools-jump-to-source-command (&optional other-window) 31 | "Jump to the source code of the object at point. 32 | When the prefix argument ``C-u`` is given, open the source code 33 | in the other window. You can explicitly specify the object by 34 | selecting it." 35 | (interactive "P") 36 | (cl-letf (((symbol-function 'xref--prompt-p) #'ignore)) 37 | (if other-window 38 | (call-interactively #'xref-find-definitions-other-window) 39 | (call-interactively #'xref-find-definitions)))) 40 | 41 | (defun ein:pytools-jump-back-command (&optional _other-window) 42 | "Go back to the point where `ein:pytools-jump-to-source-command' 43 | is executed last time. When the prefix argument ``C-u`` is 44 | given, open the last point in the other window." 45 | (interactive "P") 46 | (call-interactively (if (fboundp 'xref-go-back) 47 | #'xref-go-back 48 | (symbol-function 'xref-pop-marker-stack)))) 49 | 50 | (define-obsolete-function-alias 51 | 'ein:pytools-eval-string-internal 52 | 'ein:shared-output-eval-string "0.1.2") 53 | 54 | (provide 'ein-pytools) 55 | 56 | ;;; ein-pytools.el ends here 57 | -------------------------------------------------------------------------------- /lisp/ein-kill-ring.el: -------------------------------------------------------------------------------- 1 | ;;; ein-kill-ring.el --- Kill-ring for cells -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-kill-ring.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-kill-ring.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-kill-ring.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; Stolen from simple.el. 25 | 26 | ;;; Code: 27 | 28 | (defvar ein:kill-ring nil) 29 | (defvar ein:kill-ring-yank-pointer nil) 30 | (defvar ein:kill-ring-max kill-ring-max) 31 | 32 | (defun ein:kill-new (obj) 33 | "Make OBJ the latest kill in the kill ring `ein:kill-ring'. 34 | Set `ein:kill-ring-yank-pointer' to point to it." 35 | (push obj ein:kill-ring) 36 | (if (> (length ein:kill-ring) ein:kill-ring-max) 37 | (setcdr (nthcdr (1- ein:kill-ring-max) ein:kill-ring) nil)) 38 | (setq ein:kill-ring-yank-pointer ein:kill-ring)) 39 | 40 | (defun ein:current-kill (n &optional do-not-move) 41 | "Rotate the yanking point by N places, and then return that kill. 42 | If optional arg DO-NOT-MOVE is non-nil, then don't actually 43 | move the yanking point; just return the Nth kill forward." 44 | (unless ein:kill-ring (error "Kill ring is empty")) 45 | (let ((ARGth-kill-element 46 | (nthcdr (mod (- n (length ein:kill-ring-yank-pointer)) 47 | (length ein:kill-ring)) 48 | ein:kill-ring))) 49 | (unless do-not-move 50 | (setq ein:kill-ring-yank-pointer ARGth-kill-element)) 51 | (car ARGth-kill-element))) 52 | 53 | (provide 'ein-kill-ring) 54 | 55 | ;;; ein-kill-ring.el ends here 56 | -------------------------------------------------------------------------------- /lisp/ein-events.el: -------------------------------------------------------------------------------- 1 | ;;; ein-events.el --- Event module -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-events.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-events.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-events.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'eieio) 29 | 30 | (require 'ein-core) 31 | (require 'ein-classes) 32 | (require 'ein-log) 33 | 34 | (defun ein:events-new () 35 | "Return a new event handler instance." 36 | (make-instance 'ein:events)) 37 | 38 | (defun ein:events-trigger (events event-type &optional data) 39 | "Trigger EVENT-TYPE and let event handler EVENTS handle that event." 40 | (ein:log 'debug "Event: %S" event-type) 41 | (aif (gethash event-type (slot-value events 'callbacks)) 42 | (mapc (lambda (cb-arg) (ein:funcall-packed cb-arg data)) it) 43 | (ein:log 'info "Unknown event: %S" event-type))) 44 | 45 | 46 | (cl-defmethod ein:events-on ((events ein:events) event-type 47 | callback &optional arg) 48 | "Set event trigger hook. 49 | 50 | When EVENT-TYPE is triggered on the event handler EVENTS, 51 | CALLBACK is called. CALLBACK must take two arguments: 52 | ARG as the first argument and DATA, which is passed via 53 | `ein:events-trigger', as the second." 54 | (cl-assert (symbolp event-type) t "%s not symbol" event-type) 55 | (let* ((table (slot-value events 'callbacks)) 56 | (cbs (gethash event-type table))) 57 | (push (cons callback arg) cbs) 58 | (puthash event-type cbs table))) 59 | 60 | 61 | (provide 'ein-events) 62 | 63 | ;;; ein-events.el ends here 64 | -------------------------------------------------------------------------------- /lisp/ein.el: -------------------------------------------------------------------------------- 1 | ;;; ein.el --- jupyter notebook client -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012-2019 The Authors of the Emacs IPython Notebook (EIN) 4 | 5 | ;; Authors: dickmao 6 | ;; John Miller 7 | ;; Takafumi Arakaki 8 | ;; Version: 0.17.1pre 9 | ;; 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")) 10 | ;; URL: https://github.com/dickmao/emacs-ipython-notebook 11 | ;; Keywords: jupyter, literate programming, reproducible research 12 | 13 | ;; This file is NOT part of GNU Emacs. 14 | 15 | ;; ein.el is free software: you can redistribute it and/or modify 16 | ;; it under the terms of the GNU General Public License as published by 17 | ;; the Free Software Foundation, either version 3 of the License, or 18 | ;; (at your option) any later version. 19 | 20 | ;; ein.el is distributed in the hope that it will be useful, 21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | ;; GNU General Public License for more details. 24 | 25 | ;; You should have received a copy of the GNU General Public License 26 | ;; along with ein.el. If not, see . 27 | 28 | ;;; Commentary: 29 | 30 | ;; Emacs IPython Notebook (EIN), despite its name, is a jupyter client for all 31 | ;; languages. It does not work under non-WSL Windows environments. 32 | ;; 33 | ;; As of 2023, EIN has been sunset for a number of years having been 34 | ;; unable to keep up with jupyter's web-first ecosystem. Even during 35 | ;; its heyday EIN never fully reconciled emac's monolithic buffer 36 | ;; architecture to the notebook's by-cell discretization, leaving 37 | ;; gaping functional holes like crippled undo. 38 | ;; 39 | ;; **As of 2025, a greenfield notebook implementation resides at,** 40 | ;; 41 | ;; https://github.com/commercial-emacs/xjupyter.git 42 | ;; 43 | ;; It features full-fledged undo and relies on "mode overlays" 44 | ;; instead of the complex and fragile polymode. 45 | 46 | ;;; Code: 47 | 48 | (when (boundp 'mouse-buffer-menu-mode-groups) 49 | (add-to-list 'mouse-buffer-menu-mode-groups 50 | '("^ein:" . "ein"))) 51 | 52 | (provide 'ein) 53 | 54 | ;;; ein.el ends here 55 | -------------------------------------------------------------------------------- /lisp/ein-node.el: -------------------------------------------------------------------------------- 1 | ;;; ein-node.el --- Structure to hold data in ewoc node -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-node.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-node.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-node.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'ewoc) 29 | (require 'ein-core) 30 | 31 | (cl-defstruct ein:$node 32 | path ; list of path 33 | data ; actual data 34 | class ; list 35 | ) 36 | 37 | (defun ein:node-new (path data &optional class &rest args) 38 | (apply #'make-ein:$node :path path :data data :class class args)) 39 | 40 | (defun ein:node-add-class (node &rest classes) 41 | (mapc (lambda (c) (cl-pushnew c (ein:$node-class node))) classes)) 42 | 43 | (defun ein:node-remove-class (node &rest classes) 44 | (let ((node-class (ein:$node-class node))) 45 | (mapc (lambda (c) (setq node-class (delq c node-class))) classes) 46 | (setf (ein:$node-class node) node-class))) 47 | 48 | (defun ein:node-has-class (node class) 49 | (memq class (ein:$node-class node))) 50 | 51 | (defun ein:node-filter (ewoc-node-list &rest args) 52 | (cl-loop for (key . class) in (ein:plist-iter args) 53 | do (setq ewoc-node-list 54 | (cl-loop for ewoc-node in ewoc-node-list 55 | for node = (ewoc-data ewoc-node) 56 | when (cl-case key 57 | (:is (ein:node-has-class node class)) 58 | (:not (not (ein:node-has-class node class))) 59 | (t (error "%s is not supported" key))) 60 | collect ewoc-node))) 61 | ewoc-node-list) 62 | 63 | (provide 'ein-node) 64 | 65 | ;;; ein-node.el ends here 66 | -------------------------------------------------------------------------------- /features/undo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "autoscroll": false, 8 | "collapsed": false, 9 | "ein.hycell": false, 10 | "ein.tags": "worksheet-0", 11 | "slideshow": { 12 | "slide_type": "-" 13 | } 14 | }, 15 | "outputs": [ 16 | { 17 | "name": "stdout", 18 | "output_type": "stream", 19 | "text": [ 20 | "hello\n" 21 | ] 22 | } 23 | ], 24 | "source": [ 25 | "print(\"hello\")" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": { 32 | "autoscroll": false, 33 | "collapsed": false, 34 | "ein.hycell": false, 35 | "ein.tags": "worksheet-0", 36 | "slideshow": { 37 | "slide_type": "-" 38 | } 39 | }, 40 | "outputs": [], 41 | "source": [ 42 | "abba\n", 43 | "\n", 44 | "\n", 45 | "abab\n", 46 | "baba" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 3, 52 | "metadata": { 53 | "autoscroll": false, 54 | "collapsed": false, 55 | "ein.hycell": false, 56 | "ein.tags": "worksheet-0", 57 | "slideshow": { 58 | "slide_type": "-" 59 | } 60 | }, 61 | "outputs": [ 62 | { 63 | "data": { 64 | "text/plain": [ 65 | "12.182493960703473" 66 | ] 67 | }, 68 | "execution_count": 3, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "import math\n", 75 | "math.exp(2.5)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": { 82 | "autoscroll": false, 83 | "collapsed": false, 84 | "ein.hycell": false, 85 | "ein.tags": "worksheet-0", 86 | "slideshow": { 87 | "slide_type": "-" 88 | } 89 | }, 90 | "outputs": [], 91 | "source": [ 92 | "abab\n", 93 | "abab" 94 | ] 95 | } 96 | ], 97 | "metadata": { 98 | "kernelspec": { 99 | "argv": [ 100 | "python", 101 | "-m", 102 | "ipykernel_launcher", 103 | "-f", 104 | "{connection_file}" 105 | ], 106 | "display_name": "Python 3", 107 | "env": null, 108 | "interrupt_mode": "signal", 109 | "language": "python", 110 | "metadata": null, 111 | "name": "python3" 112 | }, 113 | "name": "undo.ipynb" 114 | }, 115 | "nbformat": 4, 116 | "nbformat_minor": 2 117 | } 118 | -------------------------------------------------------------------------------- /test/ein-testing-cell.el: -------------------------------------------------------------------------------- 1 | ;;; ein-testing-cell.el --- Testing utilities for cell module -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012 Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-testing-cell.el is free software: you can redistribute it 10 | ;; and/or modify it under the terms of the GNU General Public License 11 | ;; as published by the Free Software Foundation, either version 3 of 12 | ;; the License, or (at your option) any later version. 13 | 14 | ;; ein-testing-cell.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-testing-cell.el. 21 | ;; If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; 26 | 27 | ;;; Code: 28 | 29 | (require 'json) 30 | 31 | (defvar ein:testing-example-svg "\ 32 | 33 | 35 | 36 | 37 | 38 | ") 39 | 40 | (defun ein:testing-codecell-pyout-data (text &optional prompt-number) 41 | "Create a plist representing JSON data for code-cell output. 42 | TEXT is a string and PROMPT-NUMBER is an integer." 43 | (list :output_type "pyout" 44 | :prompt_number (or prompt-number 0) 45 | :text text)) 46 | 47 | (defun ein:testing-codecell-data (&optional input prompt-number outputs) 48 | "Create a plist representing JSON data for code-type cell. 49 | To make OUTPUTS data, use `ein:testing-codecell-pyout-data'." 50 | (list :cell_type "code" 51 | :source (or input "") 52 | :language "python" 53 | :outputs outputs 54 | :metadata (list :collapsed json-false :autoscroll json-false) 55 | :execution_count prompt-number)) 56 | 57 | (defun ein:testing-textcell-data (&optional source cell-type) 58 | (list :cell_type cell-type 59 | :source (or source ""))) 60 | 61 | (defun ein:testing-markdowncell-data (&optional source) 62 | (ein:testing-textcell-data source "markdown")) 63 | 64 | (defun ein:testing-rawcell-data (&optional source) 65 | (ein:testing-textcell-data source "raw")) 66 | 67 | (defun ein:testing-htmlcell-data (&optional source) 68 | (ein:testing-textcell-data source "html")) 69 | 70 | (provide 'ein-testing-cell) 71 | 72 | ;;; ein-testing-cell.el ends here 73 | -------------------------------------------------------------------------------- /lisp/ein-file.el: -------------------------------------------------------------------------------- 1 | ;;; ein-file.el --- Editing files downloaded from jupyter -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2017- John M. Miller 4 | 5 | ;; Authors: Takafumi Arakaki 6 | ;; John M. Miller 7 | 8 | ;; This file is NOT part of GNU Emacs. 9 | 10 | ;; ein-file.el is free software: you can redistribute it and/or modify 11 | ;; it under the terms of the GNU General Public License as published by 12 | ;; the Free Software Foundation, either version 3 of the License, or 13 | ;; (at your option) any later version. 14 | 15 | ;; ein-file.el is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; You should have received a copy of the GNU General Public License 21 | ;; along with ein-notebooklist.el. If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | (defvar *ein:file-buffername-template* "'/ein:%s:%s") 26 | (ein:deflocal ein:content-file-buffer--content nil) 27 | 28 | ;; (push '("^ein:.*" . ein:content-file-handler) 29 | ;; file-name-handler-alist) 30 | 31 | (defun ein:file-buffer-name (urlport path) 32 | (format *ein:file-buffername-template* 33 | urlport 34 | path)) 35 | 36 | (defun ein:file-open (url-or-port path) 37 | (interactive (ein:notebooklist-parse-nbpath (ein:notebooklist-ask-path "file"))) 38 | (ein:content-query-contents url-or-port path #'ein:file-open-finish nil)) 39 | 40 | (defun ein:file-open-finish (content) 41 | (with-current-buffer (get-buffer-create (ein:file-buffer-name (ein:$content-url-or-port content) 42 | (ein:$content-path content))) 43 | (setq ein:content-file-buffer--content content) 44 | (let ((raw-content (ein:$content-raw-content content))) 45 | (if (eq system-type 'windows-nt) 46 | (insert (decode-coding-string raw-content 'utf-8)) 47 | (insert raw-content))) 48 | (set-visited-file-name (buffer-name)) 49 | (set-auto-mode) 50 | (add-hook 'write-contents-functions 'ein:content-file-save nil t) ;; FIXME Brittle, will not work 51 | ;; if user changes major mode. 52 | (ein:log 'verbose "Opened file %s" (ein:$content-name content)) 53 | (set-buffer-modified-p nil) 54 | (goto-char (point-min)) 55 | (pop-to-buffer (buffer-name)))) 56 | 57 | (defun ein:content-file-save () 58 | (setf (ein:$content-raw-content ein:content-file-buffer--content) (buffer-string)) 59 | (ein:content-save ein:content-file-buffer--content) 60 | (set-buffer-modified-p nil) 61 | t) 62 | 63 | (provide 'ein-file) 64 | -------------------------------------------------------------------------------- /lisp/ein-pager.el: -------------------------------------------------------------------------------- 1 | ;;; ein-pager.el --- Pager module -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-pager.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-pager.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-pager.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'ansi-color) 29 | 30 | (require 'ein-core) 31 | (require 'ein-events) 32 | (require 'view) 33 | 34 | ;; FIXME: Make a class with `:get-notebook-name' slot like `ein:worksheet' 35 | 36 | (declare-function ess-help-underline "ess-help") 37 | 38 | (defun ein:pager-new (name events) 39 | ;; currently pager = name. 40 | (ein:pager-bind-events name events) 41 | name) 42 | 43 | (defun ein:pager-bind-events (pager events) 44 | "Bind events related to PAGER to the event handler EVENTS." 45 | (ein:events-on events 46 | 'open_with_text.Pager 47 | #'ein:pager--open-with-text 48 | pager)) 49 | 50 | (defun ein:pager--open-with-text (pager data) 51 | (let ((text (plist-get data :text))) 52 | (unless (equal (ein:trim text) "") 53 | (ein:pager-clear pager) 54 | (ein:pager-expand pager) 55 | (ein:pager-append-text pager text)))) 56 | 57 | (defun ein:pager-clear (pager) 58 | (ein:with-read-only-buffer (get-buffer-create pager) 59 | (erase-buffer))) 60 | 61 | (defun ein:pager-expand (pager) 62 | (pop-to-buffer (get-buffer-create pager)) 63 | (goto-char (point-min))) 64 | 65 | (defun ein:pager-append-text (pager text) 66 | (ein:with-read-only-buffer (get-buffer-create pager) 67 | (insert (ansi-color-apply text)) 68 | (if (featurep 'ess-help) 69 | (ess-help-underline)) 70 | (unless (eql 'ein:pager-mode major-mode) 71 | (ein:pager-mode)))) 72 | 73 | ;; FIXME: this should be automatically called when opening pager. 74 | (defun ein:pager-goto-docstring-bset-loc () 75 | "Goto the best location of the documentation." 76 | (interactive) 77 | (goto-char (point-min)) 78 | (search-forward-regexp "^Docstring:") 79 | (beginning-of-line 0) 80 | (recenter 0)) 81 | 82 | (defvar ein:pager-mode-map 83 | (let ((map (make-sparse-keymap))) 84 | (define-key map "\C-c\C-b" 'ein:pager-goto-docstring-bset-loc) 85 | map) 86 | "Keymap for ein:pager-mode.") 87 | 88 | (define-derived-mode ein:pager-mode view-mode "ein:pager" 89 | "IPython notebook pager mode. 90 | Commands: 91 | \\{ein:pager-mode-map}" 92 | (setq-local view-no-disable-on-exit t) 93 | (font-lock-mode)) 94 | 95 | 96 | (provide 'ein-pager) 97 | 98 | ;;; ein-pager.el ends here 99 | -------------------------------------------------------------------------------- /lisp/ein-ipynb-mode.el: -------------------------------------------------------------------------------- 1 | ;;; ein-ipynb-mode.el --- A simple mode for ipynb file -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012 Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-ipynb-mode.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-ipynb-mode.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-ipynb-mode.el. 21 | ;; If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; 26 | 27 | ;;; Code: 28 | 29 | (require 'ein-process) 30 | (require 'js) 31 | 32 | ;;;###autoload 33 | (define-derived-mode ein:ipynb-mode js-mode "ein:ipynb" 34 | "A simple mode for ipynb file. 35 | 36 | \\{ein:ipynb-mode-map} 37 | " 38 | :group 'ein 39 | :after-hook 40 | (let* ((filename (file-name-nondirectory buffer-file-truename)) 41 | (remote-filename (concat (file-name-as-directory "run-remote") filename))) 42 | ;; fragile hack to refresh s3fuse 43 | (call-process "cat" nil nil nil remote-filename) 44 | (when (and (file-readable-p remote-filename) 45 | (file-newer-than-file-p remote-filename filename) 46 | (prog1 47 | (let ((inhibit-quit t)) 48 | (prog1 49 | (with-local-quit 50 | (y-or-n-p "Corresponding run-remote is newer. Replace? (will first backup) ")) 51 | (setq quit-flag nil))) 52 | (message ""))) 53 | (if-let ((make-backup-files t) 54 | (where-to (funcall make-backup-file-name-function buffer-file-name))) 55 | (let* (backup-inhibited 56 | (orig-hooks find-file-hook) 57 | (reassure (lambda () 58 | (message "Backed up to %s" where-to) 59 | (setq find-file-hook orig-hooks)))) 60 | (backup-buffer) 61 | (copy-file remote-filename filename t t) 62 | (add-hook 'find-file-hook reassure nil) 63 | (find-file-noselect filename t)) 64 | (message "Backup failed. Not replaced"))))) 65 | 66 | (let ((map ein:ipynb-mode-map)) 67 | (set-keymap-parent map js-mode-map) 68 | (define-key map "\C-c\C-z" 'ein:process-find-file-callback) 69 | (define-key map "\C-c\C-o" 'ein:process-find-file-callback) 70 | (define-key map "\C-c\C-r" 'ein:gat-run-remote) 71 | (easy-menu-define ein:ipynb-menu map "EIN IPyNB Mode Menu" 72 | `("EIN IPyNB File" 73 | ,@(ein:generate-menu 74 | '(("Open notebook" ein:process-find-file-callback)))))) 75 | 76 | ;;;###autoload 77 | (add-to-list 'auto-mode-alist '("\\.ipynb\\'" . ein:ipynb-mode)) 78 | 79 | (provide 'ein-ipynb-mode) 80 | 81 | ;;; ein-ipynb-mode.el ends here 82 | -------------------------------------------------------------------------------- /test/test-ein-kernel.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | 3 | (require 'ert) 4 | 5 | (require 'ein-kernel) 6 | (require 'ein-testing-notebook) 7 | 8 | (defun eintest:kernel-new (port) 9 | (ein:kernel-new port "" nil "/api/kernels" 10 | (get-buffer-create "*eintest: dummy for kernel test*"))) 11 | 12 | (ert-deftest ein:kernel-restart-check-url () 13 | (let* ((notebook (ein:notebook-new ein:testing-notebook-dummy-url "" nil)) 14 | (kernel (eintest:kernel-new 8888)) 15 | (kernel-id "KERNEL-ID") 16 | (desired-url "http://127.0.0.1:8888/api/sessions/KERNEL-ID") 17 | (dummy-response (make-request-response)) 18 | got-url) 19 | (setf (ein:$notebook-kernel notebook) kernel) 20 | (cl-letf (((symbol-function 'request) 21 | (lambda (url &rest _ignore) (setq got-url url) dummy-response)) 22 | ((symbol-function 'set-process-query-on-exit-flag) #'ignore) 23 | ((symbol-function 'ein:kernel-stop-channels) #'ignore) 24 | ((symbol-function 'ein:websocket) (lambda (&rest _ignore) (make-ein:$websocket :ws nil :kernel kernel :closed-by-client nil))) 25 | ((symbol-function 'ein:events-trigger) #'ignore) 26 | ((symbol-function 'ein:get-notebook-or-error) (lambda () (ein:get-notebook)))) 27 | (ein:kernel-retrieve-session--success 28 | kernel nil :data (list :ws_url "ws://127.0.0.1:8888" :id kernel-id)) 29 | (ein:kernel-restart-session (ein:$notebook-kernel notebook)) 30 | (should (equal got-url desired-url))))) 31 | 32 | (ert-deftest ein:kernel-interrupt-check-url () 33 | (let* ((kernel (eintest:kernel-new 8888)) 34 | (kernel-id "KERNEL-ID") 35 | (desired-url "http://127.0.0.1:8888/api/kernels/KERNEL-ID/interrupt") 36 | (dummy-response (make-request-response)) 37 | got-url) 38 | 39 | (cl-letf (((symbol-function 'request) 40 | (lambda (url &rest _ignore) (setq got-url url) dummy-response)) 41 | ((symbol-function 'set-process-query-on-exit-flag) #'ignore) 42 | ((symbol-function 'ein:kernel-stop-channels) #'ignore) 43 | ((symbol-function 'ein:websocket) (lambda (&rest _ignore) (make-ein:$websocket :ws nil :kernel kernel :closed-by-client nil))) 44 | ((symbol-function 'ein:websocket-open-p) (lambda (&rest _ignore) t))) 45 | (ein:kernel-retrieve-session--success 46 | kernel nil :data (list :ws_url "ws://127.0.0.1:8888" :id kernel-id)) 47 | (ein:kernel-interrupt kernel) 48 | (should (equal got-url desired-url))))) 49 | 50 | (ert-deftest ein:kernel-kill-check-url () 51 | (let* ((kernel (eintest:kernel-new 8888)) 52 | (kernel-id "KERNEL-ID") 53 | (desired-url "http://127.0.0.1:8888/api/sessions/KERNEL-ID") 54 | (dummy-response (make-request-response)) 55 | got-url) 56 | (cl-letf (((symbol-function 'request) 57 | (lambda (url &rest _ignore) (setq got-url url) dummy-response)) 58 | ((symbol-function 'set-process-query-on-exit-flag) #'ignore) 59 | ((symbol-function 'ein:kernel-stop-channels) #'ignore) 60 | ((symbol-function 'ein:websocket) 61 | (lambda (&rest _ignore) (make-ein:$websocket :ws nil :kernel kernel 62 | :closed-by-client nil))) 63 | ((symbol-function 'ein:websocket-open-p) (lambda (&rest _ignore) t))) 64 | (ein:kernel-retrieve-session--success 65 | kernel nil :data (list :ws_url "ws://127.0.0.1:8888" :id kernel-id)) 66 | (ein:kernel-delete-session nil :kernel kernel)) 67 | (should (equal got-url desired-url)))) 68 | -------------------------------------------------------------------------------- /test/test-ein-notification.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'ein-notification) 5 | 6 | (defun ein:testing-notification-tab-mock () 7 | (make-instance 'ein:notification-tab 8 | :get-list (lambda () '(a b c)) 9 | :get-current (lambda () 'a))) 10 | 11 | (ert-deftest ein:header-line-normal () 12 | (let* ((ein:%notification% (ein:notification)) 13 | (kernel (oref ein:%notification% :kernel))) 14 | (ignore kernel) 15 | (oset ein:%notification% :tab (ein:testing-notification-tab-mock)) 16 | (should (string-prefix-p "IP[y]: " (ein:header-line))))) 17 | 18 | (ert-deftest ein:header-line-kernel-status-busy () 19 | (let* ((ein:%notification% (ein:notification)) 20 | (kernel (oref ein:%notification% :kernel))) 21 | (oset ein:%notification% :tab (ein:testing-notification-tab-mock)) 22 | (ein:notification-status-set kernel 23 | 'status_busy.Kernel) 24 | (should (string-prefix-p "IP[y]: Kernel busy..." 25 | (ein:header-line))))) 26 | 27 | (ert-deftest ein:header-line-notebook-status-busy () 28 | (let* ((ein:%notification% (ein:notification)) 29 | (notebook (oref ein:%notification% :notebook))) 30 | (oset ein:%notification% :tab (ein:testing-notification-tab-mock)) 31 | (ein:notification-status-set notebook 32 | 'notebook_saved.Notebook) 33 | (should (string-prefix-p "IP[y]: Notebook saved" 34 | (ein:header-line))))) 35 | 36 | (ert-deftest ein:header-line-notebook-complex () 37 | (let* ((ein:%notification% (ein:notification)) 38 | (kernel (oref ein:%notification% :kernel)) 39 | (notebook (oref ein:%notification% :notebook))) 40 | (oset ein:%notification% :tab (ein:testing-notification-tab-mock)) 41 | (ein:notification-status-set kernel 42 | 'status_dead.Kernel) 43 | (ein:notification-status-set notebook 44 | 'notebook_saving.Notebook) 45 | (should (string-prefix-p 46 | (concat "IP[y]: Saving notebook... | " 47 | (substitute-command-keys "Kernel requires restart \\\\[ein:notebook-restart-session-command-km]")) 48 | (ein:header-line))))) 49 | 50 | (ert-deftest ein:notification-and-events () 51 | (let* ((notification (ein:notification)) 52 | (kernel (oref notification :kernel)) 53 | (notebook (oref notification :notebook)) 54 | (events (ein:events-new)) 55 | (event-symbols 56 | '(notebook_saved.Notebook 57 | notebook_saving.Notebook 58 | notebook_save_failed.Notebook 59 | execution_count.Kernel 60 | status_idle.Kernel 61 | status_busy.Kernel 62 | status_restarting.Kernel 63 | status_restarted.Kernel 64 | status_dead.Kernel 65 | status_reconnecting.Kernel 66 | status_reconnected.Kernel 67 | status_disconnected.Kernel 68 | )) 69 | (callbacks (oref events :callbacks))) 70 | (ein:notification-bind-events notification events) 71 | (mapc (lambda (s) (should (gethash s callbacks))) event-symbols) 72 | (should (= (hash-table-count callbacks) (length event-symbols))) 73 | (should (equal (oref kernel :status) nil)) 74 | (cl-loop for et in (mapcar #'car (oref kernel :s2m)) 75 | do (ein:events-trigger events et) 76 | do (should (equal (oref kernel :status) et)) 77 | do (should (equal (oref notebook :status) nil))) 78 | (cl-loop for et in (mapcar #'car (oref notebook :s2m)) 79 | do (ein:events-trigger events et) 80 | do (should (equal (oref kernel :status) 'status_disconnected.Kernel)) 81 | do (should (equal (oref notebook :status) et))))) 82 | -------------------------------------------------------------------------------- /test/test-ein-node.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'ein-node) 5 | 6 | (defun ein:testing-node-dummy-ewco-node (data) 7 | `[nil nil ,data]) 8 | 9 | (defun ein:testing-node-ewoc-data (ewoc-node) 10 | (ein:$node-data (ewoc-data ewoc-node))) 11 | 12 | (ert-deftest ein:testing-node-dummy-ewco-node () 13 | (let* ((obj "some-object") 14 | (ewoc-node (ein:testing-node-dummy-ewco-node obj))) 15 | (should (eq (ewoc-data ewoc-node) obj)))) 16 | 17 | (ert-deftest ein:node-filter-is () 18 | (let ((en-list (mapcar 19 | #'ein:testing-node-dummy-ewco-node 20 | (list (ein:node-new nil "s" '(spam sag)) 21 | (ein:node-new nil "p" '(spam)) 22 | (ein:node-new nil "e" '(egg)) 23 | (ein:node-new nil "a" '(spam sag)) 24 | (ein:node-new nil "g" '(egg sag)) 25 | (ein:node-new nil "m" '(spam)) 26 | (ein:node-new nil "g" '(egg)))))) 27 | (should (equal (mapcar #'ein:testing-node-ewoc-data 28 | (ein:node-filter en-list :is 'spam)) 29 | '("s" "p" "a" "m"))) 30 | (should (equal (mapcar #'ein:testing-node-ewoc-data 31 | (ein:node-filter en-list :is 'egg)) 32 | '("e" "g" "g"))) 33 | (should (equal (mapcar #'ein:testing-node-ewoc-data 34 | (ein:node-filter en-list :is 'sag)) 35 | '("s" "a" "g"))))) 36 | 37 | (ert-deftest ein:node-filter-not () 38 | (let ((en-list (mapcar 39 | #'ein:testing-node-dummy-ewco-node 40 | (list (ein:node-new nil "s" '(spam sag)) 41 | (ein:node-new nil "p" '(spam)) 42 | (ein:node-new nil "e" '(egg)) 43 | (ein:node-new nil "a" '(spam sag)) 44 | (ein:node-new nil "g" '(egg sag)) 45 | (ein:node-new nil "m" '(spam)) 46 | (ein:node-new nil "g" '(egg)))))) 47 | (should (equal (mapcar #'ein:testing-node-ewoc-data 48 | (ein:node-filter en-list :not 'spam)) 49 | '("e" "g" "g"))) 50 | (should (equal (mapcar #'ein:testing-node-ewoc-data 51 | (ein:node-filter en-list :not 'egg)) 52 | '("s" "p" "a" "m"))) 53 | (should (equal (mapcar #'ein:testing-node-ewoc-data 54 | (ein:node-filter en-list :not 'sag)) 55 | '("p" "e" "m" "g"))))) 56 | 57 | (ert-deftest ein:node-filter-is-and-not () 58 | (let ((en-list (mapcar 59 | #'ein:testing-node-dummy-ewco-node 60 | (list (ein:node-new nil "s" '(spam sag)) 61 | (ein:node-new nil "p" '(spam)) 62 | (ein:node-new nil "e" '(egg)) 63 | (ein:node-new nil "a" '(spam sag)) 64 | (ein:node-new nil "g" '(egg sag)) 65 | (ein:node-new nil "m" '(spam)) 66 | (ein:node-new nil "g" '(egg)))))) 67 | (should (equal (mapcar #'ein:testing-node-ewoc-data 68 | (ein:node-filter en-list :not 'spam :is 'sag)) 69 | '("g"))) 70 | (should (equal (mapcar #'ein:testing-node-ewoc-data 71 | (ein:node-filter en-list :is 'sag :not 'spam)) 72 | '("g"))) 73 | (should (equal (mapcar #'ein:testing-node-ewoc-data 74 | (ein:node-filter en-list :is 'spam :is 'sag)) 75 | '("s" "a"))) 76 | (should (equal (mapcar #'ein:testing-node-ewoc-data 77 | (ein:node-filter en-list :is 'sag :not 'spam 78 | :not 'not-existing)) 79 | '("g"))) 80 | (should (equal (mapcar #'ein:testing-node-ewoc-data 81 | (ein:node-filter en-list :is 'sag :is 'spam)) 82 | '("s" "a"))))) 83 | -------------------------------------------------------------------------------- /lisp/ein-log.el: -------------------------------------------------------------------------------- 1 | ;;; ein-log.el --- Logging module for ein.el -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-log.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-log.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-log.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'ein-core) 29 | 30 | (defvar ein:log-all-buffer-name "*ein:log-all*") 31 | 32 | (defvar ein:log-level-def 33 | '(;; debugging 34 | (blather . 60) (trace . 50) (debug . 40) 35 | ;; information 36 | (verbose . 30) (info . 20) 37 | ;; errors 38 | (warn . 10) (error . 0)) 39 | "Named logging levels.") 40 | ;; Some names are stolen from supervisord (http://supervisord.org/logging.html) 41 | 42 | (defvar ein:log-level 30) 43 | (defvar ein:log-message-level 20) 44 | 45 | (defvar ein:log-print-length 10 "`print-length' for `ein:log'") 46 | (defvar ein:log-print-level 1 "`print-level' for `ein:log'") 47 | (defvar ein:log-max-string 1000) 48 | 49 | (defun ein:log-set-level (level) 50 | (setq ein:log-level (ein:log-level-name-to-int level))) 51 | 52 | (defun ein:log-set-message-level (level) 53 | (setq ein:log-message-level (ein:log-level-name-to-int level))) 54 | 55 | (defun ein:log-level-int-to-name (int) 56 | (cl-loop for (n . i) in ein:log-level-def 57 | when (>= int i) 58 | return n 59 | finally 'error)) 60 | 61 | (defun ein:log-level-name-to-int (name) 62 | (cdr (assq name ein:log-level-def))) 63 | 64 | (defsubst ein:log-strip-timestamp (msg) 65 | (replace-regexp-in-string "^[0-9: ]+" "" msg)) 66 | 67 | (defun ein:log-wrapper (level func) 68 | (setq level (ein:log-level-name-to-int level)) 69 | (when (<= level ein:log-level) 70 | (let* ((levname (ein:log-level-int-to-name level)) 71 | (print-level ein:log-print-level) 72 | (print-length ein:log-print-length) 73 | (msg (format "%s: [%s] %s" (format-time-string "%H:%M:%S:%3N") levname (funcall func))) 74 | (orig-buffer (current-buffer))) 75 | (if (and ein:log-max-string 76 | (> (length msg) ein:log-max-string)) 77 | (setq msg (substring msg 0 ein:log-max-string))) 78 | (ein:with-read-only-buffer (get-buffer-create ein:log-all-buffer-name) 79 | (goto-char (point-max)) 80 | (insert msg (format " @%S" orig-buffer) "\n")) 81 | (when (<= level ein:log-message-level) 82 | (message "ein: %s" (ein:log-strip-timestamp msg)))))) 83 | 84 | (make-obsolete-variable 'ein:debug nil "0.17.0") 85 | 86 | (defmacro ein:log (level string &rest args) 87 | (declare (indent 1)) 88 | `(ein:log-wrapper ,level (lambda () (format ,string ,@args)))) 89 | 90 | (defsubst ein:debug-p () 91 | "Set to non-`nil' to raise errors instead of suppressing it. 92 | Change the behavior of `ein:log-ignore-errors'." 93 | (>= ein:log-level (alist-get 'debug ein:log-level-def))) 94 | 95 | (defun ein:log-pop-to-ws-buffer () 96 | (interactive) 97 | (-if-let* ((kernel (ein:get-kernel--notebook)) 98 | (websocket (ein:$kernel-websocket kernel))) 99 | (pop-to-buffer 100 | (websocket-get-debug-buffer-create 101 | (ein:$websocket-ws websocket))) 102 | (message "Must be run from notebook buffer"))) 103 | 104 | (defun ein:log-pop-to-request-buffer () 105 | (interactive) 106 | (aif (get-buffer request-log-buffer-name) 107 | (pop-to-buffer it) 108 | (message "No buffer %s" request-log-buffer-name))) 109 | 110 | (defun ein:log-pop-to-all-buffer () 111 | (interactive) 112 | (pop-to-buffer (get-buffer-create ein:log-all-buffer-name))) 113 | 114 | (provide 'ein-log) 115 | 116 | ;;; ein-log.el ends here 117 | -------------------------------------------------------------------------------- /lisp/ein-websocket.el: -------------------------------------------------------------------------------- 1 | ;;; ein-websocket.el --- Wrapper of websocket.el -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-websocket.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-websocket.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-websocket.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'websocket) 29 | (require 'ein-core) 30 | (require 'ein-classes) 31 | (require 'url-cookie) 32 | (require 'request) 33 | 34 | (defun ein:websocket-store-cookie (c host-port url-filename securep) 35 | (url-cookie-store (car c) (cdr c) nil host-port url-filename securep)) 36 | 37 | (defun ein:maybe-get-jhconn-user (url) 38 | (let ((paths (cl-rest (split-string (url-filename (url-generic-parse-url url)) "/")))) 39 | (when (string= (cl-first paths) "user") 40 | (list (format "/%s/%s/" (cl-first paths) (cl-second paths)))))) 41 | 42 | (defun ein:websocket--prepare-cookies (url) 43 | "Websocket gets its cookies using the url-cookie API, so we need 44 | to transcribe any cookies stored in `request-cookie-alist' during 45 | earlier calls to `request' (request.el)." 46 | (let* ((parsed-url (url-generic-parse-url url)) 47 | (host-port (format "%s:%s" (url-host parsed-url) (url-port parsed-url))) 48 | (base-url (file-name-as-directory (url-filename parsed-url))) 49 | (securep (string-match "^wss://" url)) 50 | (read-cookies-func (lambda (path) 51 | (request-cookie-alist 52 | (url-host parsed-url) path securep))) 53 | (cookies (cl-loop 54 | repeat 4 55 | for cand = (cl-mapcan read-cookies-func 56 | `("/" 57 | "/hub/" 58 | ,base-url 59 | ,@(ein:maybe-get-jhconn-user url))) 60 | until (cl-some (lambda (x) (string= "_xsrf" (car x))) cand) 61 | do (ein:log 'info 62 | "ein:websocket--prepare-cookies: no _xsrf among %s, retrying." 63 | cand) 64 | do (sleep-for 0 300) 65 | finally return cand))) 66 | (dolist (c cookies) 67 | (ein:websocket-store-cookie 68 | c host-port (car (url-path-and-query parsed-url)) securep)))) 69 | 70 | (defun ein:websocket (url kernel on-message on-close on-open) 71 | (ein:websocket--prepare-cookies (ein:$kernel-ws-url kernel)) 72 | (let* ((ws (websocket-open url 73 | :on-open on-open 74 | :on-message on-message 75 | :on-close on-close 76 | :on-error (lambda (ws action err) 77 | (ein:log 'info "WS action [%s] %s (%s)" 78 | err action (websocket-url ws))))) 79 | (websocket (make-ein:$websocket :ws ws :kernel kernel :closed-by-client nil))) 80 | (setf (websocket-client-data ws) websocket) 81 | websocket)) 82 | 83 | (defun ein:websocket-open-p (websocket) 84 | (eql (websocket-ready-state (ein:$websocket-ws websocket)) 'open)) 85 | 86 | 87 | (defun ein:websocket-send (websocket text) 88 | ;; (ein:log 'info "WS: Sent message %s" text) 89 | (condition-case-unless-debug err 90 | (websocket-send-text (ein:$websocket-ws websocket) text) 91 | (error (message "Error %s on sending websocket message %s." err text)))) 92 | 93 | 94 | (defun ein:websocket-close (websocket) 95 | (setf (ein:$websocket-closed-by-client websocket) t) 96 | (websocket-close (ein:$websocket-ws websocket))) 97 | 98 | 99 | (defun ein:websocket-send-shell-channel (kernel msg) 100 | (cond ((= (ein:$kernel-api-version kernel) 2) 101 | (ein:websocket-send 102 | (ein:$kernel-shell-channel kernel) 103 | (ein:json-encode msg))) 104 | ((>= (ein:$kernel-api-version kernel) 3) 105 | (ein:websocket-send 106 | (ein:$kernel-websocket kernel) 107 | (ein:json-encode (plist-put msg :channel "shell")))))) 108 | 109 | (defun ein:websocket-send-stdin-channel (kernel msg) 110 | (cond ((= (ein:$kernel-api-version kernel) 2) 111 | (ein:log 'warn "Stdin messages only supported with IPython 3.")) 112 | ((>= (ein:$kernel-api-version kernel) 3) 113 | (ein:websocket-send 114 | (ein:$kernel-websocket kernel) 115 | (ein:json-encode (plist-put msg :channel "stdin")))))) 116 | 117 | (provide 'ein-websocket) 118 | 119 | ;;; ein-websocket.el ends here 120 | -------------------------------------------------------------------------------- /lisp/ein-ipdb.el: -------------------------------------------------------------------------------- 1 | ;;; ein-ipdb.el --- Support ipython debugger (ipdb) -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2015 - John Miller 4 | 5 | ;; Author: John Miller 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-ipdb.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-ipdb.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-kernel.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'cl-lib) 29 | 30 | (defvar *ein:ipdb-sessions* (make-hash-table) 31 | "Kernel Id to ein:$ipdb-session.") 32 | 33 | (declare-function ein:kernel--get-msg "ein-kernel") 34 | 35 | (cl-defstruct ein:$ipdb-session buffer kernel prompt notebook) 36 | 37 | (defun ein:ipdb-get-session (kernel) 38 | (gethash (ein:$kernel-kernel-id kernel) *ein:ipdb-sessions*)) 39 | 40 | (defun ein:ipdb-start-session (kernel prompt notebook) 41 | (let* ((buffer (get-buffer-create 42 | (format "*ipdb: %s*" 43 | (ein:$kernel-kernel-id kernel)))) 44 | (session (make-ein:$ipdb-session :buffer buffer 45 | :kernel kernel 46 | :prompt prompt 47 | :notebook notebook))) 48 | (puthash (ein:$kernel-kernel-id kernel) session *ein:ipdb-sessions*) 49 | (with-current-buffer buffer 50 | (kill-all-local-variables) 51 | (add-hook 'kill-buffer-hook 52 | (apply-partially #'ein:ipdb-quit-session session) nil t) 53 | (ein:ipdb-mode) 54 | (setq comint-use-prompt-regexp t) 55 | (setq comint-prompt-regexp (concat "^" (regexp-quote prompt))) 56 | (setq comint-input-sender (apply-partially #'ein:ipdb-input-sender session)) 57 | (setq comint-prompt-read-only t) 58 | (set (make-local-variable 'comint-output-filter-functions) 59 | '(ansi-color-process-output)) 60 | (let ((proc (start-process "ein:ipdb" buffer "cat")) 61 | (sentinel (lambda (process _event) 62 | (when (memq (process-status process) '(exit signal)) 63 | (ein:ipdb-cleanup-session session))))) 64 | (set-process-query-on-exit-flag proc nil) 65 | (set-process-sentinel proc sentinel) 66 | (set-marker (process-mark proc) (point)) 67 | (comint-output-filter proc (concat "\n" (ein:$ipdb-session-prompt session))))) 68 | (pop-to-buffer buffer))) 69 | 70 | (defun ein:ipdb-quit-session (session) 71 | (let* ((kernel (ein:$ipdb-session-kernel session)) 72 | (msg (ein:kernel--get-msg kernel "input_reply" (list :value "exit")))) 73 | (ein:websocket-send-stdin-channel kernel msg))) 74 | 75 | (defun ein:ipdb-stop-session (session) 76 | (awhen (get-buffer-process (ein:$ipdb-session-buffer session)) 77 | (when (process-live-p it) 78 | (kill-process it)))) 79 | 80 | (defun ein:ipdb-cleanup-session (session) 81 | (let ((kernel (ein:$ipdb-session-kernel session)) 82 | (notebook (ein:$ipdb-session-notebook session)) 83 | (buffer (ein:$ipdb-session-buffer session))) 84 | (remhash (ein:$kernel-kernel-id kernel) *ein:ipdb-sessions*) 85 | (when (buffer-live-p buffer) 86 | (with-current-buffer buffer 87 | (insert "\nFinished\n"))) 88 | (awhen (ein:notebook-buffer notebook) 89 | (when (buffer-live-p it) 90 | (pop-to-buffer it))))) 91 | 92 | (defun ein:ipdb--handle-iopub-reply (kernel packet) 93 | (cl-destructuring-bind 94 | (&key content &allow-other-keys) 95 | (ein:json-read-from-string packet) 96 | (-when-let* ((session (ein:ipdb-get-session kernel)) 97 | (buffer (ein:$ipdb-session-buffer session)) 98 | (prompt (ein:$ipdb-session-prompt session)) 99 | (proc (get-buffer-process buffer)) 100 | (proc-live-p (process-live-p proc))) 101 | (let ((text (plist-get content :text)) 102 | (ename (plist-get content :ename))) 103 | (when (stringp text) 104 | (comint-output-filter proc text)) 105 | (if (and (stringp ename) (string-match-p "bdbquit" ename)) 106 | (ein:ipdb-stop-session session) 107 | (comint-output-filter proc prompt)))))) 108 | 109 | (defun ein:ipdb-input-sender (session proc input) 110 | ;; in case of eof, comint-input-sender-no-newline is t 111 | (if comint-input-sender-no-newline 112 | (ein:ipdb-quit-session session) 113 | (when (process-live-p proc) 114 | (with-current-buffer (process-buffer proc) 115 | (let* ((buffer-read-only nil) 116 | (kernel (ein:$ipdb-session-kernel session)) 117 | (content (list :value input)) 118 | (msg (ein:kernel--get-msg kernel "input_reply" content))) 119 | (ein:websocket-send-stdin-channel kernel msg)))))) 120 | 121 | (define-derived-mode ein:ipdb-mode comint-mode "ein:debugger" 122 | "Run an EIN debug session. 123 | 124 | \\") 125 | 126 | (provide 'ein-ipdb) 127 | -------------------------------------------------------------------------------- /README.in.rst: -------------------------------------------------------------------------------- 1 | ========================================================== 2 | EIN -- Emacs IPython Notebook |build-status| |melpa-dev| 3 | ========================================================== 4 | 5 | .. image:: https://github.com/dickmao/emacs-ipython-notebook/blob/master/thumbnail.png 6 | :target: https://youtu.be/8VzWc9QeOxE 7 | :alt: Kaggle Notebooks in AWS 8 | 9 | .. COMMENTARY (see Makefile) 10 | 11 | .. |build-status| 12 | image:: https://github.com/millejoh/emacs-ipython-notebook/workflows/CI/badge.svg 13 | :target: https://github.com/millejoh/emacs-ipython-notebook/actions 14 | :alt: Build Status 15 | .. |melpa-dev| 16 | image:: https://melpa.org/packages/ein-badge.svg 17 | :target: http://melpa.org/#/ein 18 | :alt: MELPA current version 19 | .. _Jupyter: http://jupyter.org 20 | .. _Babel: https://orgmode.org/worg/org-contrib/babel/intro.html 21 | .. _Org: https://orgmode.org 22 | .. _[tkf]: http://tkf.github.io 23 | .. _[gregsexton]: https://github.com/gregsexton/ob-ipython 24 | 25 | Install 26 | ======= 27 | As described in `Getting started`_, ensure melpa's whereabouts in ``init.el`` or ``.emacs``:: 28 | 29 | (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")) 30 | 31 | Then 32 | 33 | :: 34 | 35 | M-x package-install RET ein RET 36 | 37 | Alternatively, directly clone this repo and ``make install``. 38 | 39 | For jupyterlab 3.0+, reconfigure the subcommand from "notebook" to "server". 40 | 41 | :: 42 | 43 | M-x customize-option RET ein:jupyter-server-use-subcommand RET 44 | 45 | Usage 46 | ===== 47 | Start EIN using **ONE** of the following: 48 | 49 | * Open an ``.ipynb`` file, press ``C-c C-o``, or, 50 | * ``M-x ein:run`` launches a jupyter process from emacs, or, 51 | * ``M-x ein:login`` to a running jupyter server, or, 52 | 53 | ``M-x ein:stop`` prompts to halt local and remote jupyter services. 54 | 55 | Alternatively, ob-ein_. 56 | 57 | .. _Cask: https://cask.readthedocs.io/en/latest/guide/installation.html 58 | .. _Getting started: http://melpa.org/#/getting-started 59 | 60 | FAQ 61 | === 62 | 63 | How do I... 64 | ----------- 65 | 66 | ... report a bug? 67 | Note EIN is tested only for *released* GNU Emacs versions 68 | .. CI VERSION (see Makefile) 69 | and later. Pre-release versions will not work. 70 | 71 | First try ``emacs -Q -f package-initialize -f ein:dev-start-debug`` and reproduce the bug. The ``-Q`` skips any user configuration that might interfere with EIN. 72 | 73 | Then file an issue using ``M-x ein:dev-bug-report-template``. 74 | 75 | ... display images inline? 76 | We find inserting images into emacs disruptive, and so default to spawning an external viewer. To override this, 77 | :: 78 | 79 | M-x customize-group RET ein 80 | Ein:Output Area Inlined Images 81 | 82 | ... configure the external image viewer? 83 | :: 84 | 85 | M-x customize-group RET mailcap 86 | Mailcap User Mime Data 87 | 88 | On a typical Linux system, one might configure a viewer for MIME Type ``image/png`` as a shell command ``convert %s -background white -alpha remove -alpha off - | display -immutable``. 89 | 90 | ... get IDE-like behavior? 91 | You can't. EIN's architecture is fundamentally incompatible with LSP. 92 | 93 | .. _Issues: https://github.com/millejoh/emacs-ipython-notebook/issues 94 | .. _prevailing documentation: http://millejoh.github.io/emacs-ipython-notebook 95 | .. _spacemacs layer: https://github.com/syl20bnr/spacemacs/tree/master/layers/%2Blang/ipython-notebook 96 | .. _company-mode: https://github.com/company-mode/company-mode 97 | .. _jupyterhub: https://github.com/jupyterhub/jupyterhub 98 | .. _elpy: https://melpa.org/#/elpy 99 | .. _math-preview: https://gitlab.com/matsievskiysv/math-preview 100 | .. _program modes: https://www.gnu.org/software/emacs/manual/html_node/emacs/Program-Modes.html 101 | .. _undo boundaries: https://www.gnu.org/software/emacs/manual/html_node/elisp/Undo.html 102 | 103 | ob-ein 104 | ====== 105 | Configuration: 106 | 107 | :: 108 | 109 | M-x customize-group RET org-babel 110 | Org Babel Load Languages: 111 | Insert (ein . t) 112 | For example, '((emacs-lisp . t) (ein . t)) 113 | 114 | Snippet: 115 | 116 | :: 117 | 118 | #+BEGIN_SRC ein-python :session localhost 119 | import numpy, math, matplotlib.pyplot as plt 120 | %matplotlib inline 121 | x = numpy.linspace(0, 2*math.pi) 122 | plt.plot(x, numpy.sin(x)) 123 | #+END_SRC 124 | 125 | The ``:session`` is the notebook url, e.g., ``http://localhost:8888/my.ipynb``, or simply ``localhost``, in which case org evaluates anonymously. A port may also be specified, e.g., ``localhost:8889``. 126 | 127 | *Language* can be ``ein-python``, ``ein-r``, or ``ein-julia``. **The relevant** `jupyter kernel`_ **must be installed before use**. Additional languages can be configured via:: 128 | 129 | M-x customize-group RET ein 130 | Ob Ein Languages 131 | 132 | .. _polymode: https://github.com/polymode/polymode 133 | .. _ob-ipython: https://github.com/gregsexton/ob-ipython 134 | .. _scimax: https://github.com/jkitchin/scimax 135 | .. _jupyter kernel: https://github.com/jupyter/jupyter/wiki/Jupyter-kernels 136 | 137 | .. _gat utility: https://dickmaogat.readthedocs.io/en/latest/install.html 138 | .. _gat usage: https://dickmaogat.readthedocs.io/en/latest/usage.html 139 | .. _batch mode: https://nbconvert.readthedocs.io/en/latest/execute_api.html 140 | .. _dickmao/Kaggler: https://github.com/dickmao/Kaggler/tree/gcspath#importing-datasets 141 | 142 | Keymap (C-h m) 143 | ============== 144 | 145 | :: 146 | 147 | .. KEYS NOTEBOOK (see Makefile) 148 | -------------------------------------------------------------------------------- /features/notebook.feature: -------------------------------------------------------------------------------- 1 | @paren 2 | Scenario: show-paren-mode should behave 3 | Given new python notebook 4 | And I press "C-c C-b" 5 | And I press "C-c C-p" 6 | And I type "x = np.arange(0" 7 | And I press "C-b" 8 | And I press "C-b" 9 | And eval "(let ((orig (point))) (funcall show-paren-data-function) (cl-assert (equal (point) orig)))" 10 | 11 | @rename 12 | Scenario: rename notebook 13 | Given new python notebook 14 | And I press "C-c C-/" 15 | And I switch to buffer like "Untitled" 16 | And rename notebook to "Renamed" succeeds 17 | 18 | @image 19 | Scenario: in ipython<=7.10, image failed to materialize initially 20 | Given new python notebook 21 | And I type "import numpy, math, matplotlib.pyplot as plt" 22 | And I press "RET" 23 | And I type "x = numpy.linspace(0, 2*math.pi)" 24 | And I press "RET" 25 | And I type "plt.plot(x, numpy.sin(x))" 26 | And I press "RET" 27 | And I clear log expr "ein:log-all-buffer-name" 28 | And I wait for cell to execute 29 | And I switch to log expr "ein:log-all-buffer-name" 30 | Then I should see "msg_type=display_data" 31 | 32 | @switch 33 | Scenario: switch kernel 34 | Given new python notebook 35 | And I type "import itertools" 36 | And I wait for cell to execute 37 | And I switch kernel to "ir" 38 | 39 | @pager 40 | Scenario: so-called paging in ein:kernel--handle-payload 41 | Given new python notebook 42 | And I type "help?" 43 | And I wait for cell to execute 44 | And I switch to buffer like "*ein:pager" 45 | And I should see "help()" 46 | And I press "C-x k" 47 | 48 | @traceback 49 | Scenario: native json parsing broke traceback 50 | Given new python notebook 51 | And I type "def func1():" 52 | And I press "RET" 53 | And I type " func2()" 54 | And I press "RET" 55 | And I type "def func2():" 56 | And I press "RET" 57 | And I type " 1/0" 58 | And I press "RET" 59 | And I type "func1()" 60 | And I press "RET" 61 | And I wait for cell to execute 62 | Then I should see "Traceback" 63 | And I press "C-c C-$" 64 | And I switch to buffer like "*ein:tb" 65 | Then I should see "ZeroDivisionError" 66 | 67 | @reconnect 68 | Scenario: kernel reconnect succeeds 69 | Given new python notebook 70 | When I type "import math" 71 | And I wait for cell to execute 72 | And I kill processes like "websocket" 73 | And I switch to log expr "ein:log-all-buffer-name" 74 | Then I should see "WS closed unexpectedly" 75 | And I switch to buffer like "Untitled" 76 | And header says "Kernel requires reconnect \\[ein:notebook-reconnect-session-command-km]" 77 | And I clear log expr "ein:log-all-buffer-name" 78 | And I press "C-c C-r" 79 | And I wait for the smoke to clear 80 | And header does not say "Kernel requires reconnect \\[ein:notebook-reconnect-session-command-km]" 81 | And I switch to log expr "ein:log-all-buffer-name" 82 | Then I should not see "[warn]" 83 | And I should not see ": [error]" 84 | And I should see "ein:kernel-retrieve-session--complete" 85 | And I switch to buffer like "Untitled" 86 | And I kill processes like "websocket" 87 | And I switch to log expr "ein:log-all-buffer-name" 88 | Then I should see "WS closed unexpectedly" 89 | And I switch to buffer like "Untitled" 90 | And header says "Kernel requires reconnect \\[ein:notebook-reconnect-session-command-km]" 91 | And I clear log expr "ein:log-all-buffer-name" 92 | And I wait for cell to execute 93 | And header does not say "Kernel requires reconnect \\[ein:notebook-reconnect-session-command-km]" 94 | And I switch to log expr "ein:log-all-buffer-name" 95 | Then I should not see "[warn]" 96 | And I should not see ": [error]" 97 | And I should see "ein:kernel-retrieve-session--complete" 98 | And I switch to buffer like "Untitled" 99 | And I clear log expr "ein:log-all-buffer-name" 100 | And I restart kernel 101 | And I switch to log expr "ein:log-all-buffer-name" 102 | Then I should not see "[warn]" 103 | And I should not see ": [error]" 104 | And I should see "ein:kernel-retrieve-session--complete" 105 | And I switch to buffer like "Untitled" 106 | And I kill kernel 107 | And header says "Kernel requires reconnect \\[ein:notebook-reconnect-session-command-km]" 108 | And I clear log expr "ein:log-all-buffer-name" 109 | And my reconnect is questioned 110 | 111 | @exit 112 | Scenario: Saving fails upon quit, need to consult user 113 | Given new python notebook 114 | When I type "import math" 115 | And I wait for cell to execute 116 | And I cannot save upon quit 117 | 118 | @kill 119 | Scenario: Assign variable, save, kill notebook buffer, get it back, check variable 120 | Given new python notebook 121 | When I type "import math" 122 | And I press "RET" 123 | And I type "b = math.pi" 124 | And I press "RET" 125 | And I wait for cell to execute 126 | And I clear log expr "ein:log-all-buffer-name" 127 | And I press "C-x C-s" 128 | And I switch to log expr "ein:log-all-buffer-name" 129 | And I wait for buffer to say "Notebook is saved" 130 | And I switch to buffer like "Untitled" 131 | And I kill buffer and reopen 132 | And I press "C-c C-b" 133 | And I dump buffer 134 | And I type "b" 135 | And I wait for cell to execute 136 | Then I should see "3.1415" 137 | 138 | @julia 139 | Scenario: Smoke test julia 140 | Given new julia notebook 141 | When I type "isapprox(Base.MathConstants.e ^ (pi * im), -1)" 142 | And I wait for cell to execute 143 | Then I should see "true" 144 | And I dump buffer 145 | -------------------------------------------------------------------------------- /test/ein-testing-notebook.el: -------------------------------------------------------------------------------- 1 | ;;; ein-testing-notebook.el --- Testing utilities for notebook module -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012 Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-testing-notebook.el is free software: you can redistribute it 10 | ;; and/or modify it under the terms of the GNU General Public License 11 | ;; as published by the Free Software Foundation, either version 3 of 12 | ;; the License, or (at your option) any later version. 13 | 14 | ;; ein-testing-notebook.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-testing-notebook.el. 21 | ;; If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; 26 | 27 | ;;; Code: 28 | 29 | (require 'ein-notebook) 30 | (require 'ein-testing-cell) 31 | 32 | (defvar ein:testing-notebook-dummy-name "Dummy Name.ipynb") 33 | (defvar ein:testing-notebook-dummy-url "DUMMY-URL") 34 | 35 | (defun ein:testing-notebook-from-json (json-string) 36 | (let* ((data (ein:with-json-setting (json-read-from-string json-string))) ;; intentionally not ein:json-read-from-string 37 | (path (plist-get data :path)) 38 | (kernelspec (make-ein:$kernelspec :name "python" :language "python")) 39 | (content (make-ein:$content :url-or-port ein:testing-notebook-dummy-url 40 | :notebook-api-version "5" 41 | :path path))) 42 | (ignore content) 43 | (cl-letf (((symbol-function 'ein:need-notebook-api-version) (lambda (&rest _ignore) "5")) 44 | ((symbol-function 'ein:kernel-retrieve-session) #'ignore) 45 | ((symbol-function 'ein:notebook-enable-autosaves) #'ignore)) 46 | (let ((notebook (ein:notebook-new ein:testing-notebook-dummy-url path kernelspec))) 47 | (setf (ein:$notebook-kernel notebook) 48 | (ein:kernel-new 8888 "" nil "/kernels" (ein:$notebook-events notebook) (ein:need-notebook-api-version (ein:$notebook-url-or-port notebook)))) 49 | (setf (ein:$kernel-events (ein:$notebook-kernel notebook)) 50 | (ein:events-new)) 51 | ; matryoshka: new-content makes a ein:$content using CONTENT as template 52 | ; populating its raw_content field with DATA's content field 53 | (ein:notebook-open--callback 54 | notebook nil 55 | (ein:new-content (ein:$notebook-url-or-port notebook) 56 | (ein:$notebook-notebook-path notebook) data)) 57 | (ein:notebook-buffer notebook))))) 58 | 59 | (defun ein:testing-notebook-make-data (name path cells) 60 | (setq cells 61 | (ein:testing-notebook--preprocess-cells-data-for-json-encode cells)) 62 | `((path . ,path) 63 | (name . ,name) 64 | (type . "notebook") 65 | (format . "json") 66 | (mimetype . nil) 67 | (writeable . t) 68 | (content (metadata . ()) 69 | (nbformat . 4) 70 | (nbformat_minor . 0) 71 | (cells . ,(apply #'vector cells))))) 72 | 73 | (defun ein:testing-notebook--preprocess-cells-data-for-json-encode (cells) 74 | "Preprocess CELLS data to make it work nice with `json-encode'." 75 | (mapcar (lambda (c) 76 | (cond 77 | ((equal (plist-get c :cell_type) "code") 78 | ;; turn `:outputs' into an array. 79 | (plist-put c :outputs (apply #'vector (plist-get c :outputs)))) 80 | (t c))) 81 | cells)) 82 | 83 | (defun ein:testing-notebook-make-new (&optional name path cells) 84 | "Make new notebook. One empty cell will be inserted 85 | automatically if CELLS is nil." 86 | (ein:testing-notebook-from-json 87 | (ein:json-encode (ein:testing-notebook-make-data 88 | (or name ein:testing-notebook-dummy-name) 89 | (or path name ein:testing-notebook-dummy-name) 90 | cells)))) 91 | 92 | (defun ein:testing-notebook-make-empty (&optional name path) 93 | "Make empty notebook and return its buffer. 94 | Automatically inserted cell for new notebook is deleted." 95 | (let ((buffer (ein:testing-notebook-make-new name path))) 96 | (with-current-buffer buffer 97 | (call-interactively #'ein:worksheet-delete-cell)) 98 | buffer)) 99 | 100 | (defmacro ein:testing-with-one-cell (type-or-cell &rest body) 101 | "Insert new cell of TYPE-OR-CELL in a clean notebook and execute BODY. 102 | The new cell is bound to a variable `cell'." 103 | (declare (indent 1)) 104 | `(with-current-buffer (ein:testing-notebook-make-empty) 105 | (let ((cell (ein:worksheet-insert-cell-below ein:%worksheet% 106 | ,type-or-cell nil t))) 107 | ,@body))) 108 | 109 | (defun ein:testing-make-notebook-with-outputs (list-outputs) 110 | "Make a new notebook with cells with output. 111 | LIST-OUTPUTS is a list of list of strings (pyout text). Number 112 | of LIST-OUTPUTS equals to the number cells to be contained in the 113 | notebook." 114 | (ein:testing-notebook-make-new 115 | ein:testing-notebook-dummy-name nil 116 | (mapcar (lambda (outputs) 117 | (ein:testing-codecell-data 118 | nil nil (mapcar #'ein:testing-codecell-pyout-data outputs))) 119 | list-outputs))) 120 | 121 | (provide 'ein-testing-notebook) 122 | 123 | ;;; ein-testing-notebook.el ends here 124 | -------------------------------------------------------------------------------- /features/notebooklist.feature: -------------------------------------------------------------------------------- 1 | @bread 2 | Scenario: Breadcrumbs 3 | Given I am in notebooklist buffer 4 | When I click on dir "step-definitions" until "ein-steps" 5 | And I click on "Home" 6 | Then I wait for buffer to say "support" 7 | 8 | @kernel 9 | Scenario: Default kernel 10 | And I am in notebooklist buffer 11 | Then I should see "(*) Python" 12 | 13 | Scenario: New Notebook 14 | Given I am in notebooklist buffer 15 | When I clear log expr "ein:log-all-buffer-name" 16 | And I click on "New Notebook" 17 | And no notebooks pending 18 | And I switch to log expr "ein:log-all-buffer-name" 19 | Then I should see "Opened notebook Untitled" 20 | 21 | Scenario: Resync 22 | Given I am in notebooklist buffer 23 | When I clear log expr "ein:log-all-buffer-name" 24 | And I click on "Resync" 25 | And I switch to log expr "ein:log-all-buffer-name" 26 | Then I should see "kernelspecs--complete" 27 | 28 | Scenario: File open 29 | Given I am in notebooklist buffer 30 | And I go to word "notebooklist.feature" 31 | And I go to beginning of line 32 | And I click without going top on file "Open" 33 | And I switch to buffer like "notebooklist.feature" 34 | Then I should see "File open" 35 | And I press "C-x k" 36 | 37 | @stop 38 | Scenario: Stop after closing notebook 39 | Given I am in notebooklist buffer 40 | Given I clear log expr "ein:log-all-buffer-name" 41 | And I click on "New Notebook" 42 | And no notebooks pending 43 | And I switch to buffer like "Untitled" 44 | And I press "C-x k" 45 | And I am in notebooklist buffer 46 | And I clear log expr "ein:log-all-buffer-name" 47 | And I keep clicking "Resync" until "Stop" 48 | And I click on "Stop" 49 | And I switch to log expr "ein:log-all-buffer-name" 50 | Then I wait for buffer to say "kernel-delete-session--success" 51 | And I am in notebooklist buffer 52 | And I go to word "Untitled" 53 | And I go to beginning of line 54 | And I click without going top on "Open" 55 | And no notebooks pending 56 | And I switch to buffer like "Untitled" 57 | 58 | @delete 59 | Scenario: Delete closes buffers and sessions 60 | Given I am in notebooklist buffer 61 | Given I clear log expr "ein:log-all-buffer-name" 62 | And I click on "New Notebook" 63 | And no notebooks pending 64 | And I switch to buffer like "Untitled" 65 | And I am in notebooklist buffer 66 | And I clear log expr "ein:log-all-buffer-name" 67 | And I click on "Delete" 68 | And I wait for buffer to not say "Stop" 69 | Then eval "(cl-assert (not (ein:notebook-opened-notebooks)))" 70 | Then eval "(cl-assert (not (seq-some (lambda (b) (cl-search "Untitled" (buffer-name b))) (buffer-list))))" 71 | And I switch to log expr "ein:log-all-buffer-name" 72 | Then I wait for buffer to say "kernel-delete-session--success" 73 | Then I should see "notebooklist-delete-notebook--complete" 74 | 75 | @content 76 | Scenario: Read a massive directory 77 | Given I create a directory "/var/tmp/fg7Cv8" with depth 4 and width 8 78 | And I get into notebook mode "/var/tmp/fg7Cv8" "8/4/3/bar.ipynb" 79 | And I open notebook "bar.ipynb" 80 | And I open file "foo.txt" 81 | And notebooklist-list-paths does not contain "4/4/4/foo.txt" 82 | And notebooklist-list-paths contains "foo.txt" 83 | 84 | @login 85 | Scenario: No token server 86 | Given I start the server configured "c.NotebookApp.token = u''\n" 87 | And I switch to log expr "ein:log-all-buffer-name" 88 | Then I should not see "[warn]" 89 | And I should not see ": [error]" 90 | 91 | @login 92 | Scenario: With token server, get from server buffer 93 | Given I start the server configured "\n" 94 | And I login disabling crib token 95 | And I switch to log expr "ein:log-all-buffer-name" 96 | Then I should not see "[warn]" 97 | And I should not see ": [error]" 98 | 99 | @login 100 | Scenario: To the cloud with password 101 | Given I start the server configured "c.NotebookApp.password=u'sha1:712118ed6c09:bc02227d84b76b720cc320b855e1006d0b120f98'\n" 102 | And I login forcing ping with password "foo" 103 | And I switch to log expr "ein:log-all-buffer-name" 104 | Then I should not see "[warn]" 105 | And I should not see ": [error]" 106 | And new python notebook 107 | When I type "import math" 108 | And I wait for cell to execute 109 | 110 | @login 111 | Scenario: Nonempty base_url and password 112 | Given I start the server configured "c.NotebookApp.base_url=u'nonempty'\nc.NotebookApp.password=u'sha1:712118ed6c09:bc02227d84b76b720cc320b855e1006d0b120f98'\n" 113 | And I login forcing ping with password "foo" 114 | And I switch to log expr "ein:log-all-buffer-name" 115 | Then I should not see "[warn]" 116 | And I should not see ": [error]" 117 | And new python notebook 118 | When I type "import math" 119 | And I wait for cell to execute 120 | 121 | @login 122 | Scenario: Logging into nowhere 123 | Given I login erroneously to 0 124 | And I switch to log expr "ein:log-all-buffer-name" 125 | Then I should see "[error] Login to http://127.0.0.1:0 failed" 126 | 127 | @login 128 | Scenario: Logging into nowhere again 129 | Given I login erroneously to adfljdsf.org:8432 130 | And I switch to log expr "ein:log-all-buffer-name" 131 | Then I should see "[error] Login to https://adfljdsf.org:8432 failed" 132 | 133 | @login 134 | Scenario: Bad curl invocation produces sensible error message 135 | Given I start the server configured "\n" 136 | And I login with bad curl 137 | And I call "ein:log-pop-to-all-buffer" 138 | And I wait for buffer to say "error-thrown" 139 | 140 | @login 141 | Scenario: jupyter not found 142 | And I start bad jupyter path 143 | 144 | @jupyter-notebook 145 | Scenario: Someone uses jupyter-notebook 146 | Given I customize "ein:jupyter-default-server-command" to "jupyter-notebook" 147 | And I set "ein:jupyter-server-use-subcommand" to eval "nil" 148 | Given I start and login to the server configured "\n" 149 | And I switch to log expr "ein:log-all-buffer-name" 150 | Then I should not see "[warn]" 151 | And I should not see ": [error]" 152 | And I customize "ein:jupyter-default-server-command" to "jupyter" 153 | And I set "ein:jupyter-server-use-subcommand" to "notebook" 154 | 155 | @login 156 | Scenario: With token server 157 | Given I start and login to the server configured "\n" 158 | And I switch to log expr "ein:log-all-buffer-name" 159 | Then I should not see "[warn]" 160 | And I should not see ": [error]" 161 | 162 | @exit 163 | Scenario: Ask user to save unsaved notebooks 164 | Given new python notebook 165 | When I type "import math" 166 | And I press "RET" 167 | Then save on exit 168 | -------------------------------------------------------------------------------- /lisp/ein-python-send.el: -------------------------------------------------------------------------------- 1 | ;;; ein-python-send.el --- Ad hoc sending of code fragments to kernel -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2012- The Authors 4 | 5 | ;; This file is NOT part of GNU Emacs. 6 | 7 | ;; ein-python-send.el is free software: you can redistribute it and/or modify 8 | ;; it under the terms of the GNU General Public License as published by 9 | ;; the Free Software Foundation, either version 3 of the License, or 10 | ;; (at your option) any later version. 11 | 12 | ;; ein-python-send.el is distributed in the hope that it will be useful, 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ;; GNU General Public License for more details. 16 | 17 | ;; You should have received a copy of the GNU General Public License 18 | ;; along with ein-python-send.el. If not, see . 19 | 20 | ;;; Commentary: 21 | 22 | ;; python parsing code by authors of elpy (Schaefer et al) 23 | 24 | ;;; Code: 25 | 26 | (autoload 'ein:get-notebook "ein-notebook") 27 | 28 | (defvar ein:python-send-map) 29 | 30 | (defun ein:python-send--prepare (&optional reset) 31 | (cl-assert (boundp 'ein:python-send-map) nil 32 | "ein:python-send--prepare: %s not called" 33 | "ein:python-send--init") 34 | (unless (and (buffer-live-p (current-buffer)) 35 | (eq major-mode 'python-mode)) 36 | (error "ein:python-send--prepare: %s is not a python buffer" (buffer-name))) 37 | (when (or (not (ein:get-notebook)) reset) 38 | (aif (ein:notebook-opened-notebooks) 39 | (let ((choice 40 | (ein:completing-read 41 | "Notebook: " 42 | (mapcar (lambda (nb) (ein:$notebook-notebook-name nb)) it)))) 43 | (setq ein:%notebook% (seq-find 44 | (lambda (nb) 45 | (string= choice (ein:$notebook-notebook-name nb))) 46 | it))) 47 | (error "ein:python-send--prepare: No open notebooks")))) 48 | 49 | (defun ein:python-send-region-or-buffer (&optional reset) 50 | "Based on `elpy-shell--send-region-or-buffer-internal' by Schaefer et al." 51 | (interactive "P") 52 | (ein:python-send--prepare reset) 53 | (if (use-region-p) 54 | (let ((region (python-shell-buffer-substring 55 | (region-beginning) (region-end)))) 56 | (when (string-match "\t" region) 57 | (message "Region contained tabs, this might cause weird errors")) 58 | ;; python-shell-buffer-substring (intentionally?) does not accurately 59 | ;; respect (region-beginning); it always start on the first character 60 | ;; of the respective line even if that's before the region beginning 61 | ;; Here we post-process the output to remove the characters before 62 | ;; (region-beginning) and the start of the line. The end of the region 63 | ;; is handled correctly and needs no special treatment. 64 | (let* ((bounds (save-excursion 65 | (goto-char (region-beginning)) 66 | (bounds-of-thing-at-point 'line))) 67 | (used-part (string-trim 68 | (buffer-substring-no-properties 69 | (car bounds) 70 | (min (cdr bounds) (region-end))))) 71 | (relevant-part (string-trim 72 | (buffer-substring-no-properties 73 | (max (car bounds) (region-beginning)) 74 | (min (cdr bounds) (region-end)))))) 75 | (setq region 76 | ;; replace just first match 77 | (replace-regexp-in-string 78 | (concat "\\(" (regexp-quote used-part) "\\)\\(?:.*\n?\\)*\\'") 79 | relevant-part 80 | region t t 1)) 81 | (ein:shared-output-eval-string (ein:get-kernel) region))) 82 | (ein:shared-output-eval-string (ein:get-kernel) (buffer-string))) 83 | (if (use-region-p) 84 | (progn 85 | (goto-char (region-end)) 86 | (deactivate-mark)) 87 | (goto-char (point-max)))) 88 | 89 | (defun ein:python-send-statement (&optional reset) 90 | "Based on `elpy-shell-send-statement' by Schaefer et al." 91 | (interactive "P") 92 | (ein:python-send--prepare reset) 93 | (python-nav-beginning-of-statement) 94 | (unless (looking-at "[[:space:]]*$") 95 | (let ((beg (save-excursion (beginning-of-line) (point))) 96 | (end (progn (ein:python-send--nav-end-of-statement) (point)))) 97 | (unless (eq beg end) 98 | (ein:shared-output-eval-string (ein:get-kernel) 99 | (buffer-substring beg end)))))) 100 | 101 | (defun ein:python-send--nav-end-of-statement () 102 | "Based on `elpy-shell--nav-end-of-statement' by Schaefer et al." 103 | (let ((continue t) 104 | p) 105 | (while (and (not (eq p (point))) continue) 106 | ;; is there another block at same indentation level? 107 | (setq p (point)) 108 | (ein:python-send--nav-forward-block) 109 | (if (eq p (point)) 110 | (progn 111 | ;; nope, go to the end of the block and done 112 | (python-nav-end-of-block) 113 | (setq continue nil)) 114 | (unless (eq 0 (string-match-p "\\s-*el\\(?:se:\\|if[^\w]\\)" 115 | (thing-at-point 'line))) 116 | (forward-line -1) 117 | (while (and (or (eq (string-match-p "\\s-*$" (thing-at-point 'line)) 0) 118 | (python-info-current-line-comment-p)) 119 | (not (eq (point) (point-min)))) 120 | (forward-line -1)) 121 | (setq continue nil))))) 122 | (end-of-line)) 123 | 124 | (defun ein:python-send--nav-forward-block () 125 | "Based on `elpy-shell--nav-forward-block' by Schaefer et al. 126 | 127 | Move to the next line indented like point. This will skip over lines and 128 | statements with different indentation levels." 129 | (interactive "^") 130 | (let ((indent (current-column)) 131 | (start (point)) 132 | (cur nil)) 133 | (when (/= (% indent python-indent-offset) 134 | 0) 135 | (setq indent (* (1+ (/ indent python-indent-offset)) 136 | python-indent-offset))) 137 | (python-nav-forward-statement) 138 | (while (and (< indent (current-indentation)) 139 | (not (eobp))) 140 | (when (equal (point) cur) 141 | (error "Statement does not finish")) 142 | (setq cur (point)) 143 | (python-nav-forward-statement)) 144 | (when (< (current-indentation) 145 | indent) 146 | (goto-char start)))) 147 | 148 | (defun ein:python-send--init () 149 | (unless (boundp 'ein:python-send-map) 150 | (require 'python) 151 | (setq ein:python-send-map 152 | (let ((map (make-sparse-keymap))) 153 | (define-key map (kbd "e") 'ein:python-send-statement) 154 | (define-key map (kbd "r") 'ein:python-send-region-or-buffer) 155 | map)) 156 | (define-key python-mode-map (kbd "C-c C-/") ein:python-send-map))) 157 | 158 | (provide 'ein-python-send) 159 | 160 | ;;; ein-python-send.el ends here 161 | -------------------------------------------------------------------------------- /features/ob-ein.feature: -------------------------------------------------------------------------------- 1 | @memory 2 | Scenario: R and Julia in the same org file 3 | Given I stop the server 4 | When I open temp file "ecukes.org" 5 | And I call "org-mode" 6 | And I type "" 15 | And I type "" 39 | And I type "" 81 | And I type "" 101 | And I type "" 170 | And I type "" 195 | And I press "RET" 196 | And I press "RET" 197 | And I type "= emacs-major-version 27) 26 | (require 'org-tempo)) 27 | 28 | (unless with-editor-emacsclient-executable 29 | (!cons "gat" ecukes-exclude-tags)) 30 | 31 | (!cons "timestamp" ecukes-exclude-tags) 32 | 33 | (unless (member "jupyterhub" ecukes-include-tags) 34 | (!cons "jupyterhub" ecukes-exclude-tags)) 35 | 36 | (when (getenv "GITHUB_ACTIONS") 37 | (cl-assert (not (eq system-type 'darwin))) 38 | (!cons "memory" ecukes-exclude-tags) 39 | (!cons "julia" ecukes-exclude-tags) 40 | (!cons "content" ecukes-exclude-tags) 41 | (!cons "svg" ecukes-exclude-tags) 42 | (!cons "gat" ecukes-exclude-tags) 43 | (!cons "pass" ecukes-exclude-tags) ;; salt? stopped working around 20210316 44 | (!cons "switch" ecukes-exclude-tags)) 45 | 46 | (defalias 'activate-cursor-for-undo #'ignore) 47 | (defalias 'deactivate-cursor-after-undo #'ignore) 48 | 49 | (defvar ein:testing-jupyter-server-root (f-parent (f-dirname load-file-name))) 50 | 51 | (defconst ein:testing-project-path (ecukes-project-path)) 52 | 53 | (defun ein:testing-after-scenario () 54 | (let ((default-directory ein:testing-project-path)) 55 | (with-current-buffer (ein:notebooklist-get-buffer (ein:jupyter-my-url-or-port)) 56 | (cl-loop for notebook in (ein:notebook-opened-notebooks) 57 | for url-or-port = (ein:$notebook-url-or-port notebook) 58 | for path = (ein:$notebook-notebook-path notebook) 59 | for done-p = nil 60 | do (ein:notebook-kill-kernel-then-close-command 61 | notebook (lambda (_kernel) (setq done-p t))) 62 | do (cl-loop repeat 16 63 | until done-p 64 | do (sleep-for 0 1000) 65 | finally do (unless done-p 66 | (ein:display-warning (format "cannot close %s" path)))) 67 | do (when (or (ob-ein-anonymous-p path) 68 | (cl-search "Untitled" path) 69 | (cl-search "Renamed" path)) 70 | (ein:notebooklist-delete-notebook ein:%notebooklist% url-or-port path) 71 | (cl-loop with fullpath = (concat (file-name-as-directory ein:testing-jupyter-server-root) path) 72 | repeat 10 73 | for extant = (file-exists-p fullpath) 74 | until (not extant) 75 | do (sleep-for 0 1000) 76 | finally do (when extant 77 | (ein:display-warning (format "cannot delete %s" path)))))))) 78 | (awhen (ein:notebook-opened-notebooks) 79 | (cl-loop for nb in it 80 | for path = (ein:$notebook-notebook-path nb) 81 | do (ein:log 'debug "Notebook %s still open" path) 82 | finally do (cl-assert nil))) 83 | (cl-loop repeat 5 84 | for stragglers = (file-name-all-completions "Untitled" 85 | ein:testing-jupyter-server-root) 86 | until (null stragglers) 87 | ;; do (message "ein:testing-after-scenario: fs stale handles: %s" 88 | ;; (mapconcat #'identity stragglers ", ")) 89 | do (sleep-for 0 1000)) 90 | (mapc #'delete-file 91 | (mapcar (apply-partially #'concat 92 | (file-name-as-directory ein:testing-jupyter-server-root)) 93 | (file-name-all-completions "Untitled" ein:testing-jupyter-server-root)))) 94 | 95 | (defmacro ein--remove-ecukes-io-advices (function class name) 96 | "The princ advice is known to peg CPU when cl-prin1 of nested objects." 97 | `(when (ad-find-advice ',function ',class ',name) 98 | (ad-remove-advice ',function ',class ',name) 99 | (ad-activate ',function))) 100 | 101 | (Setup 102 | (ein--remove-ecukes-io-advices princ around princ-around) 103 | (ein--remove-ecukes-io-advices print around print-around) 104 | (ein:dev-start-debug) 105 | (setenv "GAT_APPLICATION_CREDENTIALS" "nonempty") 106 | (custom-set-variables '(python-indent-guess-indent-offset-verbose nil) 107 | '(ein:jupyter-use-containers nil) 108 | '(ein:gat-gce-zone "abc") 109 | '(ein:gat-gce-region "abc") 110 | '(ein:gat-aws-region "abc") 111 | '(ein:gat-gce-project "abc") 112 | '(electric-indent-mode nil) 113 | '(ein:gat-machine-types '("abc")) 114 | `(request-storage-directory ,(expand-file-name "test" ein:testing-project-path))) 115 | (setq ein:jupyter-default-kernel 116 | (cl-loop with cand = "" 117 | for (k . spec) in 118 | (alist-get 119 | 'kernelspecs 120 | (let ((json-object-type 'alist)) 121 | (json-read-from-string ;; intentionally not ein:json-read-from-string 122 | (shell-command-to-string 123 | (format "%s kernelspec list --json" 124 | ein:jupyter-server-command))))) 125 | if (let ((lang (alist-get 'language (alist-get 'spec spec)))) 126 | (and (string= "python" lang) 127 | (string> (symbol-name k) cand))) 128 | do (setq cand (symbol-name k)) 129 | end 130 | finally return (intern cand))) 131 | (setq ein:testing-dump-file-log (concat default-directory "log/ecukes.log")) 132 | (setq ein:testing-dump-file-messages (concat default-directory "log/ecukes.messages")) 133 | (setq ein:testing-dump-file-server (concat default-directory "log/ecukes.server")) 134 | (setq ein:testing-dump-file-websocket (concat default-directory "log/ecukes.websocket")) 135 | (setq ein:testing-dump-file-request (concat default-directory "log/ecukes.request")) 136 | (setq org-confirm-babel-evaluate nil) 137 | (setq transient-mark-mode t) 138 | (Given "I start and login to the server configured \"\\n\"")) 139 | 140 | (Before 141 | (setq default-directory ein:testing-project-path)) 142 | 143 | (After 144 | (ein:testing-after-scenario)) 145 | 146 | (Teardown 147 | (Given "I finally stop the server")) 148 | 149 | (Fail 150 | (if noninteractive 151 | (ein:testing-after-scenario) 152 | (keyboard-quit))) ;; useful to prevent emacs from quitting 153 | -------------------------------------------------------------------------------- /test/ein-testing.el: -------------------------------------------------------------------------------- 1 | ;;; ein-testing.el --- Tools for testing -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012 Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-testing.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-testing.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-testing.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'ein-log) 29 | (require 'ein-jupyter) 30 | (require 'request) 31 | (require 'anaphora) 32 | (require 'f) 33 | 34 | (defmacro ein:setq-if-not (sym val) 35 | `(unless ,sym (setq ,sym ,val))) 36 | 37 | (defvar ein:testing-dump-file-log nil 38 | "File to save buffer specified by `ein:log-all-buffer-name'.") 39 | 40 | (defvar ein:testing-dump-file-messages nil 41 | "File to save the ``*Messages*`` buffer.") 42 | 43 | (defvar ein:testing-dump-file-server nil 44 | "File to save `*ein:jupyter-server-buffer-name*`.") 45 | 46 | (defvar ein:testing-dump-file-request nil 47 | "File to save `request-log-buffer-name`.") 48 | 49 | (defun ein:testing-save-buffer (buffer-or-name file-name) 50 | (when (and buffer-or-name (get-buffer buffer-or-name) file-name) 51 | (let ((dir (file-name-directory file-name))) 52 | (make-directory dir t) 53 | (with-current-buffer buffer-or-name 54 | (let ((coding-system-for-write 'raw-text)) 55 | (write-region (point-min) (point-max) file-name)))))) 56 | 57 | (defun ein:testing-dump-logs () 58 | (ein:testing-save-buffer "*Messages*" ein:testing-dump-file-messages) 59 | (ein:testing-save-buffer *ein:jupyter-server-buffer-name* ein:testing-dump-file-server) 60 | (mapc (lambda (b) 61 | (ein:and-let* ((bname (buffer-name b)) 62 | (prefix "kernels/") 63 | (is-websocket (cl-search "*websocket" bname)) 64 | (kernel-start (cl-search prefix bname)) 65 | (sofar (cl-subseq bname (+ kernel-start (length prefix)))) 66 | (kernel-end (cl-search "/" sofar))) 67 | (ein:testing-save-buffer 68 | bname 69 | (concat ein:testing-dump-file-websocket "." 70 | (seq-take sofar kernel-end))))) 71 | (buffer-list)) 72 | (ein:testing-save-buffer ein:log-all-buffer-name ein:testing-dump-file-log) 73 | (ein:testing-save-buffer request-log-buffer-name ein:testing-dump-file-request)) 74 | 75 | (defun ein:testing-flush-queries (&optional ms interval) 76 | "I need the smoke to clear, but just waiting for zero running processes doesn't work 77 | if I call this between links in a deferred chain. Adding a flush-queue." 78 | (deferred:flush-queue!) 79 | (ein:testing-wait-until (lambda () 80 | (not (seq-some (lambda (proc) 81 | (cl-search "request curl" 82 | (process-name proc))) 83 | (process-list)))) 84 | nil ms interval t)) 85 | 86 | (defun ein:testing-make-directory-level (parent current-depth width depth) 87 | (let ((write-region-inhibit-fsync nil)) 88 | (f-touch (concat (file-name-as-directory parent) "foo.txt")) 89 | (f-touch (concat (file-name-as-directory parent) "bar.ipynb")) 90 | (f-write-text "{ 91 | \"cells\": [], 92 | \"metadata\": {}, 93 | \"nbformat\": 4, 94 | \"nbformat_minor\": 2 95 | } 96 | " 'utf-8 (concat (file-name-as-directory parent) "bar.ipynb"))) 97 | (if (< current-depth depth) 98 | (cl-loop for w from 1 to width 99 | for dir = (concat (file-name-as-directory parent) (number-to-string w)) 100 | do (f-mkdir dir) 101 | (ein:testing-make-directory-level dir (1+ current-depth) width depth)))) 102 | 103 | (defun ein:testing-wait-until (predicate &optional predargs ms interval continue) 104 | "Wait until PREDICATE function returns non-`nil'. 105 | PREDARGS is argument list for the PREDICATE function. 106 | MS is milliseconds to wait. INTERVAL is polling interval in milliseconds." 107 | (let* ((int (aif interval it (aif ms (max 300 (/ ms 10)) 300))) 108 | (count (max 1 (if ms (truncate (/ ms int)) 25)))) 109 | (unless (or (cl-loop repeat count 110 | when (apply predicate predargs) 111 | return t 112 | do (accept-process-output nil (/ (float int) 1000)) 113 | finally return nil) 114 | continue) 115 | (cl-assert nil nil "Timeout %s" predicate)))) 116 | 117 | (defun ein:testing-new-notebook (url-or-port ks &optional retry subdir) 118 | (condition-case err 119 | (let (notebook) 120 | (ein:testing-wait-until (lambda () 121 | (ein:notebooklist-list-get url-or-port)) 122 | nil 10000 1000) 123 | (ein:notebooklist-new-notebook url-or-port ks 124 | (lambda (nb _created) 125 | (setq notebook nb)) 126 | nil nil subdir) 127 | (ein:testing-wait-until (lambda () 128 | (aand notebook 129 | (ein:$notebook-kernel it) 130 | (ein:kernel-live-p it))) 131 | nil 20000 1000) 132 | notebook) 133 | (error (let ((notice (format "ein:testing-new-notebook: [%s] %s" 134 | url-or-port (error-message-string err)))) 135 | (if retry 136 | (progn (ein:log 'error notice) nil) 137 | (ein:log 'info notice) 138 | (sleep-for 0 1500) 139 | (ein:testing-new-notebook url-or-port ks t subdir)))))) 140 | 141 | (add-hook 'kill-emacs-hook #'ein:testing-dump-logs) 142 | 143 | (with-eval-after-load "ein-notebook" 144 | ;; if y-or-n-p isn't specially overridden, make it always "no" 145 | (let ((original-y-or-n-p (symbol-function 'y-or-n-p))) 146 | (add-function :around (symbol-function 'ein:notebook-ask-save) 147 | (lambda (f &rest args) 148 | (if (not (eq (symbol-function 'y-or-n-p) original-y-or-n-p)) 149 | (apply f args) 150 | (cl-letf (((symbol-function 'y-or-n-p) (lambda (&rest _args) nil))) 151 | (apply f args))))))) 152 | 153 | (provide 'ein-testing) 154 | 155 | ;;; ein-testing.el ends here 156 | -------------------------------------------------------------------------------- /lisp/ein-core.el: -------------------------------------------------------------------------------- 1 | ;;; ein-core.el --- EIN core -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012 Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-core.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-core.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-core.el. 21 | ;; If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; 26 | 27 | ;;; Code: 28 | 29 | (require 'ein) ; get autoloaded functions into namespace 30 | (require 'ein-utils) 31 | (require 'anaphora) 32 | (require 'request) 33 | 34 | (defgroup ein nil 35 | "IPython notebook client in Emacs" 36 | :group 'applications 37 | :prefix "ein:") 38 | 39 | (define-obsolete-variable-alias 'ein:url-or-port 'ein:urls "0.17.0") 40 | (defcustom ein:urls nil 41 | "List of default urls." 42 | :type '(repeat (choice (string :tag "Remote url") 43 | (integer :tag "Local port" 8888))) 44 | :group 'ein) 45 | 46 | (make-obsolete-variable 'ein:default-url-or-port nil "0.17.0") 47 | 48 | (defconst ein:source-dir (file-name-directory load-file-name) 49 | "Directory in which `ein*.el` files are located.") 50 | 51 | (defun ein:version (&optional interactively copy-to-kill) 52 | "Return a longer version string. 53 | With prefix argument, copy the string to kill ring. 54 | The result contains `ein:version' and either git revision (if 55 | the source is in git repository) or elpa version." 56 | (interactive (list t current-prefix-arg)) 57 | (let* ((version 58 | (or (and (ein:git-root-p 59 | (concat (file-name-as-directory ein:source-dir) "..")) 60 | (let ((default-directory ein:source-dir)) 61 | (ein:git-revision-dirty))) 62 | (and (string-match "/ein-\\([0-9\\.]*\\)/$" ein:source-dir) 63 | (match-string 1 ein:source-dir))))) 64 | (when interactively 65 | (message "EIN version is %s" version)) 66 | (when copy-to-kill 67 | (kill-new version)) 68 | version)) 69 | 70 | ;;; Server attribute getters. These should be moved to ein-open.el 71 | 72 | (defvar *ein:notebook-api-version* (make-hash-table :test #'equal) 73 | "url-or-port to major notebook version") 74 | 75 | (defvar *ein:kernelspecs* (make-hash-table :test #'equal) 76 | "url-or-port to kernelspecs") 77 | 78 | (defun ein:get-kernelspec (url-or-port name &optional lang) 79 | (let* ((kernelspecs (ein:need-kernelspecs url-or-port)) 80 | (name (if (stringp name) 81 | (intern (format ":%s" name)) 82 | name)) 83 | (ks (or (plist-get kernelspecs name) 84 | (cl-loop for (_key spec) on (ein:plist-exclude kernelspecs '(:default)) by 'cddr 85 | if (string= (ein:$kernelspec-language spec) lang) 86 | return spec 87 | end)))) 88 | (cond ((stringp ks) 89 | (ein:get-kernelspec url-or-port ks)) 90 | (t ks)))) 91 | 92 | (defun ein:need-kernelspecs (url-or-port) 93 | "Callers assume ein:query-kernelspecs succeeded. If not, nil." 94 | (aif (gethash url-or-port *ein:kernelspecs*) it 95 | (ein:log 'warn "No recorded kernelspecs for %s" url-or-port) 96 | nil)) 97 | 98 | (defsubst ein:notebook-api-version-numeric (url-or-port) 99 | (truncate (string-to-number (ein:need-notebook-api-version url-or-port)))) 100 | 101 | (defun ein:need-notebook-api-version (url-or-port) 102 | "Callers assume `ein:query-notebook-api-version' succeeded. 103 | If not, we hardcode a guess." 104 | (aif (gethash url-or-port *ein:notebook-api-version*) it 105 | (ein:log 'warn "No recorded notebook version for %s" url-or-port) 106 | "5")) 107 | 108 | (defun ein:generic-getter (func-list) 109 | "Internal function for generic getter functions (`ein:get-*'). 110 | 111 | FUNC-LIST is a list of function which takes no argument and 112 | return what is desired or nil. Each function in FUNC-LIST is 113 | called one by one and the first non-nil result will be used. The 114 | function is not called when it is not bound. So, it is safe to 115 | give functions defined in lazy-loaded sub-modules. 116 | 117 | This is something similar to dispatching in generic function such 118 | as `defgeneric' in EIEIO, but it takes no argument. Actual 119 | implementation is chosen based on context (buffer, point, etc.). 120 | This helps writing generic commands which requires same object 121 | but can operate in different contexts." 122 | (cl-loop for func in func-list 123 | if (and (functionp func) (funcall func)) 124 | return it)) 125 | 126 | (defun ein:get-url-or-port () 127 | (ein:generic-getter '(ein:get-url-or-port--notebooklist 128 | ein:get-url-or-port--notebook 129 | ein:get-url-or-port--worksheet 130 | ein:get-url-or-port--shared-output))) 131 | 132 | (defun ein:get-kernel () 133 | (ein:generic-getter '(ein:get-kernel--notebook 134 | ein:get-kernel--worksheet 135 | ein:get-kernel--shared-output 136 | ein:get-kernel--connect))) 137 | 138 | (defun ein:get-kernel-or-error () 139 | (or (ein:get-kernel) 140 | (error "No kernel related to the current buffer."))) 141 | 142 | (defun ein:get-cell-at-point () 143 | (ein:generic-getter '(ein:get-cell-at-point--worksheet 144 | ein:get-cell-at-point--shared-output))) 145 | 146 | (defun ein:get-traceback-data () 147 | (append (ein:generic-getter '(ein:get-traceback-data--worksheet 148 | ein:get-traceback-data--shared-output 149 | ein:get-traceback-data--connect)) 150 | nil)) 151 | 152 | ;;; Emacs utilities 153 | 154 | (defun ein:clean-compiled-files () 155 | (let* ((files (directory-files ein:source-dir 'full "^ein-.*\\.elc$"))) 156 | (mapc #'delete-file files) 157 | (message "Removed %s byte-compiled files." (length files)))) 158 | 159 | (defun ein:byte-compile-ein () 160 | "Byte compile EIN files." 161 | (interactive) 162 | (ein:clean-compiled-files) 163 | (let* ((files (directory-files ein:source-dir 'full "^ein-.*\\.el$")) 164 | (errors (cl-mapcan (lambda (f) (unless (byte-compile-file f) (list f))) 165 | files))) 166 | (aif errors 167 | (error "Got %s errors while compiling these files: %s" 168 | (length errors) 169 | (ein:join-str " " (mapcar #'file-name-nondirectory it)))) 170 | (message "Compiled %s files" (length files)))) 171 | 172 | (provide 'ein-core) 173 | 174 | ;;; ein-core.el ends here 175 | -------------------------------------------------------------------------------- /lisp/ein-output-area.el: -------------------------------------------------------------------------------- 1 | ;;; ein-output-area.el --- Output area module 2 | 3 | ;; Copyright (C) 2012 Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-output-area.el is free software: you can redistribute it and/or 10 | ;; modify it under the terms of the GNU General Public License as 11 | ;; published by the Free Software Foundation, either version 3 of the 12 | ;; License, or (at your option) any later version. 13 | 14 | ;; ein-output-area.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-output-area.el. 21 | ;; If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; 26 | 27 | ;;; Code: 28 | 29 | (require 'xml) 30 | (require 'shr) 31 | (require 'ein-core) 32 | 33 | 34 | (defvar ein:output-area-case-types '(:image/svg+xml :image/png :image/jpeg :text/plain :text/html :application/latex :application/tex :application/javascript) 35 | "Prefer :text/plain. 36 | Unless it's a single line \"\" or 37 | \"TemporalData[TimeSeries, <<1>>]\" in which case prefer :text/html.") 38 | 39 | (defcustom ein:output-area-inlined-images nil 40 | "Turn on to insert images into buffer. Default spawns external viewer." 41 | :type 'boolean 42 | :group 'ein) 43 | 44 | (defcustom ein:output-area-inlined-image-properties '(:foreground "black" :background "white") 45 | "Additional properties for inlined images. 46 | This is passed to `create-image' for some supported image types, 47 | such as SVG ones whose foregrounds are taken from the current 48 | frame by default and may appear unreadable." 49 | :type '(plist :value-type color) 50 | :group 'ein) 51 | 52 | (defcustom ein:shr-env 53 | '((shr-table-horizontal-line ?-) 54 | (shr-table-vertical-line ?|) 55 | (shr-table-corner ?+)) 56 | "Variables let-bound while calling `shr-insert-document'. 57 | 58 | To use default shr setting: 59 | 60 | (setq ein:shr-env nil) 61 | 62 | Draw boundaries for table (default): 63 | 64 | (setq ein:shr-env 65 | \\='((shr-table-horizontal-line ?-) 66 | (shr-table-vertical-line ?|) 67 | (shr-table-corner ?+))) 68 | " 69 | :type '(sexp) 70 | :group 'ein) 71 | 72 | ;;; XML/HTML utils 73 | 74 | (defun ein:xml-parse-html-string (html-string) 75 | "Parse HTML-STRING and return a dom object which 76 | can be handled by the xml module." 77 | (with-temp-buffer 78 | (insert html-string) 79 | (when (fboundp 'libxml-parse-html-region) 80 | (cl-loop with result 81 | repeat 3 82 | do (setq result 83 | (libxml-parse-html-region (point-min) (point-max))) 84 | until result 85 | finally return result)))) 86 | 87 | (defalias 'ein:xml-node-p 'listp) 88 | 89 | (defun ein:xml-tree-apply (dom operation) 90 | "Apply OPERATION on nodes in DOM. Apply the same OPERATION on 91 | the next level children when it returns `nil'." 92 | (cl-loop for child in (xml-node-children dom) 93 | if (and (not (funcall operation child)) 94 | (ein:xml-node-p child)) 95 | do (ein:xml-tree-apply child operation))) 96 | 97 | (defun ein:xml-replace-attributes (dom tag attr replace-p replacer) 98 | "Replace value of ATTR of TAG in DOM using REPLACER 99 | when REPLACE-P returns non-`nil'." 100 | (ein:xml-tree-apply 101 | dom 102 | (lambda (node) 103 | (ein:and-let* (((ein:xml-node-p node)) 104 | ((eq (xml-node-name node) tag)) 105 | (attr-cell (assoc attr (xml-node-attributes node))) 106 | (val (cdr attr-cell)) 107 | ((funcall replace-p val))) 108 | (setcdr attr-cell (funcall replacer val)) 109 | t)))) 110 | 111 | 112 | (defun ein:output-area-get-html-renderer () 113 | (if (fboundp 'libxml-parse-xml-region) 114 | #'ein:insert-html-shr 115 | #'ein:insert-read-only)) 116 | 117 | (defun ein:shr-insert-document (dom) 118 | "`shr-insert-document' with EIN setting." 119 | (eval `(let ,ein:shr-env (shr-insert-document dom)))) 120 | 121 | (defun ein:insert-html-shr (html-string) 122 | "Render HTML-STRING using `shr-insert-document'. 123 | 124 | Usage:: 125 | 126 | (ein:insert-html-shr \"HTML string\") 127 | 128 | " 129 | (let ((dom (ein:xml-parse-html-string html-string)) 130 | (start (point)) 131 | end 132 | (buffer-undo-list t)) 133 | (ein:insert-html--fix-urls dom) 134 | (ein:shr-insert-document dom) 135 | (setq end (point)) 136 | (put-text-property start end 'read-only t) 137 | (put-text-property start end 'front-sticky t))) 138 | 139 | (defun ein:insert-html--fix-urls (dom &optional url-or-port) 140 | "Destructively prepend notebook server URL to local URLs in DOM." 141 | (ein:and-let* ((url-or-port (or url-or-port (ein:get-url-or-port))) 142 | (replace-p (lambda (val) (string-match-p "^/?files/" val))) 143 | (replacer (lambda (val) (ein:url url-or-port val)))) 144 | (ein:xml-replace-attributes dom 'a 'href replace-p replacer) 145 | (ein:xml-replace-attributes dom 'img 'src replace-p replacer))) 146 | 147 | (defun ein:output-area-type (mime-type) 148 | "Investigate why :image/svg+xml to :svg and :text/plain to :text" 149 | (let* ((mime-str (if (symbolp mime-type) (symbol-name mime-type) mime-type)) 150 | (minor-kw (car (nreverse (split-string mime-str "/")))) 151 | (minor (car (nreverse (split-string minor-kw ":"))))) 152 | (intern (concat ":" 153 | (cond ((string= minor "plain") "text") 154 | (t (cl-subseq minor 0 (cl-search "+" minor)))))))) 155 | 156 | (defun ein:output-area-convert-mime-types (json data) 157 | (let ((known-mimes (cl-remove-if-not 158 | #'identity 159 | (mapcar (lambda (x) (intern-soft (concat ":" x))) 160 | (mailcap-mime-types))))) 161 | (mapc (lambda (x) 162 | (-when-let* ((mime-val (plist-get data x)) 163 | (minor-kw (ein:output-area-type x))) 164 | (setq json (plist-put json minor-kw mime-val)))) 165 | known-mimes) 166 | json)) 167 | 168 | (defmacro ein:output-area-case-type (json &rest case-body) 169 | `(let* ((types (cl-copy-list ein:output-area-case-types)) 170 | (heuristic-p (and (memq :text/plain types) 171 | (memq :text/html types))) 172 | (,json (or (plist-get ,json :data) ,json)) 173 | (plain (plist-get ,json :text/plain)) 174 | (html (plist-get ,json :text/html))) 175 | (when (and heuristic-p 176 | (stringp plain) (< (length plain) 60) 177 | (stringp html) (> (length html) 300)) 178 | (delq :text/plain types)) 179 | (seq-some (lambda (type) 180 | (when-let ((value (plist-get ,json type))) 181 | ,@case-body 182 | t)) 183 | types))) 184 | 185 | (provide 'ein-output-area) 186 | 187 | ;;; ein-output-area.el ends here 188 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export EMACS ?= $(shell which emacs) 2 | export CASK := $(shell which cask) 3 | TEST_VERSION := $(shell python -c "from yq import cli; import sys; sys.exit(cli())" .jobs.build.strategy.matrix.emacs_version .github/workflows/test.yml | jq .[] | egrep "[0-9]" | sort -n | head -1) 4 | ifeq ($(CASK),) 5 | $(error Please install CASK at https://cask.readthedocs.io/en/latest/guide/installation.html) 6 | endif 7 | CASK_DIR := $(shell EMACS=$(EMACS) $(CASK) package-directory || exit 1) 8 | SRC=$(shell $(CASK) files) 9 | PKBUILD=2.3 10 | ELCFILES = $(SRC:.el=.elc) 11 | ifeq ($(TRAVIS_PULL_REQUEST_SLUG),) 12 | TRAVIS_PULL_REQUEST_SLUG := $(shell git config --global user.name)/$(shell basename `git rev-parse --show-toplevel`) 13 | endif 14 | ifeq ($(TRAVIS_PULL_REQUEST_BRANCH),) 15 | TRAVIS_PULL_REQUEST_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 16 | endif 17 | ifeq ($(TRAVIS),true) 18 | ifeq ($(TRAVIS_PULL_REQUEST_SHA),) 19 | TRAVIS_PULL_REQUEST_SHA := $(shell if git show-ref --quiet --verify origin/$(TRAVIS_PULL_REQUEST_BRANCH) ; then git rev-parse origin/$(TRAVIS_PULL_REQUEST_BRANCH) ; fi)) 20 | endif 21 | endif 22 | 23 | .DEFAULT_GOAL := test-compile 24 | 25 | README.rst: cask README.in.rst lisp/ein.el lisp/ein-notebook.el 26 | $(CASK) eval "(progn \ 27 | (add-to-list 'load-path \"./lisp\") \ 28 | (load \"ein-notebook\") \ 29 | (describe-minor-mode \"ein:notebook-mode\") \ 30 | (with-current-buffer \"*Help*\" (princ (buffer-string))))" 2>/dev/null \ 31 | | tools/readme-sed.sh "KEYS NOTEBOOK" README.in.rst "key.*binding" > README.rst0 32 | perl -ne "s/^((\s*)C-c C-c(\s+).*)$$/\$${1}\n\$${2}C-u C-c C-c \$${3}ein:worksheet-execute-all-cells/; print" README.rst0 > README.rst1 33 | perl -ne "s/^(\s*)\S+.*CI VERSION.*$$/\$${1}$(TEST_VERSION)/; print" README.rst1 > README.rst0 34 | grep ';;' lisp/ein.el \ 35 | | gawk '/;;;\s*Commentary/{within=1;next}/;;;\s*/{within=0}within' \ 36 | | sed -e 's/^\s*;;*\s*//g' \ 37 | | tools/readme-sed.sh "COMMENTARY" README.rst0 > README.rst 38 | rm README.rst0 README.rst1 39 | 40 | .PHONY: autoloads 41 | autoloads: 42 | $(CASK) emacs -Q --batch -l package --eval "(let ((v (format \"%s.%s\" emacs-major-version emacs-minor-version))) (custom-set-variables (backquote (package-user-dir ,(concat \".cask/\" v)))))" -f package-initialize --eval "(package-generate-autoloads \"ein\" \"./lisp\")" 43 | 44 | .PHONY: clean 45 | clean: 46 | $(CASK) clean-elc 47 | rm -rf test/test-install 48 | rm -f test/curl-cookie-jar 49 | rm -rf log 50 | rm -f features/[Uu]ntitled* 51 | rm -f features/Renamed.ipynb 52 | rm -f ert-profile* 53 | rm -rf features/test-repo 54 | rm -f gmon.out 55 | 56 | .PHONY: cask 57 | cask: $(CASK_DIR) 58 | 59 | $(CASK_DIR): Cask 60 | $(CASK) install 61 | touch $(CASK_DIR) 62 | 63 | .PHONY: test-compile 64 | test-compile: clean 65 | ! ($(CASK) eval "(let ((byte-compile-error-on-warn t)) (cask-cli/build))" 2>&1 | egrep -a "(Warning|Error):") ; (ret=$$? ; $(CASK) clean-elc && exit $$ret) 66 | 67 | .PHONY: quick 68 | quick: $(CASK_DIR) autoloads test-compile test-ob-ein-recurse test-unit 69 | 70 | # Last I tried, I can get jupyterhub to launch a user server 71 | # on a random port 58153, but that instance is still-born, i.e., 72 | # it posts 404 from the browser. SimpleLocalProcessSpawner 73 | # results in Module not found. 74 | .PHONY: test-jupyterhub 75 | test-jupyterhub: test-compile 76 | $(CASK) exec ecukes --tags @jupyterhub --reporter magnars 77 | 78 | .PHONY: test 79 | test: quick test-int 80 | 81 | .PHONY: test-int 82 | test-int: clean 83 | $(CASK) exec ecukes --reporter magnars --debug 84 | 85 | .PHONY: test-unit 86 | test-unit: 87 | $(CASK) exec ert-runner -L ./lisp -L ./test -l test/testein.el test/test-ein*.el 88 | 89 | .PHONY: test-ob-ein-recurse 90 | test-ob-ein-recurse: 91 | $(CASK) eval "(progn (require 'cl) (custom-set-variables (quote (org-babel-load-languages (quote ((emacs-lisp . t) (ein . t)))))) (org-version))" 92 | 93 | .PHONY: travis-install 94 | travis-install: 95 | mkdir -p test/test-install 96 | if [ ! -s "test/test-install/$(PKBUILD).tar.gz" ] ; then \ 97 | cd test/test-install ; curl -sLOk https://github.com/melpa/package-build/archive/$(PKBUILD).tar.gz ; fi 98 | cd test/test-install ; tar xfz $(PKBUILD).tar.gz 99 | cd test/test-install ; rm -f $(PKBUILD).tar.gz 100 | cd test/test-install/package-build-$(PKBUILD) ; make -s loaddefs 101 | mkdir -p test/test-install/recipes 102 | cd test/test-install/recipes ; curl -sLOk https://raw.githubusercontent.com/melpa/melpa/master/recipes/ein 103 | $(EMACS) -Q --batch -L test/test-install/package-build-$(PKBUILD) \ 104 | --eval "(require 'package-build)" \ 105 | --eval "(require 'subr-x)" \ 106 | --eval "(package-initialize)" \ 107 | --eval "(add-to-list 'package-archives '(\"melpa\" . \"http://melpa.org/packages/\"))" \ 108 | --eval "(package-refresh-contents)" \ 109 | --eval "(setq rcp (package-recipe-lookup \"ein\"))" \ 110 | --eval "(unless (file-exists-p package-build-archive-dir) \ 111 | (make-directory package-build-archive-dir))" \ 112 | --eval "(let* ((my-repo \"$(TRAVIS_PULL_REQUEST_SLUG)\") \ 113 | (my-branch \"$(TRAVIS_PULL_REQUEST_BRANCH)\") \ 114 | (my-commit \"$(TRAVIS_PULL_REQUEST_SHA)\")) \ 115 | (oset rcp :repo my-repo) \ 116 | (oset rcp :branch my-branch) \ 117 | (oset rcp :commit my-commit))" \ 118 | --eval "(let ((version (package-build--checkout rcp))) \ 119 | (delete-directory (expand-file-name (concat \"ein-\" version) package-user-dir) t) \ 120 | (package-build--package rcp version))" \ 121 | --eval "(package-install-file (car (file-expand-wildcards (concat package-build-archive-dir \"ein*.tar\"))))" 2>&1 | tee /tmp/test-install.out 122 | ! ( egrep -a "Error: " /tmp/test-install.out ) 123 | 124 | .PHONY: dist-clean 125 | dist-clean: 126 | rm -rf dist 127 | 128 | .PHONY: dist 129 | dist: dist-clean autoloads 130 | $(CASK) package 131 | 132 | .PHONY: backup-melpa 133 | backup-melpa: 134 | $(EMACS) -Q --batch --eval "(package-initialize)" --eval \ 135 | "(with-temp-buffer \ 136 | (insert-file-contents-literally (car (file-expand-wildcards \"dist/ein*.tar\"))) \ 137 | (tar-mode) \ 138 | (let* ((my-desc (package-tar-file-info)) \ 139 | (name (package-desc-name my-desc)) \ 140 | (other-pkgs (cdr (assq name package-alist)))) \ 141 | (mapc (lambda (odesc) \ 142 | (let* ((odir (package-desc-dir odesc)) \ 143 | (parent (file-name-directory odir)) \ 144 | (leaf (file-name-nondirectory odir))) \ 145 | (if (equal (package-desc-version my-desc) \ 146 | (package-desc-version odesc)) \ 147 | (delete-directory odir t) \ 148 | (rename-file odir \ 149 | (expand-file-name (format \"BACKUP-%s\" leaf) parent) \ 150 | t)))) \ 151 | other-pkgs)))" 152 | 153 | .PHONY: install 154 | install: dist backup-melpa 155 | $(EMACS) -Q --batch --eval "(package-initialize)" \ 156 | --eval "(add-to-list 'package-archives '(\"shmelpa\" . \"https://shmelpa.commandlinesystems.com/packages/\"))" \ 157 | --eval "(package-refresh-contents)" \ 158 | --eval "(package-install-file (car (file-expand-wildcards \"dist/ein*.tar\")))" 159 | -------------------------------------------------------------------------------- /lisp/ein-notification.el: -------------------------------------------------------------------------------- 1 | ;;; ein-notification.el --- Notification widget for Notebook -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-notification.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-notification.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-notification.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | 29 | (require 'eieio) 30 | 31 | (require 'ein-core) 32 | (require 'ein-classes) 33 | (require 'ein-events) 34 | 35 | (declare-function ein:get-notebook "ein:notebook") 36 | (declare-function ein:notebook-opened-buffer-names "ein:notebook") 37 | (declare-function ein:list-available-kernels "ein:notebook") 38 | (declare-function ein:notebook-switch-kernel "ein:notebook") 39 | 40 | (define-obsolete-variable-alias 'ein:@notification 'ein:%notification% "0.1.2") 41 | (ein:deflocal ein:%notification% nil 42 | "Buffer local variable to hold an instance of `ein:notification'.") 43 | 44 | (defvar ein:header-line-format '(:eval (ein:header-line))) 45 | (defvar ein:header-line-switch-kernel-map (make-sparse-keymap)) 46 | 47 | (cl-defmethod ein:notification-status-set ((ns ein:notification-status) status) 48 | (let* ((message (cdr (assoc status (slot-value ns 's2m))))) 49 | (setf (slot-value ns 'status) status) 50 | (setf (slot-value ns 'message) (substitute-command-keys message)) 51 | (force-mode-line-update t))) 52 | 53 | (cl-defmethod ein:notification-bind-events ((notification ein:notification) events) 54 | "Bind a callback to events of the event handler EVENTS which 55 | just set the status (= event-type): 56 | (ein:notification-status-set NS EVENT-TYPE) 57 | where NS is `:kernel' or `:notebook' slot of NOTIFICATION." 58 | (cl-loop for ns in (list (slot-value notification 'kernel) 59 | (slot-value notification 'notebook)) 60 | for statuses = (mapcar #'car (slot-value ns 's2m)) 61 | do (cl-loop for st in statuses 62 | do (ein:events-on events 63 | st ; = event-type 64 | #'ein:notification--callback 65 | (cons ns st)))) 66 | (ein:events-on events 67 | 'notebook_saved.Notebook 68 | #'ein:notification--fadeout-callback 69 | (list (slot-value notification 'notebook) 70 | "Notebook is saved" 71 | 'notebook_saved.Notebook 72 | nil)) 73 | (ein:events-on events 74 | 'execution_count.Kernel 75 | #'ein:notification--set-execution-count 76 | notification)) 77 | 78 | (defun ein:notification--callback (packed _data) 79 | (let ((ns (car packed)) 80 | (status (cdr packed))) 81 | (ein:notification-status-set ns status))) 82 | 83 | (defun ein:notification--set-execution-count (notification count) 84 | (setf (oref notification :execution-count) count)) 85 | 86 | (defun ein:notification--fadeout-callback (packed _data) 87 | ;; FIXME: I can simplify this. 88 | ;; Do not pass around message, for exmaple. 89 | (cl-destructuring-bind (ns message status &rest) packed 90 | (setf (oref ns :status) status) 91 | (setf (oref ns :message) message) 92 | (apply #'run-at-time 93 | 1 nil 94 | (lambda (ns _message status next) 95 | (when (equal (slot-value ns 'status) status) 96 | (ein:notification-status-set ns next) 97 | ;; (ein:with-live-buffer (slot-value ns :buffer) 98 | ;; (force-mode-line-update)) 99 | )) 100 | packed))) 101 | 102 | (defun ein:notification-setup (buffer events &rest tab-slots) 103 | "Setup a new notification widget in the BUFFER. 104 | This function saves the new notification widget instance in the 105 | local variable of the BUFFER. 106 | 107 | Rest of the arguments are for TABs in `header-line'. 108 | 109 | GET-LIST : function 110 | Return a list of worksheets. 111 | 112 | GET-CURRENT : function 113 | Return the current worksheet. 114 | 115 | GET-NAME : function 116 | Return a name of the worksheet given as its argument. 117 | 118 | \(fn buffer events &key get-list get-current)" 119 | (with-current-buffer buffer 120 | (setq ein:%notification% 121 | (make-instance 'ein:notification 122 | :buffer buffer)) 123 | (setq header-line-format ein:header-line-format) 124 | (ein:notification-bind-events ein:%notification% events) 125 | (setf (oref ein:%notification% :tab) 126 | (apply #'make-instance 'ein:notification-tab tab-slots)) 127 | ein:%notification%)) 128 | 129 | (defface ein:notification-tab-normal 130 | '((t :inherit (header-line) :underline t :height 0.8)) 131 | "Face for headline selected tab." 132 | :group 'ein) 133 | 134 | (define-key ein:header-line-switch-kernel-map 135 | [header-line mouse-1] 'ein:header-line-switch-kernel) 136 | 137 | (defmacro ein:with-destructuring-bind-key-event (key-event &rest body) 138 | (declare (debug (form &rest form)) 139 | (indent 1)) 140 | ;; See: (info "(elisp) Click Events") 141 | `(cl-destructuring-bind 142 | (event-type 143 | (window pos-or-area (x . y) timestamp 144 | object text-pos (col . row) 145 | image (dx . dy) (width . height))) 146 | ,key-event 147 | ,@body)) 148 | 149 | (defun ein:header-line-switch-kernel (_key-event) 150 | (interactive "e") 151 | (let* ((notebook (or (ein:get-notebook) 152 | (ein:completing-read 153 | "Select notebook: " 154 | (ein:notebook-opened-buffer-names)))) 155 | (kernel-name (ein:completing-read 156 | "Select kernel: " 157 | (ein:list-available-kernels (ein:$notebook-url-or-port notebook))))) 158 | (ein:notebook-switch-kernel notebook kernel-name))) 159 | 160 | (defun ein:header-line () 161 | (format 162 | "IP[%s]: %s" 163 | (slot-value ein:%notification% 'execution-count) 164 | (ein:join-str 165 | " | " 166 | (cl-remove-if-not 167 | #'identity 168 | (list (slot-value (slot-value ein:%notification% 'notebook) 'message) 169 | (slot-value (slot-value ein:%notification% 'kernel) 'message) 170 | (propertize (aif (aand (ein:get-notebook) (ein:$notebook-kernelspec it)) 171 | (format "|%s|" (ein:$kernelspec-name it)) 172 | "|unknown: please click and select a kernel|") 173 | 'keymap ein:header-line-switch-kernel-map 174 | 'help-echo "Click (mouse-1) to change the running kernel." 175 | 'mouse-face 'highlight 176 | 'face 'ein:notification-tab-normal)))))) 177 | 178 | (provide 'ein-notification) 179 | 180 | ;;; ein-notification.el ends here 181 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | 3 | # you can use {branch} name in version format too 4 | # version: 1.0.{build}-{branch} 5 | 6 | # Do not build on tags (GitHub and BitBucket) 7 | skip_tags: true 8 | 9 | # Skipping commits with particular message or from specific user 10 | skip_commits: 11 | message: /\[skip appveyor\]/ # Regex for matching commit message 12 | 13 | # Do not build feature branch with open Pull Requests 14 | # skip_branch_with_pr: true 15 | 16 | # Maximum number of concurrent jobs for the project 17 | max_jobs: 1 18 | 19 | # Build worker image (VM template) 20 | image: Visual Studio 2017 21 | 22 | # scripts that are called at very beginning, before repo cloning 23 | init: 24 | - git config --global core.autocrlf input 25 | 26 | # clone directory 27 | clone_folder: c:\projects\myproject 28 | 29 | # fetch repository as zip archive 30 | shallow_clone: true # default is "false" 31 | 32 | # set clone depth 33 | clone_depth: 5 # clone entire repository history if not defined 34 | 35 | # setting up etc\hosts file 36 | hosts: 37 | queue-server: 127.0.0.1 38 | db.server.com: 127.0.0.2 39 | 40 | # environment variables 41 | environment: 42 | # this is how to set encrypted variable. Go to "Settings" -> "Encrypt YAML" page in account menu to encrypt data. 43 | my_secure_var1: 44 | secure: FW3tJ3fMncxvs58/ifSP7w== 45 | 46 | # environment: 47 | # global: 48 | # connection_string: server=12;password=13; 49 | # service_url: https://127.0.0.1:8090 50 | # 51 | # matrix: 52 | # - db: mysql 53 | # provider: mysql 54 | # 55 | # - db: mssql 56 | # provider: mssql 57 | # password: 58 | # secure: $#(JFDA)jQ@#$ 59 | 60 | # this is how to allow failing jobs in the matrix 61 | matrix: 62 | fast_finish: true # set this flag to immediately finish build once one of the jobs fails. 63 | allow_failures: 64 | - platform: x86 65 | configuration: Debug 66 | - platform: x64 67 | configuration: Release 68 | 69 | # exclude configuration from the matrix. Works similarly to 'allow_failures' but build not even being started for excluded combination. 70 | exclude: 71 | - platform: x86 72 | configuration: Debug 73 | 74 | # build cache to preserve files/folders between builds 75 | cache: 76 | # - packages -> **\packages.config # preserve "packages" directory in the root of build folder but will reset it if packages.config is modified 77 | - C:\python27\ 78 | - C:\ProgramData\chocolatey\lib 79 | - C:\ProgramData\chocolatey\bin 80 | # - '%LocalAppData%' 81 | # - node_modules # local npm modules 82 | - '%LocalAppData%\NuGet\Cache' # NuGet < v3 83 | - '%LocalAppData%\NuGet\v3-cache' # NuGet v3 84 | 85 | # enable service required for build/tests 86 | #services: 87 | # - mssql2014 # start SQL Server 2014 Express 88 | # - mssql2014rs # start SQL Server 2014 Express and Reporting Services 89 | # - mssql2012sp1 # start SQL Server 2012 SP1 Express 90 | # - mssql2012sp1rs # start SQL Server 2012 SP1 Express and Reporting Services 91 | # - mssql2008r2sp2 # start SQL Server 2008 R2 SP2 Express 92 | # - mssql2008r2sp2rs # start SQL Server 2008 R2 SP2 Express and Reporting Services 93 | # - mysql # start MySQL 5.6 service 94 | # - postgresql # start PostgreSQL 9.5 service 95 | # - iis # start IIS 96 | # - msmq # start Queuing services 97 | # - mongodb # start MongoDB 98 | 99 | # scripts that run after cloning repository 100 | 101 | # enable patching of AssemblyInfo.* files 102 | # assembly_info: 103 | # patch: true 104 | # file: AssemblyInfo.* 105 | # assembly_version: "2.2.{build}" 106 | # assembly_file_version: "{version}" 107 | # assembly_informational_version: "{version}" 108 | 109 | 110 | # Automatically register private account and/or project AppVeyor NuGet feeds. 111 | #nuget: 112 | # account_feed: true 113 | # project_feed: true 114 | # disable_publish_on_pr: true # disable publishing of .nupkg artifacts to 115 | # # account/project feeds for pull request builds 116 | 117 | #---------------------------------# 118 | # build configuration # 119 | #---------------------------------# 120 | 121 | # build platform, i.e. x86, x64, Any CPU. This setting is optional. 122 | #platform: Any CPU 123 | 124 | # to add several platforms to build matrix: 125 | #platform: 126 | # - x86 127 | # - Any CPU 128 | 129 | # build Configuration, i.e. Debug, Release, etc. 130 | # configuration: Release 131 | 132 | # to add several configurations to build matrix: 133 | #configuration: 134 | # - Debug 135 | # - Release 136 | 137 | # Build settings, not to be confused with "before_build" and "after_build". 138 | # "project" is relative to the original build directory and not influenced by directory changes in "before_build". 139 | #build: 140 | # parallel: true # enable MSBuild parallel builds 141 | # project: MyTestAzureCS.sln # path to Visual Studio solution or project 142 | # publish_wap: true # package Web Application Projects (WAP) for Web Deploy 143 | # publish_wap_xcopy: true # package Web Application Projects (WAP) for XCopy deployment 144 | # publish_azure: true # package Azure Cloud Service projects and push to artifacts 145 | # publish_nuget: true # package projects with .nuspec files and push to artifacts 146 | # publish_nuget_symbols: true # generate and publish NuGet symbol packages 147 | # include_nuget_references: true # add -IncludeReferencedProjects option while packaging NuGet artifacts 148 | 149 | # MSBuild verbosity level 150 | # verbosity: quiet|minimal|normal|detailed 151 | 152 | 153 | # to disable automatic builds 154 | build: off 155 | 156 | #---------------------------------# 157 | # tests configuration # 158 | #---------------------------------# 159 | 160 | # to run tests against only selected assemblies and/or categories 161 | #test: 162 | # assemblies: 163 | # only: 164 | # - asm1.dll 165 | # - asm2.dll 166 | # 167 | # categories: 168 | # only: 169 | # - UI 170 | # - E2E 171 | 172 | # to run tests against all except selected assemblies and/or categories 173 | #test: 174 | # assemblies: 175 | # except: 176 | # - asm1.dll 177 | # - asm2.dll 178 | # 179 | # categories: 180 | # except: 181 | # - UI 182 | # - E2E 183 | 184 | # to run tests from different categories as separate jobs in parallel 185 | #test: 186 | # categories: 187 | # - A # A category common for all jobs 188 | # - [UI] # 1st job 189 | # - [DAL, BL] # 2nd job 190 | 191 | # scripts to run before tests (working directory and environment changes are persisted from the previous steps such as "before_build") 192 | before_test: 193 | 194 | # to run your custom scripts instead of automatic tests 195 | test_script: 196 | - cd c:\projects\myproject 197 | # - make quick 198 | 199 | # scripts to run after tests 200 | after_test: 201 | 202 | # to disable automatic tests 203 | test: off 204 | 205 | # to disable deployment 206 | deploy: off 207 | 208 | #---------------------------------# 209 | # global handlers # 210 | #---------------------------------# 211 | 212 | # on successful build 213 | #on_success: 214 | 215 | # on build failure 216 | #on_failure: 217 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 218 | 219 | # after build failure or success 220 | on_finish: 221 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 222 | -------------------------------------------------------------------------------- /test/test-ein-utils.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding:t -*- 2 | (require 'ert) 3 | 4 | (require 'ein) 5 | (require 'ein-utils) 6 | 7 | (ert-deftest ein-url-simple () 8 | (should (null (ein:url nil))) 9 | (should (equal (ein:url 8888) "http://127.0.0.1:8888")) 10 | (should (equal (ein:url "http://:8000") "http://127.0.0.1:8000")) 11 | (should (equal (ein:url "http://localhost") "http://127.0.0.1")) 12 | (should (equal (ein:url "https://localhost:8888") "https://127.0.0.1:8888")) 13 | (should (equal (ein:url "http://localhost:8000" "" "" "" "Untitled.ipynb") "http://127.0.0.1:8000/Untitled.ipynb")) 14 | (should (equal (ein:url "http://localhost:8000" "") "http://127.0.0.1:8000")) 15 | (should (equal (ein:url "http://localhost:8000") "http://127.0.0.1:8000")) 16 | (should (equal (ein:url "localhost" "foo" "bar") "http://127.0.0.1/foo/bar")) 17 | (should (equal (ein:url "https://localhost" "foo" "bar") "https://127.0.0.1/foo/bar")) 18 | (should (equal (ein:url "datasci-1:8888") "https://datasci-1:8888")) 19 | (should (equal (ein:url "datasci-1" "foo" "bar") "https://datasci-1/foo/bar")) 20 | (should (equal (ein:glom-paths "" nil "Untitled1.ipynb") "Untitled1.ipynb")) 21 | (should (equal (ein:glom-paths) "")) 22 | (cl-loop for url in '("http://127.0.0.1:8888" "http://localhost:8888" "http://127.0.0.1:8888/" "http://localhost:8888/" "8888" 8888) 23 | do (should (equal (ein:url "http://localhost:8888") (ein:url url))))) 24 | 25 | (ert-deftest ein-url-slashes () 26 | (cl-loop for a in '("a" "a/" "/a") 27 | do (cl-loop for b in '("b" "/b") 28 | do (should (equal (ein:url 8888 a b) 29 | "http://127.0.0.1:8888/a/b"))) 30 | do (should (equal (ein:url 8888 a "b/") 31 | "http://127.0.0.1:8888/a/b")))) 32 | 33 | (ert-deftest ein-trim-simple () 34 | (should (equal (ein:trim "a") "a")) 35 | (should (equal (ein:trim " a ") "a")) 36 | (should (equal (ein:trim "\na\n") "a"))) 37 | 38 | (ert-deftest ein-trim-middle-spaces () 39 | (should (equal (ein:trim "a b") "a b")) 40 | (should (equal (ein:trim " a b ") "a b")) 41 | (should (equal (ein:trim "\na b\n") "a b"))) 42 | 43 | (ert-deftest ein-trim-left-simple () 44 | (should (equal (ein:trim-left "a") "a")) 45 | (should (equal (ein:trim-left " a ") "a ")) 46 | (should (equal (ein:trim-left "\na\n") "a\n"))) 47 | 48 | (ert-deftest ein-trim-right-simple () 49 | (should (equal (ein:trim-right "a") "a")) 50 | (should (equal (ein:trim-right " a ") " a")) 51 | (should (equal (ein:trim-right "\na\n") "\na"))) 52 | 53 | (ert-deftest ein:trim-indent-empty () 54 | (should (equal (ein:trim-indent "") ""))) 55 | 56 | (ert-deftest ein:trim-indent-one-line () 57 | (should (equal (ein:trim-indent "one line") "one line"))) 58 | 59 | (ert-deftest ein:trim-indent-one-newline () 60 | (should (equal (ein:trim-indent "one line\n") "one line\n"))) 61 | 62 | (ert-deftest ein:trim-indent-multi-lines-no-trim () 63 | (let ((original "\ 64 | def func(): 65 | pass 66 | ") 67 | (trimmed "\ 68 | def func(): 69 | pass 70 | ")) 71 | (should (equal (ein:trim-indent original) trimmed)))) 72 | 73 | (ert-deftest ein:trim-indent-multi-lines-one-trim () 74 | (let ((original "\ 75 | def func(): 76 | pass 77 | ") 78 | (trimmed "\ 79 | def func(): 80 | pass 81 | ")) 82 | (should (equal (ein:trim-indent original) trimmed)))) 83 | 84 | (ert-deftest ein:trim-indent-multi-lines-with-empty-lines () 85 | (let ((original "\ 86 | 87 | def func(): 88 | pass 89 | 90 | ") 91 | (trimmed "\ 92 | 93 | def func(): 94 | pass 95 | 96 | ")) 97 | (should (equal (ein:trim-indent original) trimmed)))) 98 | 99 | 100 | ;;; Text manipulation on buffer 101 | 102 | (ert-deftest ein:find-leftmost-column-simple-cases () 103 | (cl-loop for (indent text) in 104 | '(;; No indent 105 | (0 "\ 106 | def f(): 107 | pass") 108 | ;; Indented python code 109 | (4 "\ 110 | def f(): 111 | pass") 112 | ;; Deeper indent can come first 113 | (4 "\ 114 | # indent = 8 115 | # indent 4") 116 | ;; With empty lines 117 | (4 "\ 118 | 119 | # indent = 8 120 | 121 | # indent 4 122 | 123 | ") 124 | ) 125 | do (with-temp-buffer 126 | (insert text) 127 | (should (= (ein:find-leftmost-column (point-min) (point-max)) 128 | indent))))) 129 | 130 | 131 | ;;; Misc 132 | 133 | (ert-deftest ein:list-insert-after () 134 | (should (equal (ein:list-insert-after '(a) 'a 'X) '(a X))) 135 | (should (equal (ein:list-insert-after '(a b c) 'a 'X) '(a X b c))) 136 | (should (equal (ein:list-insert-after '(a b c) 'b 'X) '(a b X c))) 137 | (should (equal (ein:list-insert-after '(a b c) 'c 'X) '(a b c X))) 138 | (should-error (ein:list-insert-after '(a b c) 'd 'X))) 139 | 140 | (ert-deftest ein:list-insert-before () 141 | (should (equal (ein:list-insert-before '(a) 'a 'X) '(X a))) 142 | (should (equal (ein:list-insert-before '(a b c) 'a 'X) '(X a b c))) 143 | (should (equal (ein:list-insert-before '(a b c) 'b 'X) '(a X b c))) 144 | (should (equal (ein:list-insert-before '(a b c) 'c 'X) '(a b X c))) 145 | (should-error (ein:list-insert-before '(a b c) 'd 'X))) 146 | 147 | (ert-deftest ein:list-move-left () 148 | (should (equal (ein:list-move-left '(a) 'a) '(a))) 149 | (should (equal (ein:list-move-left '(a b) 'a) '(b a))) 150 | (should (equal (ein:list-move-left '(a b) 'b) '(b a))) 151 | (should (equal (ein:list-move-left '(a b c d) 'a) '(b c d a))) 152 | (should (equal (ein:list-move-left '(a b c d) 'b) '(b a c d))) 153 | (should (equal (ein:list-move-left '(a b c d) 'c) '(a c b d))) 154 | (should (equal (ein:list-move-left '(a b c d) 'd) '(a b d c))) 155 | (should-error (ein:list-move-left '(a b c d) 'X))) 156 | 157 | (ert-deftest ein:list-move-right () 158 | (should (equal (ein:list-move-right '(a) 'a) '(a))) 159 | (should (equal (ein:list-move-right '(a b) 'a) '(b a))) 160 | (should (equal (ein:list-move-right '(a b) 'b) '(b a))) 161 | (should (equal (ein:list-move-right '(a b c d) 'a) '(b a c d))) 162 | (should (equal (ein:list-move-right '(a b c d) 'b) '(a c b d))) 163 | (should (equal (ein:list-move-right '(a b c d) 'c) '(a b d c))) 164 | (should (equal (ein:list-move-right '(a b c d) 'd) '(d a b c))) 165 | (should-error (ein:list-move-right '(a b c d) 'X))) 166 | 167 | (defvar test-ein-utils-setting) 168 | (defun ein:testing-choose-setting-should-equal (setting value desired &optional single-p) 169 | (setq test-ein-utils-setting setting) 170 | (should (equal (ein:choose-setting 'test-ein-utils-setting value single-p) desired))) 171 | 172 | (ert-deftest ein:choose-setting-single-string () 173 | (let ((test 'ein:testing-choose-setting-should-equal)) 174 | (funcall test "a" nil "a") 175 | (funcall test "a" 'whatever "a"))) 176 | 177 | (ert-deftest ein:choose-setting-single-int () 178 | (let ((test #'ein:testing-choose-setting-should-equal)) 179 | (funcall test 1 nil 1 #'integerp) 180 | (funcall test 1 'whatever 1 #'integerp))) 181 | 182 | (ert-deftest ein:choose-setting-alist () 183 | (let ((test (lambda (&rest args) 184 | (apply #'ein:testing-choose-setting-should-equal 185 | '(("a" . 1) ("b" . 2) ("c" . 3)) 186 | args)))) 187 | (funcall test "a" 1) 188 | (funcall test "b" 2))) 189 | 190 | (ert-deftest ein:choose-setting-func () 191 | (let* ((test (lambda (&rest args) 192 | (apply #'ein:testing-choose-setting-should-equal 193 | (lambda (_x) 1) 194 | args)))) 195 | (funcall test nil 1) 196 | (funcall test 'whatever 1))) 197 | -------------------------------------------------------------------------------- /lisp/ein-traceback.el: -------------------------------------------------------------------------------- 1 | ;;; ein-traceback.el --- Traceback module -*- lexical-binding:t -*- 2 | 3 | ;; Copyright (C) 2012- Takafumi Arakaki 4 | 5 | ;; Author: Takafumi Arakaki 6 | 7 | ;; This file is NOT part of GNU Emacs. 8 | 9 | ;; ein-traceback.el is free software: you can redistribute it and/or modify 10 | ;; it under the terms of the GNU General Public License as published by 11 | ;; the Free Software Foundation, either version 3 of the License, or 12 | ;; (at your option) any later version. 13 | 14 | ;; ein-traceback.el is distributed in the hope that it will be useful, 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ;; GNU General Public License for more details. 18 | 19 | ;; You should have received a copy of the GNU General Public License 20 | ;; along with ein-traceback.el. If not, see . 21 | 22 | ;;; Commentary: 23 | 24 | ;; 25 | 26 | ;;; Code: 27 | 28 | (require 'eieio) 29 | (require 'ewoc) 30 | (require 'ansi-color) 31 | 32 | (require 'ein-core) 33 | (require 'ein-shared-output) 34 | 35 | (declare-function ein:get-notebook "ein-notebook") 36 | (declare-function ein:notebook-buffer "ein-notebook") 37 | 38 | (defclass ein:traceback () 39 | ((tb-data :initarg :tb-data :type list) 40 | (notebook :initarg :source-notebook ;; :type ein:$notebook 41 | :accessor ein:traceback-notebook) 42 | (buffer-name :initarg :buffer-name :type string) 43 | (buffer :initarg :buffer :type buffer) 44 | (ewoc :initarg :ewoc :type ewoc))) 45 | 46 | (ein:deflocal ein:%traceback% nil 47 | "Buffer local variable to store an instance of `ein:traceback'.") 48 | 49 | (defvar ein:tb-buffer-name-template "*ein:tb %s/%s*") 50 | 51 | (defun ein:tb-new (buffer-name notebook) 52 | (make-instance 'ein:traceback 53 | :buffer-name buffer-name 54 | :source-notebook notebook)) 55 | 56 | (cl-defmethod ein:tb-get-buffer ((traceback ein:traceback)) 57 | (unless (and (slot-boundp traceback :buffer) 58 | (buffer-live-p (slot-value traceback 'buffer))) 59 | (let ((buf (get-buffer-create (slot-value traceback 'buffer-name)))) 60 | (setf (slot-value traceback 'buffer) buf))) 61 | (slot-value traceback 'buffer)) 62 | 63 | (defun ein:tb-pp (ewoc-data) 64 | (insert (ansi-color-apply ewoc-data))) 65 | 66 | (cl-defmethod ein:tb-render ((traceback ein:traceback) tb-data) 67 | (with-current-buffer (ein:tb-get-buffer traceback) 68 | (setq ein:%traceback% traceback) 69 | (setq buffer-read-only t) 70 | (let ((inhibit-read-only t) 71 | (ewoc (ein:ewoc-create #'ein:tb-pp))) 72 | (erase-buffer) 73 | (setf (slot-value traceback 'ewoc) ewoc) 74 | (setf (slot-value traceback 'tb-data) tb-data) 75 | (mapc (lambda (data) (ewoc-enter-last ewoc data)) tb-data)) 76 | (ein:traceback-mode))) 77 | 78 | (cl-defmethod ein:tb-popup ((traceback ein:traceback) tb-data) 79 | (ein:tb-render traceback tb-data) 80 | (pop-to-buffer (ein:tb-get-buffer traceback))) 81 | 82 | ;;;###autoload 83 | (defun ein:tb-show () 84 | "Show full traceback in traceback viewer." 85 | (interactive) 86 | (unless 87 | (ein:and-let* ((tb-data (ein:get-traceback-data)) 88 | (url-or-port (or (ein:get-url-or-port) 89 | (ein:get-url-or-port--shared-output))) 90 | (kernel (or (ein:get-kernel) 91 | (ein:get-kernel--shared-output))) 92 | (kr-id (ein:kernel-id kernel)) 93 | (tb-name (format ein:tb-buffer-name-template 94 | url-or-port kr-id))) 95 | (ein:tb-popup (ein:tb-new tb-name (ein:get-notebook)) tb-data) 96 | t) 97 | (error "No traceback is available."))) 98 | 99 | (cl-defmethod ein:tb-range-of-node-at-point ((traceback ein:traceback)) 100 | (let* ((ewoc (slot-value traceback 'ewoc)) 101 | (ewoc-node (ewoc-locate ewoc)) 102 | (beg (ewoc-location ewoc-node)) 103 | (end (ein:aand (ewoc-next ewoc ewoc-node) (ewoc-location it)))) 104 | (list beg end))) 105 | 106 | (cl-defmethod ein:tb-file-path-at-point ((traceback ein:traceback)) 107 | (cl-destructuring-bind (beg end) 108 | (ein:tb-range-of-node-at-point traceback) 109 | (let* ((file-tail 110 | (next-single-property-change beg 'font-lock-face nil end)) 111 | (file (when file-tail 112 | (buffer-substring-no-properties beg file-tail)))) 113 | (if (string-match "\\.pyc$" file) 114 | (concat (file-name-sans-extension file) ".py") 115 | file)))) 116 | 117 | (cl-defmethod ein:tb-file-lineno-at-point ((traceback ein:traceback)) 118 | (cl-destructuring-bind (beg end) 119 | (ein:tb-range-of-node-at-point traceback) 120 | (when (save-excursion 121 | (goto-char beg) 122 | (search-forward-regexp "^[-]+> \\([0-9]+\\)" end t)) 123 | (string-to-number (match-string 1))))) 124 | 125 | (cl-defmethod ein:tb-jump-to-source-at-point ((traceback ein:traceback) 126 | &optional select) 127 | (let ((file (ein:tb-file-path-at-point traceback)) 128 | (lineno (ein:tb-file-lineno-at-point traceback))) 129 | (if (string-match "" 21 | And I press "C-" 22 | And I press "C-c C-k" 23 | And I press "C-" 24 | And I press "C-c C-y" 25 | And I press "C-/" 26 | Then the cursor should be at point "75" 27 | 28 | @undo 29 | Scenario: Collapse doesn't break undo 30 | Given I enable "ein:worksheet-enable-undo" 31 | Given new python notebook 32 | When I type "from time import sleep" 33 | And I press "RET" 34 | And I wait for cell to execute 35 | And I dump buffer 36 | And I press "C-c C-b" 37 | And I type "1 + 1" 38 | And I press "RET" 39 | And I press "C-" 40 | And I press "C-n" 41 | And I type "print("abba\nabba")" 42 | And I press "RET" 43 | And I type "1.618" 44 | And I wait for cell to execute 45 | And I press "C-" 46 | And I press "C-n" 47 | And I type "9" 48 | And I press "C-" 49 | And I dump buffer 50 | And I press "C-c C-e" 51 | And I press "C-/" 52 | And I dump buffer 53 | Then the cursor should be at point "77" 54 | And I undo again 55 | Then the cursor should be at point "53" 56 | 57 | @undo 58 | Scenario: Test the conflagrative commands 59 | Given I enable "ein:worksheet-enable-undo" 60 | Given new python notebook 61 | When I type "import math" 62 | And I press "RET" 63 | And I press "M-RET" 64 | And I type "[i for i in [1,2]]" 65 | And I press "M-RET" 66 | And I type "math.log(math.exp(1.0))" 67 | And I wait for cell to execute 68 | And I press "C-" 69 | And I press "C-" 70 | And I press "C-n" 71 | And I type "print("m")" 72 | And I wait for cell to execute 73 | And I press "C-u C-c C-v" 74 | And I press "C-/" 75 | Then the cursor should be at point "22" 76 | And I undo again 77 | And I dump buffer 78 | And I press "C-c C-v" 79 | And I press "C-/" 80 | And I undo again 81 | Then the cursor should be at point "22" 82 | And I press "C-c C-S-l" 83 | And I press "C-/" 84 | And I undo again 85 | And I undo again 86 | Then the cursor should be at point "22" 87 | 88 | @undo 89 | Scenario: Clear output doesn't break undo, throw in multiple-cursors 90 | Given I enable "ein:worksheet-enable-undo" 91 | Given new python notebook 92 | And I clear log expr "ein:log-all-buffer-name" 93 | When I type "from time import sleep" 94 | And I press "RET" 95 | And I press "C-c C-b" 96 | And I type "1 + 1" 97 | And I press "RET" 98 | And I press "C-" 99 | And I press "C-n" 100 | And I type "print("abba\nabba")" 101 | And I press "RET" 102 | And I type "1.618" 103 | And I add fake cursor to undo list 104 | And I wait for cell to execute 105 | And I press "C-" 106 | And I press "C-n" 107 | And I type "undo meee" 108 | And I press "C-" 109 | And I press "C-c C-l" 110 | And I press "C-/" 111 | Then the cursor should be at point "74" 112 | And I undo again 113 | Then the cursor should be at point "53" 114 | And I switch to log expr "ein:log-all-buffer-name" 115 | Then I should see "multiple-cursors-mode exception" 116 | 117 | @undo 118 | Scenario: Moving cells doesn't break undo 119 | Given I enable "ein:worksheet-enable-undo" 120 | Given new python notebook 121 | When I type "100" 122 | And I press "C-c C-b" 123 | And I type "200" 124 | And I press "C-c C-b" 125 | And I type "print("hello")" 126 | And I wait for cell to execute 127 | And I press "C-" 128 | And I wait for cell to execute 129 | And I press "C-" 130 | And I press "C-c " 131 | And I press "C-/" 132 | Then the cursor should be at point "54" 133 | And I press "C-" 134 | And I press "C-" 135 | And I wait for cell to execute 136 | And I press "C-c " 137 | And I press "C-/" 138 | Then the cursor should be at point "67" 139 | 140 | @undo 141 | Scenario: Split and merge don't break undo 142 | Given I enable "ein:worksheet-enable-undo" 143 | Given new python notebook 144 | When I type "print("hello")" 145 | And I press "C-c C-b" 146 | And I type "1111" 147 | And I press "RET" 148 | And I press "RET" 149 | And I press "RET" 150 | And I type "2222" 151 | And I press "RET" 152 | And I type "3333" 153 | And I press "C-c C-b" 154 | And I type "4444" 155 | And I press "C-" 156 | And I press "C-n" 157 | And I press "C-c C-s" 158 | And I wait for cell to execute 159 | And I press "C-" 160 | And I wait for cell to execute 161 | And I press "C-" 162 | And I wait for cell to execute 163 | And I press "C-/" 164 | And I press "C-" 165 | And I type "5555" 166 | And I press "RET" 167 | And I type "6666" 168 | And I wait for cell to execute 169 | And I press "C-/" 170 | And I undo again 171 | And I undo again 172 | And I undo again 173 | And I undo again 174 | Then the cursor should be at point "70" 175 | And I press "C-c C-m" 176 | And I press "C-c C-m" 177 | And I press "C-/" 178 | And I undo again 179 | And I undo again 180 | And I undo again 181 | Then the cursor should be at point "50" 182 | 183 | @undo 184 | Scenario: Undo needs to at least work for reopened notebooks 185 | Given I enable "ein:worksheet-enable-undo" 186 | Given old notebook "undo.ipynb" 187 | And I type "howdy" 188 | And I press "RET" 189 | And I press "C-" 190 | And I press "C-" 191 | And I type "rowdy" 192 | And I press "RET" 193 | And I press "C-" 194 | And I press "C-" 195 | And I press "C-c C-k" 196 | And I type "bowdy" 197 | And I press "RET" 198 | And I press "C-c C-y" 199 | And I press "C-/" 200 | Then the cursor should be at point "15" 201 | And I press "C-/" 202 | And I press "C-n" 203 | And I press "C-n" 204 | And I press "C-c C-s" 205 | And I press "C-/" 206 | And I undo again 207 | And I undo again 208 | And I undo again 209 | And I undo again 210 | Then the cursor should be at point "20" 211 | And I undo again 212 | Then the cursor should be at point "88" 213 | And I press "C-" 214 | And I press "C-k" 215 | And I press "C-k" 216 | And I press "C-k" 217 | And I type "1.618" 218 | And I wait for cell to execute 219 | And I press "C-" 220 | And I press "C-c C-m" 221 | And I press "C-n" 222 | And I press "C-n" 223 | And I press "C-c C-s" 224 | And I press "C-/" 225 | And I undo again 226 | And I undo again 227 | And I undo again 228 | Then the cursor should be at point "124" 229 | 230 | @undo 231 | Scenario: Execute all cells, mod some cells, get outputs, undo mods 232 | Given I enable "ein:worksheet-enable-undo" 233 | Given new python notebook 234 | When I type "from time import sleep" 235 | And I press "RET" 236 | And I type "sleep(3)" 237 | And I press "C-c C-b" 238 | And I type "sleep(1)" 239 | And I press "RET" 240 | And I type "import math" 241 | And I press "RET" 242 | And I type "2*math.asin(1)" 243 | And I press "C-c C-b" 244 | And I press "C-c C-t" 245 | And I type "mark" 246 | And I call "ein:worksheet-execute-all-cells-above" 247 | And I press "C-c C-b" 248 | And I type "undo" 249 | And I press "C-" 250 | And I press "C-" 251 | And I type "surprise" 252 | And I wait for buffer to say "3.14159" 253 | And I press "C-/" 254 | Then the cursor should be at point "51" 255 | And I undo again 256 | Then the cursor should be at point "139" 257 | And I undo again 258 | And dump diagnostic 259 | Then the cursor should be at point "125" 260 | 261 | @undo 262 | Scenario: Toggling between markdown and codecell does not break undo 263 | Given I enable "ein:worksheet-enable-undo" 264 | Given new python notebook 265 | When I type ""to be markdown"" 266 | And I press "C-c C-b" 267 | And I type "200" 268 | And I wait for cell to execute 269 | And I press "C-" 270 | And I press "C-c C-t" 271 | And I press "C-/" 272 | Then the cursor should be at point "38" 273 | And I press "C-" 274 | And I press "C-c C-t" 275 | And I press "C-/" 276 | Then the cursor should be at point "33" 277 | And I press "C-" 278 | And I press "C-c C-t" 279 | And I wait for cell to execute 280 | And I press "C-/" 281 | Then the cursor should be at point "62" 282 | -------------------------------------------------------------------------------- /test/test-ein-cell-notebook.el: -------------------------------------------------------------------------------- 1 | ;; Tests for cell function that requires notebook buffer -*- lexical-binding:t -*- 2 | 3 | (require 'ert) 4 | 5 | (require 'ein-notebook) 6 | (require 'ein-testing-notebook) 7 | 8 | ;; ein:cell-location 9 | 10 | (ert-deftest ein:cell-location-codecell-prompt-beg () 11 | (ein:testing-with-one-cell 'code 12 | (should (equal (marker-position (ein:cell-location cell :prompt)) 13 | (save-excursion 14 | (goto-char (point-max)) 15 | (search-backward "In [ ]:") 16 | (point)))))) 17 | 18 | (ert-deftest ein:cell-location-codecell-prompt-end () 19 | (ein:testing-with-one-cell 'code 20 | (should (equal (marker-position (ein:cell-location cell :prompt t)) 21 | (1- (point)))))) 22 | 23 | (ert-deftest ein:cell-location-codecell-input-beg () 24 | (ein:testing-with-one-cell 'code 25 | (insert "some text") 26 | (should (equal (marker-position (ein:cell-location cell :input)) 27 | (1- (line-beginning-position)))))) 28 | 29 | (ert-deftest ein:cell-location-codecell-input-end () 30 | (ein:testing-with-one-cell 'code 31 | (insert "some text") 32 | (should (equal (marker-position (ein:cell-location cell :input t)) 33 | (1+ (point)))))) 34 | 35 | ;; from-json 36 | 37 | (ert-deftest eintest:cell-input-prompt-number () 38 | (ein:testing-with-one-cell 39 | (ein:cell-from-json 40 | (list :cell_type "code" 41 | :source "some input" 42 | :metadata (list :collapsed json-false :autoscroll json-false) 43 | :execution_count 111) 44 | :ewoc (oref ein:%worksheet% :ewoc)) 45 | (goto-char (ein:cell-location cell)) 46 | (should (looking-at "\ 47 | In \\[111\\]: 48 | some input 49 | ")))) 50 | 51 | (ert-deftest eintest:cell-input-prompt-star () 52 | (ein:testing-with-one-cell 53 | (ein:cell-from-json 54 | (list :cell_type "code" 55 | :source "some input" 56 | :metadata (list :collapsed json-false :autoscroll json-false) 57 | :execution_count "*") 58 | :ewoc (oref ein:%worksheet% :ewoc)) 59 | (goto-char (ein:cell-location cell)) 60 | (should (looking-at "\ 61 | In \\[\\*\\]: 62 | some input 63 | ")))) 64 | 65 | (ert-deftest eintest:cell-input-prompt-empty () 66 | (ein:testing-with-one-cell 67 | (ein:cell-from-json 68 | (list :cell_type "code" 69 | :metadata (list :collapsed json-false :autoscroll json-false) 70 | :source "some input") 71 | :ewoc (oref ein:%worksheet% :ewoc)) 72 | (goto-char (ein:cell-location cell)) 73 | (should (looking-at "\ 74 | In \\[ \\]: 75 | some input 76 | ")))) 77 | 78 | ;; Insert pyout/display_data 79 | 80 | (defun eintest:cell-insert-output (outputs regexp) 81 | (let ((shr-width 0)) 82 | (ein:testing-with-one-cell 83 | (ein:cell-from-json 84 | (list :cell_type "code" 85 | :outputs outputs 86 | :source "some input" 87 | :metadata (list :collapsed json-false :autoscroll json-false) 88 | :execution_count 111) 89 | :ewoc (oref ein:%worksheet% :ewoc)) 90 | (goto-char (ein:cell-location cell)) 91 | (should (looking-at (format "\ 92 | In \\[111\\]: 93 | some input 94 | %s" regexp)))))) 95 | 96 | (defmacro eintest:gene-test-cell-insert-output-pyout-and-display-data (name regexps outputs) 97 | (declare (indent defun)) 98 | (let ((test-pyout 99 | (intern (format "ein:cell-insert-output-pyout-%s" name))) 100 | (test-display-data 101 | (intern (format "ein:cell-insert-output-display-data-%s" name))) 102 | (outputs-pyout 103 | (cl-loop for i from 1 104 | for x in outputs 105 | collect 106 | ;; ein:cell--handle-output doesn't get called 107 | ;; so can't use :execution_count here although that is preferable 108 | (append x (list :output_type "execute_result" :prompt_number i :metadata nil)))) 109 | (outputs-display-data 110 | (mapcar (lambda (x) (append '(:output_type "display_data" :metadata nil) x)) 111 | outputs)) 112 | (regexp-pyout 113 | (ein:join-str 114 | "" 115 | (cl-loop for i from 1 116 | for x in regexps 117 | collect (format "Out \\[%s\\]:\n%s\n" i x)))) 118 | (regexp-display-data 119 | (concat (ein:join-str "\n" regexps) "\n"))) 120 | `(progn 121 | (ert-deftest ,test-pyout () 122 | (eintest:cell-insert-output ',outputs-pyout 123 | ,regexp-pyout)) 124 | (ert-deftest ,test-display-data () 125 | (eintest:cell-insert-output ',outputs-display-data 126 | ,regexp-display-data))))) 127 | 128 | (eintest:gene-test-cell-insert-output-pyout-and-display-data 129 | text ("some output") ((:data (:text/plain "some output")))) 130 | 131 | (eintest:gene-test-cell-insert-output-pyout-and-display-data 132 | latex 133 | ("some output \\\\LaTeX") 134 | ((:data (:application/latex "some output \\LaTeX")))) 135 | 136 | (when (image-type-available-p 'svg) 137 | (eintest:gene-test-cell-insert-output-pyout-and-display-data 138 | svg 139 | ("\\.") 140 | ((:data (:text/plain "some output text" :image/svg+xml ein:testing-example-svg))))) 141 | 142 | (eintest:gene-test-cell-insert-output-pyout-and-display-data 143 | html 144 | ("some output text") 145 | ((:data (:text/plain "some output text" :text/html "not shown")))) 146 | 147 | (eintest:gene-test-cell-insert-output-pyout-and-display-data 148 | javascript 149 | ("some output text") 150 | ((:data (:text/plain "some output text" :application/javascript "$.do.something()")))) 151 | 152 | (eintest:gene-test-cell-insert-output-pyout-and-display-data 153 | text-two 154 | ("first output text" "second output text") 155 | ((:data (:text/plain "first output text")) (:data (:text/plain "second output text")))) 156 | 157 | (eintest:gene-test-cell-insert-output-pyout-and-display-data 158 | text-javascript 159 | ("first output text" "second output text") 160 | ((:data (:text/plain "first output text")) 161 | (:data (:text/plain "second output text" :application/javascript "$.do.something()")))) 162 | 163 | (when (image-type-available-p 'svg) 164 | (eintest:gene-test-cell-insert-output-pyout-and-display-data 165 | text-latex-svg 166 | ("first output text" "second output \\\\LaTeX") 167 | ((:data (:text/plain "first output text")) 168 | (:data (:application/latex "second output \\LaTeX")) 169 | (:data (:text/plain "some output text" :image/svg+xml ein:testing-example-svg))))) 170 | 171 | ;; Insert pyerr 172 | 173 | (ert-deftest ein:cell-insert-output-pyerr-simple () 174 | (eintest:cell-insert-output 175 | (list (list :output_type "pyerr" 176 | :traceback '("some traceback 1" 177 | "some traceback 2"))) 178 | "\ 179 | some traceback 1 180 | some traceback 2 181 | ")) 182 | 183 | 184 | ;; Insert stream 185 | 186 | (ert-deftest ein:cell-insert-output-stream-simple-stdout () 187 | (eintest:cell-insert-output 188 | (list (list :output_type "stream" 189 | :stream "stdout" 190 | :text "some stdout 1")) 191 | "\ 192 | some stdout 1 193 | ")) 194 | 195 | (ert-deftest ein:cell-insert-output-stream-stdout-stderr () 196 | (eintest:cell-insert-output 197 | (list (list :output_type "stream" 198 | :stream "stdout" 199 | :text "some stdout 1") 200 | (list :output_type "stream" 201 | :stream "stderr" 202 | :text "some stderr 1")) 203 | "\ 204 | some stdout 1 205 | some stderr 1 206 | ")) 207 | 208 | (ert-deftest ein:cell-insert-output-stream-flushed-stdout () 209 | (eintest:cell-insert-output 210 | (list (list :output_type "stream" 211 | :stream "stdout" 212 | :text "some stdout 1") 213 | (list :output_type "stream" 214 | :stream "stdout" 215 | :text "some stdout 2")) 216 | "\ 217 | some stdout 1some stdout 2 218 | ")) 219 | 220 | (ert-deftest ein:cell-insert-output-stream-flushed-stdout-and-stderr () 221 | (eintest:cell-insert-output 222 | (list (list :output_type "stream" 223 | :stream "stdout" 224 | :text "some stdout 1") 225 | (list :output_type "stream" 226 | :stream "stderr" 227 | :text "some stderr 1") 228 | (list :output_type "stream" 229 | :stream "stdout" 230 | :text "some stdout 2") 231 | (list :output_type "stream" 232 | :stream "stderr" 233 | :text "some stderr 2")) 234 | "\ 235 | some stdout 1 236 | some stderr 1 237 | some stdout 2 238 | some stderr 2 239 | ")) 240 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================================================== 2 | EIN -- Emacs IPython Notebook |build-status| |melpa-dev| 3 | ========================================================== 4 | 5 | .. image:: https://github.com/dickmao/emacs-ipython-notebook/blob/master/thumbnail.png 6 | :target: https://youtu.be/8VzWc9QeOxE 7 | :alt: Kaggle Notebooks in AWS 8 | 9 | Emacs IPython Notebook (EIN), despite its name, is a jupyter client for all 10 | languages. It does not work under non-WSL Windows environments. 11 | 12 | As of 2023, EIN has been sunset for a number of years having been 13 | unable to keep up with jupyter's web-first ecosystem. Even during 14 | its heyday EIN never fully reconciled emac's monolithic buffer 15 | architecture to the notebook's by-cell discretization, leaving 16 | gaping functional holes like crippled undo. 17 | 18 | **As of 2025, a greenfield notebook implementation resides at,** 19 | 20 | https://github.com/commercial-emacs/xjupyter.git 21 | 22 | It features full-fledged undo and relies on "mode overlays" 23 | instead of the complex and fragile polymode. 24 | 25 | .. |build-status| 26 | image:: https://github.com/millejoh/emacs-ipython-notebook/workflows/CI/badge.svg 27 | :target: https://github.com/millejoh/emacs-ipython-notebook/actions 28 | :alt: Build Status 29 | .. |melpa-dev| 30 | image:: https://melpa.org/packages/ein-badge.svg 31 | :target: http://melpa.org/#/ein 32 | :alt: MELPA current version 33 | .. _Jupyter: http://jupyter.org 34 | .. _Babel: https://orgmode.org/worg/org-contrib/babel/intro.html 35 | .. _Org: https://orgmode.org 36 | .. _[tkf]: http://tkf.github.io 37 | .. _[gregsexton]: https://github.com/gregsexton/ob-ipython 38 | 39 | Install 40 | ======= 41 | As described in `Getting started`_, ensure melpa's whereabouts in ``init.el`` or ``.emacs``:: 42 | 43 | (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/")) 44 | 45 | Then 46 | 47 | :: 48 | 49 | M-x package-install RET ein RET 50 | 51 | Alternatively, directly clone this repo and ``make install``. 52 | 53 | For jupyterlab 3.0+, reconfigure the subcommand from "notebook" to "server". 54 | 55 | :: 56 | 57 | M-x customize-option RET ein:jupyter-server-use-subcommand RET 58 | 59 | Usage 60 | ===== 61 | Start EIN using **ONE** of the following: 62 | 63 | * Open an ``.ipynb`` file, press ``C-c C-o``, or, 64 | * ``M-x ein:run`` launches a jupyter process from emacs, or, 65 | * ``M-x ein:login`` to a running jupyter server, or, 66 | 67 | ``M-x ein:stop`` prompts to halt local and remote jupyter services. 68 | 69 | Alternatively, ob-ein_. 70 | 71 | .. _Cask: https://cask.readthedocs.io/en/latest/guide/installation.html 72 | .. _Getting started: http://melpa.org/#/getting-started 73 | 74 | FAQ 75 | === 76 | 77 | How do I... 78 | ----------- 79 | 80 | ... report a bug? 81 | Note EIN is tested only for *released* GNU Emacs versions 82 | 26.3 83 | and later. Pre-release versions will not work. 84 | 85 | First try ``emacs -Q -f package-initialize -f ein:dev-start-debug`` and reproduce the bug. The ``-Q`` skips any user configuration that might interfere with EIN. 86 | 87 | Then file an issue using ``M-x ein:dev-bug-report-template``. 88 | 89 | ... display images inline? 90 | We find inserting images into emacs disruptive, and so default to spawning an external viewer. To override this, 91 | :: 92 | 93 | M-x customize-group RET ein 94 | Ein:Output Area Inlined Images 95 | 96 | ... configure the external image viewer? 97 | :: 98 | 99 | M-x customize-group RET mailcap 100 | Mailcap User Mime Data 101 | 102 | On a typical Linux system, one might configure a viewer for MIME Type ``image/png`` as a shell command ``convert %s -background white -alpha remove -alpha off - | display -immutable``. 103 | 104 | ... get IDE-like behavior? 105 | You can't. EIN's architecture is fundamentally incompatible with LSP. 106 | 107 | .. _Issues: https://github.com/millejoh/emacs-ipython-notebook/issues 108 | .. _prevailing documentation: http://millejoh.github.io/emacs-ipython-notebook 109 | .. _spacemacs layer: https://github.com/syl20bnr/spacemacs/tree/master/layers/%2Blang/ipython-notebook 110 | .. _company-mode: https://github.com/company-mode/company-mode 111 | .. _jupyterhub: https://github.com/jupyterhub/jupyterhub 112 | .. _elpy: https://melpa.org/#/elpy 113 | .. _math-preview: https://gitlab.com/matsievskiysv/math-preview 114 | .. _program modes: https://www.gnu.org/software/emacs/manual/html_node/emacs/Program-Modes.html 115 | .. _undo boundaries: https://www.gnu.org/software/emacs/manual/html_node/elisp/Undo.html 116 | 117 | ob-ein 118 | ====== 119 | Configuration: 120 | 121 | :: 122 | 123 | M-x customize-group RET org-babel 124 | Org Babel Load Languages: 125 | Insert (ein . t) 126 | For example, '((emacs-lisp . t) (ein . t)) 127 | 128 | Snippet: 129 | 130 | :: 131 | 132 | #+BEGIN_SRC ein-python :session localhost 133 | import numpy, math, matplotlib.pyplot as plt 134 | %matplotlib inline 135 | x = numpy.linspace(0, 2*math.pi) 136 | plt.plot(x, numpy.sin(x)) 137 | #+END_SRC 138 | 139 | The ``:session`` is the notebook url, e.g., ``http://localhost:8888/my.ipynb``, or simply ``localhost``, in which case org evaluates anonymously. A port may also be specified, e.g., ``localhost:8889``. 140 | 141 | *Language* can be ``ein-python``, ``ein-r``, or ``ein-julia``. **The relevant** `jupyter kernel`_ **must be installed before use**. Additional languages can be configured via:: 142 | 143 | M-x customize-group RET ein 144 | Ob Ein Languages 145 | 146 | .. _polymode: https://github.com/polymode/polymode 147 | .. _ob-ipython: https://github.com/gregsexton/ob-ipython 148 | .. _scimax: https://github.com/jkitchin/scimax 149 | .. _jupyter kernel: https://github.com/jupyter/jupyter/wiki/Jupyter-kernels 150 | 151 | .. _gat utility: https://dickmaogat.readthedocs.io/en/latest/install.html 152 | .. _gat usage: https://dickmaogat.readthedocs.io/en/latest/usage.html 153 | .. _batch mode: https://nbconvert.readthedocs.io/en/latest/execute_api.html 154 | .. _dickmao/Kaggler: https://github.com/dickmao/Kaggler/tree/gcspath#importing-datasets 155 | 156 | Keymap (C-h m) 157 | ============== 158 | 159 | :: 160 | 161 | Key Binding 162 | ------------------------------------------------------------------------------- 163 | C- ein:worksheet-goto-next-input-km 164 | C- ein:worksheet-goto-prev-input-km 165 | M-S- ein:worksheet-execute-cell-and-insert-below-km 166 | M- ein:worksheet-not-move-cell-down-km 167 | M- ein:worksheet-not-move-cell-up-km 168 | 169 | C-x C-s ein:notebook-save-notebook-command-km 170 | C-x C-w ein:notebook-rename-command-km 171 | 172 | M-RET ein:worksheet-execute-cell-and-goto-next-km 173 | M-, ein:pytools-jump-back-command 174 | M-. ein:pytools-jump-to-source-command 175 | 176 | C-c C-a ein:worksheet-insert-cell-above-km 177 | C-c C-b ein:worksheet-insert-cell-below-km 178 | C-c C-c ein:worksheet-execute-cell-km 179 | C-u C-c C-c ein:worksheet-execute-all-cells 180 | C-c C-e ein:worksheet-toggle-output-km 181 | C-c C-f ein:file-open-km 182 | C-c C-k ein:worksheet-kill-cell-km 183 | C-c C-l ein:worksheet-clear-output-km 184 | C-c RET ein:worksheet-merge-cell-km 185 | C-c C-n ein:worksheet-goto-next-input-km 186 | C-c C-o ein:notebook-open-km 187 | C-c C-p ein:worksheet-goto-prev-input-km 188 | C-c C-q ein:notebook-kill-kernel-then-close-command-km 189 | C-c C-r ein:notebook-reconnect-session-command-km 190 | C-c C-s ein:worksheet-split-cell-at-point-km 191 | C-c C-t ein:worksheet-toggle-cell-type-km 192 | C-c C-u ein:worksheet-change-cell-type-km 193 | C-c C-v ein:worksheet-set-output-visibility-all-km 194 | C-c C-w ein:worksheet-copy-cell-km 195 | C-c C-y ein:worksheet-yank-cell-km 196 | C-c C-z ein:notebook-kernel-interrupt-command-km 197 | C-c C-S-l ein:worksheet-clear-all-output-km 198 | C-c C-# ein:notebook-close-km 199 | C-c C-$ ein:tb-show-km 200 | C-c C-/ ein:notebook-scratchsheet-open-km 201 | C-c C-; ein:shared-output-show-code-cell-at-point-km 202 | C-c ein:worksheet-move-cell-down-km 203 | C-c ein:worksheet-move-cell-up-km 204 | 205 | C-c C-x C-r ein:notebook-restart-session-command-km 206 | 207 | C-c M-w ein:worksheet-copy-cell-km 208 | 209 | 210 | This is a minor mode. If called interactively, toggle the ‘Ein:Notebook 211 | mode’ mode. If the prefix argument is positive, enable the mode, and if 212 | it is zero or negative, disable the mode. 213 | 214 | If called from Lisp, toggle the mode if ARG is ‘toggle’. Enable the 215 | mode if ARG is nil, omitted, or is a positive number. Disable the mode 216 | if ARG is a negative number. 217 | 218 | To check whether the minor mode is enabled in the current buffer, 219 | evaluate the variable ‘ein:notebook-mode’. 220 | 221 | The mode’s hook is called both when the mode is enabled and when it is 222 | disabled. 223 | --------------------------------------------------------------------------------