ein-notebooklist.el 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. ;;; ein-notebooklist.el --- Notebook list buffer
  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-notebooklist.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-notebooklist.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-notebooklist.el. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;;
  17. ;;; Code:
  18. (eval-when-compile (require 'cl))
  19. (require 'widget)
  20. (require 'ein-core)
  21. (require 'ein-notebook)
  22. (require 'ein-subpackages)
  23. (defcustom ein:notebooklist-first-open-hook nil
  24. "Hooks to run when the notebook list is opened at first time.
  25. Example to open a notebook named _scratch_ when the notebook list
  26. is opened at first time.::
  27. (add-hook
  28. 'ein:notebooklist-first-open-hook
  29. (lambda () (ein:notebooklist-open-notebook-by-name \"_scratch_\")))
  30. "
  31. :type 'hook
  32. :group 'ein)
  33. (defstruct ein:$notebooklist
  34. "Hold notebooklist variables.
  35. `ein:$notebooklist-url-or-port'
  36. URL or port of IPython server.
  37. `ein:$notebooklist-data'
  38. JSON data sent from the server."
  39. url-or-port
  40. data)
  41. (ein:deflocal ein:%notebooklist% nil
  42. "Buffer local variable to store an instance of `ein:$notebooklist'.")
  43. (define-obsolete-variable-alias 'ein:notebooklist 'ein:%notebooklist% "0.1.2")
  44. (defvar ein:notebooklist-buffer-name-template "*ein:notebooklist %s*")
  45. (defvar ein:notebooklist-map (make-hash-table :test 'equal)
  46. "Data store for `ein:notebooklist-list'.
  47. Mapping from URL-OR-PORT to an instance of `ein:$notebooklist'.")
  48. (defun ein:notebooklist-list ()
  49. "Get a list of opened `ein:$notebooklist'."
  50. (ein:hash-vals ein:notebooklist-map))
  51. (defun ein:notebooklist-list-add (nblist)
  52. "Register notebook list instance NBLIST for global lookup.
  53. This function adds NBLIST to `ein:notebooklist-map'."
  54. (puthash (ein:$notebooklist-url-or-port nblist)
  55. nblist
  56. ein:notebooklist-map))
  57. (defun ein:notebooklist-list-get (url-or-port)
  58. "Get an instance of `ein:$notebooklist' by URL-OR-PORT as a key."
  59. (gethash url-or-port ein:notebooklist-map))
  60. (defun ein:notebooklist-open-notebook-by-name (name &optional url-or-port
  61. callback cbargs)
  62. "Open notebook named NAME in the server URL-OR-PORT.
  63. If URL-OR-PORT is not given or `nil', and the current buffer is
  64. the notebook list buffer, the notebook is searched in the
  65. notebook list of the current buffer.
  66. When used in lisp, CALLBACK and CBARGS are passed to `ein:notebook-open'.
  67. To suppress popup, you can pass a function `ein:do-nothing' as CALLBACK."
  68. (loop with nblist = (if url-or-port
  69. (ein:notebooklist-list-get url-or-port)
  70. ein:%notebooklist%)
  71. for note in (ein:$notebooklist-data nblist)
  72. for notebook-name = (plist-get note :name)
  73. for notebook-id = (plist-get note :notebook_id)
  74. when (equal notebook-name name)
  75. return (ein:notebook-open (ein:$notebooklist-url-or-port nblist)
  76. notebook-id callback cbargs)))
  77. (defun ein:notebooklist-url (url-or-port)
  78. (ein:url url-or-port "notebooks"))
  79. (defun ein:notebooklist-new-url (url-or-port)
  80. (ein:url url-or-port "new"))
  81. (defun ein:notebooklist-get-buffer (url-or-port)
  82. (get-buffer-create
  83. (format ein:notebooklist-buffer-name-template url-or-port)))
  84. (defun ein:notebooklist-ask-url-or-port ()
  85. (let* ((url-or-port-list (mapcar (lambda (x) (format "%s" x))
  86. ein:url-or-port))
  87. (default (format "%s" (ein:aif (ein:get-notebook)
  88. (ein:$notebook-url-or-port it)
  89. (ein:aif ein:%notebooklist%
  90. (ein:$notebooklist-url-or-port it)
  91. (ein:default-url-or-port)))))
  92. (url-or-port
  93. (completing-read (format "URL or port number (default %s): " default)
  94. url-or-port-list
  95. nil nil nil nil
  96. default)))
  97. (if (string-match "^[0-9]+$" url-or-port)
  98. (string-to-number url-or-port)
  99. url-or-port)))
  100. ;;;###autoload
  101. (defun ein:notebooklist-open (&optional url-or-port no-popup)
  102. "Open notebook list buffer."
  103. (interactive (list (ein:notebooklist-ask-url-or-port)))
  104. (unless url-or-port (setq url-or-port (ein:default-url-or-port)))
  105. (ein:subpackages-load)
  106. (let ((success
  107. (if no-popup
  108. #'ein:notebooklist-url-retrieve-callback
  109. (lambda (&rest args)
  110. (pop-to-buffer
  111. (apply #'ein:notebooklist-url-retrieve-callback args))))))
  112. (ein:query-singleton-ajax
  113. (list 'notebooklist-open url-or-port)
  114. (ein:notebooklist-url url-or-port)
  115. :parser #'ein:json-read
  116. :error (apply-partially #'ein:notebooklist-open-error url-or-port)
  117. :success (apply-partially success url-or-port)))
  118. (ein:notebooklist-get-buffer url-or-port))
  119. (defun* ein:notebooklist-url-retrieve-callback (url-or-port
  120. &key
  121. data
  122. &allow-other-keys)
  123. "Called via `ein:notebooklist-open'."
  124. (with-current-buffer (ein:notebooklist-get-buffer url-or-port)
  125. (let ((already-opened-p (ein:notebooklist-list-get url-or-port))
  126. (orig-point (point)))
  127. (setq ein:%notebooklist%
  128. (make-ein:$notebooklist :url-or-port url-or-port
  129. :data data))
  130. (ein:notebooklist-list-add ein:%notebooklist%)
  131. (ein:notebooklist-render)
  132. (goto-char orig-point)
  133. (ein:log 'info "Opened notebook list at %s" url-or-port)
  134. (unless already-opened-p
  135. (run-hooks 'ein:notebooklist-first-open-hook))
  136. (current-buffer))))
  137. (defun* ein:notebooklist-open-error (url-or-port
  138. &key symbol-status response
  139. &allow-other-keys)
  140. (ein:log 'verbose
  141. "Error thrown: %S" (request-response-error-thrown response))
  142. (ein:log 'error
  143. "Error (%s) while opening notebook list at the server %s."
  144. symbol-status url-or-port))
  145. ;;;###autoload
  146. (defun ein:notebooklist-reload ()
  147. "Reload current Notebook list."
  148. (interactive)
  149. (ein:notebooklist-open (ein:$notebooklist-url-or-port ein:%notebooklist%) t))
  150. (defun ein:notebooklist-refresh-related ()
  151. "Reload notebook list in which current notebook locates.
  152. This function is called via `ein:notebook-after-rename-hook'."
  153. (ein:notebooklist-open (ein:$notebook-url-or-port ein:%notebook%) t))
  154. (add-hook 'ein:notebook-after-rename-hook 'ein:notebooklist-refresh-related)
  155. (defun ein:notebooklist-open-notebook (nblist notebook-id &optional name
  156. callback cbargs)
  157. (ein:notebook-open (ein:$notebooklist-url-or-port nblist) notebook-id
  158. callback cbargs))
  159. ;;;###autoload
  160. (defun ein:notebooklist-new-notebook (&optional url-or-port callback cbargs)
  161. "Ask server to create a new notebook and open it in a new buffer."
  162. (interactive (list (ein:notebooklist-ask-url-or-port)))
  163. (ein:log 'info "Creating a new notebook...")
  164. (unless url-or-port
  165. (setq url-or-port (ein:$notebooklist-url-or-port ein:%notebooklist%)))
  166. (assert url-or-port nil
  167. (concat "URL-OR-PORT is not given and the current buffer "
  168. "is not the notebook list buffer."))
  169. (ein:query-singleton-ajax
  170. (list 'notebooklist-new-notebook url-or-port)
  171. (ein:notebooklist-new-url url-or-port)
  172. :parser (lambda ()
  173. (ein:html-get-data-in-body-tag "data-notebook-id"))
  174. :error (apply-partially #'ein:notebooklist-new-notebook-error
  175. url-or-port callback cbargs)
  176. :success (apply-partially #'ein:notebooklist-new-notebook-callback
  177. url-or-port callback cbargs)))
  178. (defun* ein:notebooklist-new-notebook-callback (url-or-port
  179. callback
  180. cbargs
  181. &key
  182. data
  183. &allow-other-keys
  184. &aux
  185. (notebook-id data)
  186. (no-popup t))
  187. (ein:log 'info "Creating a new notebook... Done.")
  188. (if notebook-id
  189. (ein:notebook-open url-or-port notebook-id callback cbargs)
  190. (ein:log 'info (concat "Oops. EIN failed to open new notebook. "
  191. "Please find it in the notebook list."))
  192. (setq no-popup nil))
  193. ;; reload or open notebook list
  194. (ein:notebooklist-open url-or-port no-popup))
  195. (defun* ein:notebooklist-new-notebook-error
  196. (url-or-port callback cbargs
  197. &key response &allow-other-keys
  198. &aux
  199. (no-popup t)
  200. (error (request-response-error-thrown response))
  201. (dest (request-response-url response)))
  202. (ein:log 'verbose
  203. "NOTEBOOKLIST-NEW-NOTEBOOK-ERROR url-or-port: %S; error: %S; dest: %S"
  204. url-or-port error dest)
  205. (ein:log 'error
  206. "Failed to open new notebook (error: %S). \
  207. You may find the new one in the notebook list." error)
  208. (setq no-popup nil)
  209. (ein:notebooklist-open url-or-port no-popup))
  210. ;;;###autoload
  211. (defun ein:notebooklist-new-notebook-with-name (name &optional url-or-port)
  212. "Open new notebook and rename the notebook."
  213. (interactive (let* ((url-or-port (or (ein:get-url-or-port)
  214. (ein:default-url-or-port)))
  215. (name (read-from-minibuffer
  216. (format "Notebook name (at %s): " url-or-port))))
  217. (list name url-or-port)))
  218. (ein:notebooklist-new-notebook
  219. url-or-port
  220. (lambda (notebook created name)
  221. (assert created)
  222. (with-current-buffer (ein:notebook-buffer notebook)
  223. (ein:notebook-rename-command name)
  224. ;; As `ein:notebook-open' does not call `pop-to-buffer' when
  225. ;; callback is specified, `pop-to-buffer' must be called here:
  226. (pop-to-buffer (current-buffer))))
  227. (list name)))
  228. (defun ein:notebooklist-delete-notebook-ask (notebook-id name)
  229. (when (y-or-n-p (format "Delete notebook %s?" name))
  230. (ein:notebooklist-delete-notebook notebook-id name)))
  231. (defun ein:notebooklist-delete-notebook (notebook-id name)
  232. (ein:log 'info "Deleting notebook %s..." name)
  233. (ein:query-singleton-ajax
  234. (list 'notebooklist-delete-notebook
  235. (ein:$notebooklist-url-or-port ein:%notebooklist%) notebook-id)
  236. (ein:notebook-url-from-url-and-id
  237. (ein:$notebooklist-url-or-port ein:%notebooklist%)
  238. notebook-id)
  239. :type "DELETE"
  240. :success (apply-partially (lambda (buffer name &rest ignore)
  241. (ein:log 'info
  242. "Deleting notebook %s... Done." name)
  243. (with-current-buffer buffer
  244. (ein:notebooklist-reload)))
  245. (current-buffer) name)))
  246. (defun ein:notebooklist-render ()
  247. "Render notebook list widget.
  248. Notebook list data is passed via the buffer local variable
  249. `ein:notebooklist-data'."
  250. (kill-all-local-variables)
  251. (let ((inhibit-read-only t))
  252. (erase-buffer))
  253. (remove-overlays)
  254. ;; Create notebook list
  255. (widget-insert "IPython Notebook list\n\n")
  256. (widget-create
  257. 'link
  258. :notify (lambda (&rest ignore) (ein:notebooklist-new-notebook))
  259. "New Notebook")
  260. (widget-insert " ")
  261. (widget-create
  262. 'link
  263. :notify (lambda (&rest ignore) (ein:notebooklist-reload))
  264. "Reload List")
  265. (widget-insert " ")
  266. (widget-create
  267. 'link
  268. :notify (lambda (&rest ignore)
  269. (browse-url
  270. (ein:url (ein:$notebooklist-url-or-port ein:%notebooklist%))))
  271. "Open In Browser")
  272. (widget-insert "\n")
  273. (loop for note in (ein:$notebooklist-data ein:%notebooklist%)
  274. for name = (plist-get note :name)
  275. for notebook-id = (plist-get note :notebook_id)
  276. do (progn (widget-create
  277. 'link
  278. :notify (lexical-let ((name name)
  279. (notebook-id notebook-id))
  280. (lambda (&rest ignore)
  281. (ein:notebooklist-open-notebook
  282. ein:%notebooklist% notebook-id name)))
  283. "Open")
  284. (widget-insert " ")
  285. (widget-create
  286. 'link
  287. :notify (lexical-let ((name name)
  288. (notebook-id notebook-id))
  289. (lambda (&rest ignore)
  290. (ein:notebooklist-delete-notebook-ask
  291. notebook-id
  292. name)))
  293. "Delete")
  294. (widget-insert " : " name)
  295. (widget-insert "\n")))
  296. (ein:notebooklist-mode)
  297. (widget-setup))
  298. ;;;###autoload
  299. (defun ein:notebooklist-list-notebooks ()
  300. "Return a list of notebook path (NBPATH). Each element NBPATH
  301. is a string of the format \"URL-OR-PORT/NOTEBOOK-NAME\"."
  302. (apply #'append
  303. (loop for nblist in (ein:notebooklist-list)
  304. for url-or-port = (ein:$notebooklist-url-or-port nblist)
  305. collect
  306. (loop for note in (ein:$notebooklist-data nblist)
  307. collect (format "%s/%s"
  308. url-or-port
  309. (plist-get note :name))))))
  310. ;;;###autoload
  311. (defun ein:notebooklist-open-notebook-global (nbpath &optional callback cbargs)
  312. "Choose notebook from all opened notebook list and open it.
  313. Notebook is specified by a string NBPATH whose format is
  314. \"URL-OR-PORT/NOTEBOOK-NAME\".
  315. When used in lisp, CALLBACK and CBARGS are passed to `ein:notebook-open'."
  316. (interactive
  317. (list (completing-read
  318. "Open notebook [URL-OR-PORT/NAME]: "
  319. (ein:notebooklist-list-notebooks))))
  320. (let* ((path (split-string nbpath "/"))
  321. (url-or-port (car path))
  322. (name (cadr path)))
  323. (when (and (stringp url-or-port)
  324. (string-match "^[0-9]+$" url-or-port))
  325. (setq url-or-port (string-to-number url-or-port)))
  326. (let ((notebook-id
  327. (loop for nblist in (ein:notebooklist-list)
  328. when (equal (ein:$notebooklist-url-or-port nblist) url-or-port)
  329. if (loop for note in (ein:$notebooklist-data nblist)
  330. when (equal (plist-get note :name) name)
  331. return (plist-get note :notebook_id))
  332. return it)))
  333. (if notebook-id
  334. (ein:notebook-open url-or-port notebook-id callback cbargs)
  335. (ein:log 'info "Notebook '%s' not found" nbpath)))))
  336. ;;;###autoload
  337. (defun ein:notebooklist-load (&optional url-or-port)
  338. "Load notebook list but do not pop-up the notebook list buffer.
  339. For example, if you want to load notebook list when Emacs starts,
  340. add this in the Emacs initialization file::
  341. (add-to-hook 'after-init-hook 'ein:notebooklist-load)
  342. or even this (if you want fast Emacs start-up)::
  343. ;; load notebook list if Emacs is idle for 3 sec after start-up
  344. (run-with-idle-timer 3 nil #'ein:notebooklist-load)
  345. You should setup `ein:url-or-port' or `ein:default-url-or-port'
  346. in order to make this code work.
  347. See also:
  348. `ein:connect-to-default-notebook', `ein:connect-default-notebook'."
  349. (ein:notebooklist-open url-or-port t))
  350. (defun ein:notebooklist-find-server-by-notebook-name (name)
  351. "Find a notebook named NAME and return a list (URL-OR-PORT NOTEBOOK-ID)."
  352. (loop named outer
  353. for nblist in (ein:notebooklist-list)
  354. for url-or-port = (ein:$notebooklist-url-or-port nblist)
  355. do (loop for note in (ein:$notebooklist-data nblist)
  356. when (equal (plist-get note :name) name)
  357. do (return-from outer
  358. (list url-or-port (plist-get note :notebook_id))))))
  359. (defun ein:notebooklist-open-notebook-by-file-name
  360. (&optional filename noerror buffer-callback)
  361. "Find the notebook named as same as the current file in the servers.
  362. Open the notebook if found. Note that this command will *not*
  363. upload the current file to the server.
  364. .. When FILENAME is unspecified the variable `buffer-file-name'
  365. is used instead. Set NOERROR to non-`nil' to suppress errors.
  366. BUFFER-CALLBACK is called after opening notebook with the
  367. current buffer as the only one argument."
  368. (interactive (progn (assert buffer-file-name nil "Not visiting a file.")
  369. nil))
  370. (unless filename (setq filename buffer-file-name))
  371. (assert filename nil "No file found.")
  372. (let* ((name (file-name-sans-extension
  373. (file-name-nondirectory (or filename))))
  374. (found (ein:notebooklist-find-server-by-notebook-name name))
  375. (callback (lambda (-ignore-1- -ignore-2- buffer buffer-callback)
  376. (ein:notebook-pop-to-current-buffer) ; default
  377. (when (buffer-live-p buffer)
  378. (funcall buffer-callback buffer))))
  379. (cbargs (list (current-buffer) (or buffer-callback #'ignore))))
  380. (unless noerror
  381. (assert found nil "No server has notebook named: %s" name))
  382. (destructuring-bind (url-or-port notebook-id) found
  383. (ein:notebook-open url-or-port notebook-id callback cbargs))))
  384. (defvar ein:notebooklist-find-file-buffer-callback #'ignore)
  385. (defun ein:notebooklist-find-file-callback ()
  386. "A callback function for `find-file-hook' to open notebook.
  387. FIMXE: document how to use `ein:notebooklist-find-file-callback'
  388. when I am convinced with the API."
  389. (ein:and-let* ((filename buffer-file-name)
  390. ((string-match-p "\\.ipynb$" filename)))
  391. (ein:notebooklist-open-notebook-by-file-name
  392. filename t ein:notebooklist-find-file-buffer-callback)))
  393. ;;; Login
  394. (defun ein:notebooklist-login (url-or-port password)
  395. "Login to IPython notebook server."
  396. (interactive (list (ein:notebooklist-ask-url-or-port)
  397. (read-passwd "Password: ")))
  398. (ein:log 'debug "NOTEBOOKLIST-LOGIN: %s" url-or-port)
  399. (ein:query-singleton-ajax
  400. (list 'notebooklist-login url-or-port)
  401. (ein:url url-or-port "login")
  402. :type "POST"
  403. :data (concat "password=" (url-hexify-string password))
  404. :parser #'ein:notebooklist-login--parser
  405. :error (apply-partially #'ein:notebooklist-login--error url-or-port)
  406. :success (apply-partially #'ein:notebooklist-login--success url-or-port)))
  407. (defun ein:notebooklist-login--parser ()
  408. (goto-char (point-min))
  409. (list :bad-page (re-search-forward "<input type=.?password" nil t)))
  410. (defun ein:notebooklist-login--success-1 (url-or-port)
  411. (ein:log 'info "Login to %s complete. \
  412. Now you can open notebook list by `ein:notebooklist-open'." url-or-port))
  413. (defun ein:notebooklist-login--error-1 (url-or-port)
  414. (ein:log 'info "Failed to login to %s" url-or-port))
  415. (defun* ein:notebooklist-login--success (url-or-port &key
  416. data
  417. &allow-other-keys)
  418. (if (plist-get data :bad-page)
  419. (ein:notebooklist-login--error-1 url-or-port)
  420. (ein:notebooklist-login--success-1 url-or-port)))
  421. (defun* ein:notebooklist-login--error
  422. (url-or-port &key
  423. data
  424. symbol-status
  425. response
  426. &allow-other-keys
  427. &aux
  428. (response-status (request-response-status-code response)))
  429. (if (or
  430. ;; workaround for url-retrieve backend
  431. (and (eq symbol-status 'timeout)
  432. (equal response-status 302)
  433. (request-response-header response "set-cookie"))
  434. ;; workaround for curl backend
  435. (and (equal response-status 405)
  436. (ein:aand (car (request-response-history response))
  437. (request-response-header it "set-cookie"))))
  438. (ein:notebooklist-login--success-1 url-or-port)
  439. (ein:notebooklist-login--error-1 url-or-port)))
  440. ;;; Generic getter
  441. (defun ein:get-url-or-port--notebooklist ()
  442. (when (ein:$notebooklist-p ein:%notebooklist%)
  443. (ein:$notebooklist-url-or-port ein:%notebooklist%)))
  444. ;;; Notebook list mode
  445. (define-derived-mode ein:notebooklist-mode fundamental-mode "ein:notebooklist"
  446. "IPython notebook list mode.")
  447. (defun ein:notebooklist-prev-item () (interactive) (move-beginning-of-line 0))
  448. (defun ein:notebooklist-next-item () (interactive) (move-beginning-of-line 2))
  449. (setq ein:notebooklist-mode-map (copy-keymap widget-keymap))
  450. (let ((map ein:notebooklist-mode-map))
  451. (define-key map "\C-c\C-r" 'ein:notebooklist-reload)
  452. (define-key map "g" 'ein:notebooklist-reload)
  453. (define-key map "p" 'ein:notebooklist-prev-item)
  454. (define-key map "n" 'ein:notebooklist-next-item)
  455. (define-key map "q" 'bury-buffer)
  456. (easy-menu-define ein:notebooklist-menu map "EIN Notebook List Mode Menu"
  457. `("EIN Notebook List"
  458. ,@(ein:generate-menu
  459. '(("Reload" ein:notebooklist-reload)
  460. ("New Notebook" ein:notebooklist-new-notebook)
  461. ("New Notebook (with name)"
  462. ein:notebooklist-new-notebook-with-name)
  463. ("New Junk Notebook" ein:junk-new))))))
  464. (provide 'ein-notebooklist)
  465. ;;; ein-notebooklist.el ends here