Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

386 lines
15 KiB

  1. ;;; ein-core.el --- EIN core -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2012 Takafumi Arakaki
  3. ;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
  4. ;; This file is NOT part of GNU Emacs.
  5. ;; ein-core.el is free software: you can redistribute it and/or modify
  6. ;; it under the terms of the GNU General Public License as published by
  7. ;; the Free Software Foundation, either version 3 of the License, or
  8. ;; (at your option) any later version.
  9. ;; ein-core.el is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with ein-core.el.
  15. ;; If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;;
  18. ;;; Code:
  19. ;; Optional dependency on tramp:
  20. (declare-function tramp-make-tramp-file-name "tramp")
  21. (declare-function tramp-file-name-localname "tramp")
  22. (declare-function tramp-dissect-file-name "tramp")
  23. (require 'ein) ; get autoloaded functions into namespace
  24. (require 'ein-utils)
  25. (defvar ein:force-sync) ; defcustom in ein-contents-api which requires this file
  26. (defvar ein:content-query-timeout) ; likewise
  27. (defgroup ein nil
  28. "IPython notebook client in Emacs"
  29. :group 'applications
  30. :prefix "ein:")
  31. ;;; Configuration
  32. (defcustom ein:url-or-port '(8888)
  33. "List of default url-or-port values.
  34. This will be used for completion. So put your IPython servers.
  35. You can connect to servers not in this list \(but you will need
  36. to type every time)."
  37. :type '(repeat (choice (integer :tag "Port number" 8888)
  38. (string :tag "URL" "http://127.0.0.1:8888")))
  39. :group 'ein)
  40. (defcustom ein:default-url-or-port nil
  41. "Default URL or port. This should be your main IPython
  42. Notebook server."
  43. :type '(choice (integer :tag "Port number" 8888)
  44. (string :tag "URL" "http://127.0.0.1:8888")
  45. (const :tag "First value of `ein:url-or-port'" nil))
  46. :group 'ein)
  47. (defcustom ein:filename-translations nil
  48. "Convert file paths between Emacs and Python process.
  49. This value can take these form:
  50. alist
  51. Its key specifies URL-OR-PORT and value must be a list of two
  52. functions: (TO-PYTHON FROM-PYTHON). Key (URL-OR-PORT) can be
  53. string (URL), integer (port), or `default' (symbol). The
  54. value of `default' is used when other key does not much.
  55. function
  56. Called with an argument URL-OR-PORT (integer or string).
  57. This function must return a list of two functions:
  58. (TO-PYTHON FROM-PYTHON).
  59. Here, the functions TO-PYTHON and FROM-PYTHON are defined as:
  60. TO-PYTHON
  61. A function which converts a file name (returned by
  62. `buffer-file-name') to the one Python understands.
  63. FROM-PYTHON
  64. A function which converts a file path returned by
  65. Python process to the one Emacs understands.
  66. Use `ein:tramp-create-filename-translator' to easily generate the
  67. pair of TO-PYTHON and FROM-PYTHON."
  68. ;; I've got the idea from `slime-filename-translations'.
  69. :type '(choice
  70. (alist :tag "Translations mapping"
  71. :key-type (choice :tag "URL or PORT"
  72. (string :tag "URL" "http://127.0.0.1:8888")
  73. (integer :tag "PORT" 8888)
  74. (const default))
  75. :value-type (list (function :tag "TO-PYTHON")
  76. (function :tag "FROM-PYTHON")))
  77. (function :tag "Translations getter"))
  78. :group 'ein)
  79. ;;; Constants
  80. (defconst ein:source-dir (file-name-directory load-file-name)
  81. "Directory in which `ein*.el` files are located.")
  82. ;;; Configuration getter
  83. (defun ein:default-url-or-port ()
  84. (or ein:default-url-or-port (car ein:url-or-port) 8888))
  85. (defun ein:version (&optional interactively copy-to-kill)
  86. "Return a longer version string.
  87. With prefix argument, copy the string to kill ring.
  88. The result contains `ein:version' and either git revision (if
  89. the source is in git repository) or elpa version."
  90. (interactive (list t current-prefix-arg))
  91. (let* ((version
  92. (or (and (ein:git-root-p
  93. (concat (file-name-as-directory ein:source-dir) ".."))
  94. (let ((default-directory ein:source-dir))
  95. (ein:git-revision-dirty)))
  96. (and (string-match "/ein-\\([0-9\\.]*\\)/$" ein:source-dir)
  97. (match-string 1 ein:source-dir)))))
  98. (when interactively
  99. (message "EIN version is %s" version))
  100. (when copy-to-kill
  101. (kill-new version))
  102. version))
  103. ;;; Server attribute getters. These should be moved to ein-open.el
  104. (defvar *ein:notebook-version* (make-hash-table :test #'equal)
  105. "url-or-port to major notebook version")
  106. (defvar *ein:kernelspecs* (make-hash-table :test #'equal)
  107. "url-or-port to kernelspecs")
  108. (defun ein:get-kernelspec (url-or-port name)
  109. (let* ((kernelspecs (ein:need-kernelspecs url-or-port))
  110. (name (if (stringp name)
  111. (intern (format ":%s" name))
  112. name))
  113. (ks (plist-get kernelspecs name)))
  114. (if (stringp ks)
  115. (ein:get-kernelspec url-or-port ks)
  116. ks)))
  117. (defun ein:need-kernelspecs (url-or-port)
  118. "Callers assume ein:query-kernelspecs succeeded. If not, nil."
  119. (ein:aif (gethash url-or-port *ein:kernelspecs*) it
  120. (ein:log 'warn "No recorded kernelspecs for %s" url-or-port)
  121. nil))
  122. (defun ein:query-kernelspecs (url-or-port callback &optional iteration)
  123. "Send for kernelspecs of URL-OR-PORT with CALLBACK arity 0 (just a semaphore)"
  124. (unless iteration
  125. (setq iteration 0))
  126. (ein:query-singleton-ajax
  127. (list 'ein:query-kernelspecs url-or-port)
  128. (ein:url url-or-port "api/kernelspecs")
  129. :type "GET"
  130. :timeout ein:content-query-timeout
  131. :parser 'ein:json-read
  132. :sync ein:force-sync
  133. :complete (apply-partially #'ein:query-kernelspecs--complete url-or-port)
  134. :success (apply-partially #'ein:query-kernelspecs--success url-or-port callback)
  135. :error (apply-partially #'ein:query-kernelspecs--error url-or-port callback iteration)))
  136. (defun ein:normalize-kernelspec-language (name)
  137. "Normalize the kernelspec language string"
  138. (if (stringp name)
  139. (replace-regexp-in-string "[ ]" "-" name)
  140. name))
  141. (cl-defun ein:query-kernelspecs--success (url-or-port callback
  142. &key data _symbol-status _response &allow-other-keys)
  143. (let ((ks (list :default (plist-get data :default)))
  144. (specs (ein:plist-iter (plist-get data :kernelspecs))))
  145. (setf (gethash url-or-port *ein:kernelspecs*)
  146. (ein:flatten (dolist (spec specs ks)
  147. (let ((name (car spec))
  148. (info (cdr spec)))
  149. (push (list name (make-ein:$kernelspec :name (plist-get info :name)
  150. :display-name (plist-get (plist-get info :spec)
  151. :display_name)
  152. :resources (plist-get info :resources)
  153. :language (ein:normalize-kernelspec-language
  154. (plist-get (plist-get info :spec)
  155. :language))
  156. :spec (plist-get info :spec)))
  157. ks))))))
  158. (when callback (funcall callback)))
  159. (cl-defun ein:query-kernelspecs--error (url-or-port callback iteration
  160. &key response error-thrown &allow-other-keys)
  161. (if (< iteration 3)
  162. (progn
  163. (ein:log 'verbose "Retry kernelspecs #%s in response to %s" iteration (request-response-status-code response))
  164. (ein:query-kernelspecs url-or-port callback (1+ iteration)))
  165. (ein:log 'error
  166. "ein:query-kernelspecs--error %s: ERROR %s DATA %s" url-or-port (car error-thrown) (cdr error-thrown))
  167. (when callback (funcall callback))))
  168. (cl-defun ein:query-kernelspecs--complete (_url-or-port &key data response &allow-other-keys
  169. &aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
  170. (ein:log 'debug "ein:query-kernelspecs--complete %s" resp-string))
  171. (defsubst ein:notebook-version-numeric (url-or-port)
  172. (truncate (string-to-number (ein:need-notebook-version url-or-port))))
  173. (defun ein:need-notebook-version (url-or-port)
  174. "Callers assume ein:query-notebook-version succeeded. If not, we hardcode a guess."
  175. (ein:aif (gethash url-or-port *ein:notebook-version*) it
  176. (ein:log 'warn "No recorded notebook version for %s" url-or-port)
  177. "5.7.0"))
  178. (defun ein:query-notebook-version (url-or-port callback)
  179. "Send for notebook version of URL-OR-PORT with CALLBACK arity 0 (just a semaphore)"
  180. (ein:query-singleton-ajax
  181. (list 'query-notebook-version url-or-port)
  182. (ein:url url-or-port "api")
  183. :parser #'ein:json-read
  184. :sync ein:force-sync
  185. :complete (apply-partially #'ein:query-notebook-version--complete url-or-port callback)))
  186. (cl-defun ein:query-notebook-version--complete (url-or-port callback
  187. &key data response &allow-other-keys
  188. &aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
  189. (ein:log 'debug "ein:query-notebook-version--complete %s" resp-string)
  190. (ein:aif (plist-get data :version)
  191. (setf (gethash url-or-port *ein:notebook-version*) it)
  192. (cl-case (request-response-status-code response)
  193. (404 (ein:log 'warn "notebook version api not implemented")
  194. (setf (gethash url-or-port *ein:notebook-version*) "2.0.0"))
  195. (t (ein:log 'warn "notebook version currently unknowable"))))
  196. (when callback (funcall callback)))
  197. ;;; File name translation (tramp support)
  198. ;; Probably it's better to define `ein:filename-translations-get' as
  199. ;; an EIEIO method so that I don't have to re-define functions such as
  200. ;; `ein:kernel-filename-to-python' and `ein:kernel-filename-from-python'.
  201. (defun ein:filename-translations-get (url-or-port)
  202. (ein:choose-setting 'ein:filename-translations url-or-port))
  203. (defun ein:filename-to-python (url-or-port filename)
  204. (ein:aif (car (ein:filename-translations-get url-or-port))
  205. (funcall it filename)
  206. filename))
  207. (defun ein:filename-from-python (url-or-port filename)
  208. (ein:aif (cadr (ein:filename-translations-get url-or-port))
  209. (funcall it filename)
  210. filename))
  211. (defun ein:make-tramp-file-name (username remote-host python-filename)
  212. "Old (with multi-hops) tramp compatibility function.
  213. Adapted from `slime-make-tramp-file-name'."
  214. (if (boundp 'tramp-multi-methods)
  215. (tramp-make-tramp-file-name nil nil
  216. username
  217. remote-host
  218. python-filename)
  219. (tramp-make-tramp-file-name nil
  220. username
  221. remote-host
  222. python-filename)))
  223. (defun ein:tramp-create-filename-translator (remote-host &optional username)
  224. "Generate a pair of TO-PYTHON and FROM-PYTHON for
  225. `ein:filename-translations'.
  226. Usage::
  227. (setq ein:filename-translations
  228. `((8888
  229. . ,(ein:tramp-create-filename-translator \"MY-HOSTNAME\"))))
  230. ;; Equivalently:
  231. (setq ein:filename-translations
  232. (lambda (url-or-port)
  233. (when (equal url-or-port 8888)
  234. (ein:tramp-create-filename-translator \"MY-HOSTNAME\"))))
  235. This setting assumes that the IPython server which can be
  236. connected using the port 8888 in localhost is actually running in
  237. the host named MY-HOSTNAME.
  238. Adapted from `slime-create-filename-translator'."
  239. (require 'tramp)
  240. (let ((remote-host remote-host)
  241. (username (or username (user-login-name))))
  242. (list (lambda (emacs-filename)
  243. (tramp-file-name-localname
  244. (tramp-dissect-file-name emacs-filename)))
  245. (lambda (python-filename)
  246. (ein:make-tramp-file-name username remote-host python-filename)))))
  247. ;;; Generic getter
  248. (defun ein:generic-getter (func-list)
  249. "Internal function for generic getter functions (`ein:get-*').
  250. FUNC-LIST is a list of function which takes no argument and
  251. return what is desired or nil. Each function in FUNC-LIST is
  252. called one by one and the first non-nil result will be used. The
  253. function is not called when it is not bound. So, it is safe to
  254. give functions defined in lazy-loaded sub-modules.
  255. This is something similar to dispatching in generic function such
  256. as `defgeneric' in EIEIO, but it takes no argument. Actual
  257. implementation is chosen based on context (buffer, point, etc.).
  258. This helps writing generic commands which requires same object
  259. but can operate in different contexts."
  260. (cl-loop for func in func-list
  261. if (and (functionp func) (funcall func))
  262. return it))
  263. (defun ein:get-url-or-port ()
  264. (ein:generic-getter '(ein:get-url-or-port--notebooklist
  265. ein:get-url-or-port--notebook
  266. ein:get-url-or-port--worksheet
  267. ein:get-url-or-port--shared-output
  268. ein:get-url-or-port--connect)))
  269. (defun ein:get-notebook ()
  270. (ein:generic-getter '(ein:get-notebook--notebook
  271. ;; ein:get-notebook--shared-output
  272. ein:get-notebook--connect)))
  273. (defun ein:get-notebook-or-error ()
  274. (or (ein:get-notebook)
  275. (error "No notebook related to the current buffer.")))
  276. (defun ein:get-kernel ()
  277. (ein:generic-getter '(ein:get-kernel--notebook
  278. ein:get-kernel--worksheet
  279. ein:get-kernel--shared-output
  280. ein:get-kernel--connect
  281. ein:get-kernel--worksheet-in-edit-cell)))
  282. (defun ein:get-kernel-or-error ()
  283. (or (ein:get-kernel)
  284. (error "No kernel related to the current buffer.")))
  285. (defun ein:get-cell-at-point ()
  286. (ein:generic-getter '(ein:get-cell-at-point--worksheet
  287. ein:get-cell-at-point--shared-output)))
  288. (defun ein:get-traceback-data ()
  289. (ein:generic-getter '(ein:get-traceback-data--worksheet
  290. ein:get-traceback-data--shared-output
  291. ein:get-traceback-data--connect)))
  292. ;;; Emacs utilities
  293. (defun ein:clean-compiled-files ()
  294. (let* ((files (directory-files ein:source-dir 'full "^ein-.*\\.elc$")))
  295. (mapc #'delete-file files)
  296. (message "Removed %s byte-compiled files." (length files))))
  297. (defun ein:byte-compile-ein ()
  298. "Byte compile EIN files."
  299. (interactive)
  300. (ein:clean-compiled-files)
  301. (let* ((files (directory-files ein:source-dir 'full "^ein-.*\\.el$"))
  302. (errors (mapcan (lambda (f) (unless (byte-compile-file f) (list f)))
  303. files)))
  304. (ein:aif errors
  305. (error "Got %s errors while compiling these files: %s"
  306. (length errors)
  307. (ein:join-str " " (mapcar #'file-name-nondirectory it))))
  308. (message "Compiled %s files" (length files))))
  309. (provide 'ein-core)
  310. ;;; ein-core.el ends here