diff --git a/doc/source/index.rst b/doc/source/index.rst index fed240f..75cdba2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -332,6 +332,8 @@ MuMaMo Misc ^^^^ +.. el:variable:: ein:filename-translations +.. el:function:: ein:tramp-create-filename-translator .. el:variable:: ein:query-timeout @@ -485,6 +487,7 @@ v0.1.1 :el:symbol:`ein:notebook-turn-on-autoexec`). * Start completion when "." is inserted. Use :el:symbol:`ein:complete-on-dot` to disable this feature. +* Support tramp. See :el:symbol:`ein:filename-translations`. v0.1 diff --git a/lisp/ein-kernel.el b/lisp/ein-kernel.el index 0f73c30..1d1b70a 100644 --- a/lisp/ein-kernel.el +++ b/lisp/ein-kernel.el @@ -495,6 +495,14 @@ http://ipython.org/ipython-doc/dev/development/messaging.html#complete ;;; Utility functions +(defun ein:kernel-filename-to-python (kernel filename) + "See: `ein:filename-to-python'." + (ein:filename-to-python (ein:$kernel-url-or-port kernel) filename)) + +(defun ein:kernel-filename-from-python (kernel filename) + "See: `ein:filename-from-python'." + (ein:filename-from-python (ein:$kernel-url-or-port kernel) filename)) + (defun ein:kernel-construct-defstring (content) "Construct call signature from CONTENT of ``:object_info_reply``. Used in `ein:cell-finish-tooltip', etc." @@ -524,6 +532,10 @@ Used in `ein:cell-finish-tooltip', etc." help)) (defun ein:kernel-request-stream (kernel code func &optional args) + "Run lisp callback FUNC with the output stream returned by Python CODE. + +The first argument to the lisp function FUNC is the stream output +as a string and the rest of the argument is the optional ARGS." (ein:kernel-execute kernel code @@ -542,8 +554,9 @@ When no such directory exists, `default-directory' will not be changed." (ein:kernel-request-stream kernel "__import__('sys').stdout.write(__import__('os').getcwd())" - (lambda (path buffer) + (lambda (path kernel buffer) (with-current-buffer buffer + (setq path (ein:kernel-filename-from-python kernel path)) (if (file-accessible-directory-p path) (progn (setq default-directory path) @@ -553,7 +566,7 @@ When no such directory exists, `default-directory' will not be changed." (ein:log 'info "Syncing directory of %s with kernel...FAILED (no dir: %s)" buffer path)))) - (list buffer))) + (list kernel buffer))) (defun ein:kernelinfo-init (kernelinfo buffer) (setf (ein:$kernelinfo-buffer kernelinfo) buffer)) @@ -576,7 +589,8 @@ When no such directory exists, `default-directory' will not be changed." (ein:kernel-request-stream kernel "__import__('sys').stdout.write(__import__('os').getcwd())" - (lambda (cwd kernelinfo buffer) + (lambda (cwd kernel kernelinfo buffer) + (setq cwd (ein:kernel-filename-from-python kernel cwd)) (setf (ein:$kernelinfo-ccwd kernelinfo) cwd) ;; sync buffer's `default-directory' with CWD (when (buffer-live-p buffer) @@ -584,7 +598,7 @@ When no such directory exists, `default-directory' will not be changed." (when (file-accessible-directory-p cwd) (setq default-directory (file-name-as-directory cwd)))))) (let ((kernelinfo (ein:$kernel-kernelinfo kernel))) - (list kernelinfo (ein:$kernelinfo-buffer kernelinfo))))) + (list kernel kernelinfo (ein:$kernelinfo-buffer kernelinfo))))) (defun ein:kernelinfo-update-hostname (kernel) (ein:kernel-request-stream diff --git a/lisp/ein-pytools.el b/lisp/ein-pytools.el index 7986a99..d696d8a 100644 --- a/lisp/ein-pytools.el +++ b/lisp/ein-pytools.el @@ -109,9 +109,8 @@ If OTHER-WINDOW is non-`nil', open the file in the other window." :output (cons (lambda (packed msg-type content) - (let ((object (nth 0 packed)) - (other-window (nth 1 packed)) - (notebook-buffer (nth 2 packed))) + (destructuring-bind (kernel object other-window notebook-buffer) + packed (ein:case-equal msg-type (("stream") (ein:aif (plist-get content :data) @@ -122,6 +121,8 @@ If OTHER-WINDOW is non-`nil', open the file in the other window." (destructuring-bind (filename &optional lineno &rest ignore) (split-string it "\n") (setq lineno (string-to-number lineno)) + (setq filename + (ein:kernel-filename-from-python kernel filename)) (ein:goto-file filename lineno other-window) (when (and notebook-buffer (not ein:@connect)) (ein:connect-to-notebook-buffer notebook-buffer)) @@ -131,7 +132,7 @@ If OTHER-WINDOW is non-`nil', open the file in the other window." (("pyerr") (ein:log 'info "Jumping to the source of %s...Not found" object))))) - (list object other-window notebook-buffer))))) + (list kernel object other-window notebook-buffer))))) (defun ein:pytools-jump-to-source-command (&optional other-window) "Jump to the source code of the object at point. diff --git a/lisp/ein-utils.el b/lisp/ein-utils.el index b08a187..8e5d0fc 100644 --- a/lisp/ein-utils.el +++ b/lisp/ein-utils.el @@ -28,6 +28,11 @@ (eval-when-compile (require 'cl)) (require 'json) +;; Optional dependency on tramp: +(declare-function tramp-make-tramp-file-name "tramp") +(declare-function tramp-file-name-localname "tramp") +(declare-function tramp-dissect-file-name "tramp") + (defgroup ein nil "IPython notebook client in Emacs" :group 'applications @@ -61,6 +66,44 @@ format string which can be passed to `format-time-string'." :type '(string :tag "Format string") :group 'ein) +(defcustom ein:filename-translations nil + "Convert file paths between Emacs and Python process. + +This value can take these form: + +alist + Its key specifies URL-OR-PORT and value must be a list of two + functions: (TO-PYTHON FROM-PYTHON). Key (URL-OR-PORT) can be + string (URL), integer (port), or `default' (symbol). The + value of `default' is used when other key does not much. +function + Called with an argument URL-OR-PORT (integer or string). + This function must return a list of two functions: + (TO-PYTHON FROM-PYTHON). + +Here, the functions TO-PYTHON and FROM-PYTHON are defined as: + +TO-PYTHON + A function which converts a file name (returned by + `buffer-file-name') to the one Python understands. +FROM-PYTHON + A function which converts a file path returned by + Python process to the one Emacs understands. + +Use `ein:tramp-create-filename-translator' to easily generate the +pair of TO-PYTHON and FROM-PYTHON." + ;; I've got the idea from `slime-filename-translations'. + :type '(choice + (alist :tag "Translations mapping" + :key-type (choice :tag "URL or PORT" + (string :tag "URL" "http://127.0.0.1:8888") + (integer :tag "PORT" 8888) + (const default)) + :value-type (list (function :tag "TO-PYTHON") + (function :tag "FROM-PYTHON"))) + (function :tag "Translations getter")) + :group 'ein) + ;;; Macros and core functions/variables @@ -388,6 +431,68 @@ NOTE: This function creates new list." (ein:join-str " " (mapcar #'file-name-nondirectory it)))) (message "Compiled %s files" (length files)))) + +;;; File name translation + +;; Probably it's better to define `ein:filename-translations-get' as +;; an EIEIO method so that I don't have to re-define functions such as +;; `ein:kernel-filename-to-python' and `ein:kernel-filename-from-python'. + +(defun ein:filename-translations-get (url-or-port) + (ein:choose-setting 'ein:filename-translations url-or-port)) + +(defun ein:filename-to-python (url-or-port filename) + (ein:aif (car (ein:filename-translations-get url-or-port)) + (funcall it filename) + filename)) + +(defun ein:filename-from-python (url-or-port filename) + (ein:aif (cadr (ein:filename-translations-get url-or-port)) + (funcall it filename) + filename)) + +(defun ein:make-tramp-file-name (username remote-host python-filename) + "Old (with multi-hops) tramp compatability function. +Adapted from `slime-make-tramp-file-name'." + (if (boundp 'tramp-multi-methods) + (tramp-make-tramp-file-name nil nil + username + remote-host + python-filename) + (tramp-make-tramp-file-name nil + username + remote-host + python-filename))) + +(defun ein:tramp-create-filename-translator (remote-host &optional username) + "Generate a pair of TO-PYTHON and FROM-PYTHON for +`ein:filename-translations'. + +Usage:: + + (setq ein:filename-translations + `((8888 + . ,(ein:tramp-create-filename-translator \"MY-HOSTNAME\")))) + ;; Equivalently: + (setq ein:filename-translations + (lambda (url-or-port) + (when (equal url-or-port 8888) + (ein:tramp-create-filename-translator \"MY-HOSTNAME\")))) + +This setting assumes that the IPython server which can be +connected using the port 8888 in localhost is actually running in +the host named MY-HOSTNAME. + +Adapted from `slime-create-filename-translator'." + (require 'tramp) + (lexical-let ((remote-host remote-host) + (username (or username (user-login-name)))) + (list (lambda (emacs-filename) + (tramp-file-name-localname + (tramp-dissect-file-name emacs-filename))) + (lambda (python-filename) + (ein:make-tramp-file-name username remote-host python-filename))))) + ;;; utils.js compatible diff --git a/tests/test-ein-utils.el b/tests/test-ein-utils.el index 1f575dc..ded0db6 100644 --- a/tests/test-ein-utils.el +++ b/tests/test-ein-utils.el @@ -85,3 +85,55 @@ def func(): (ert-deftest ein:version () "Check if `ein:version' can be parsed by `version-to-list'." (version-to-list ein:version)) + + +;;; File name translation + +;; Requiring `tramp' during (inside of) tests yields error from +;; MuMaMo. Although I don't understand the reason, requiring it +;; before running tests workarounds this problem. +(require 'tramp) + +(ert-deftest ein:filename-translations-from-to-tramp () + (loop with ein:filename-translations = + `((8888 . ,(ein:tramp-create-filename-translator "HOST" "USER"))) + with filename = "/file/name" + for port in '(7777 8888) ; check for the one w/o translation + for emacs-filename = (ein:filename-from-python port filename) + do (message "emacs-filename = %s" emacs-filename) + do (should + (equal (ein:filename-to-python port emacs-filename) + filename)))) + +(ert-deftest ein:filename-translations-to-from-tramp () + (loop with ein:filename-translations = + `((8888 . ,(ein:tramp-create-filename-translator "HOST" "USER"))) + with filename = "/USER@HOST:/filename" + for port in '(8888) + do (should + (equal (ein:filename-from-python + port (ein:filename-to-python port filename)) + filename)))) + +(ert-deftest ein:filename-to-python-tramp () + (let* ((port 8888) + (ein:filename-translations + `((,port . ,(ein:tramp-create-filename-translator "DUMMY"))))) + (loop with python-filename = "/file/name" + for emacs-filename in '("/scpc:HOST:/file/name" + "/USER@HOST:/file/name") + do (should + (equal (ein:filename-to-python port emacs-filename) + python-filename))) + ;; Error: Not a Tramp file name: /file/name + (should-error (ein:filename-to-python port "/file/name")))) + +(ert-deftest ein:filename-from-python-tramp () + (loop with ein:filename-translations = + `((8888 . ,(ein:tramp-create-filename-translator "HOST" "USER"))) + with python-filename = "/file/name" + for emacs-filename in '("/USER@HOST:/file/name" "/file/name") + for port in '(8888 7777) + do (should + (equal (ein:filename-from-python port python-filename) + emacs-filename))))