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.

768 lines
27 KiB

  1. ;;; ein-utils.el --- Utility module -*- 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-utils.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-utils.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-utils.el. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;;
  17. ;;; Code:
  18. (require 'cc-mode)
  19. (require 'json)
  20. (require 's)
  21. (require 'dash)
  22. (require 'url)
  23. (require 'deferred)
  24. ;;; Macros and core functions/variables
  25. (defmacro ein:with-undo-disabled (&rest body)
  26. "Temporarily disable undo recording while executing `body`
  27. while maintaining the undo list for the current buffer."
  28. `(let ((buffer-undo-list t))
  29. ,@body))
  30. (defmacro ein:aif (test-form then-form &rest else-forms)
  31. "Anaphoric IF. Adapted from `e2wm:aif'."
  32. (declare (debug (form form &rest form)))
  33. `(let ((it ,test-form))
  34. (if it ,then-form ,@else-forms)))
  35. (put 'ein:aif 'lisp-indent-function 2)
  36. (defmacro ein:aand (test &rest rest)
  37. "Anaphoric AND. Adapted from `e2wm:aand'."
  38. (declare (debug (form &rest form)))
  39. `(let ((it ,test))
  40. (if it ,(if rest (macroexpand-all `(ein:aand ,@rest)) 'it))))
  41. (defmacro ein:and-let* (bindings &rest form)
  42. "Gauche's `and-let*'."
  43. (declare (debug ((&rest &or symbolp (form) (gate symbolp &optional form))
  44. body))
  45. ;; See: (info "(elisp) Specification List")
  46. (indent 1))
  47. (if (null bindings)
  48. `(progn ,@form)
  49. (let* ((head (car bindings))
  50. (tail (cdr bindings))
  51. (rest (macroexpand-all `(ein:and-let* ,tail ,@form))))
  52. (cond
  53. ((symbolp head) `(if ,head ,rest))
  54. ((= (length head) 1) `(if ,(car head) ,rest))
  55. (t `(let (,head) (if ,(car head) ,rest)))))))
  56. (defvar ein:local-variables '()
  57. "Modified by `ein:deflocal'")
  58. (defmacro ein:deflocal (name &optional initvalue docstring)
  59. "Define permanent buffer local variable named NAME.
  60. INITVALUE and DOCSTRING are passed to `defvar'."
  61. (declare (indent defun)
  62. (doc-string 3))
  63. `(progn
  64. (defvar ,name ,initvalue ,docstring)
  65. (make-variable-buffer-local ',name)
  66. (put ',name 'permanent-local t)
  67. (setq ein:local-variables (append ein:local-variables '(,name)))))
  68. (defmacro ein:with-read-only-buffer (buffer &rest body)
  69. (declare (indent 1))
  70. `(with-current-buffer ,buffer
  71. (setq buffer-read-only t)
  72. (save-excursion
  73. (let ((inhibit-read-only t))
  74. ,@body))))
  75. (defmacro ein:with-live-buffer (buffer &rest body)
  76. "Execute BODY in BUFFER if BUFFER is alive."
  77. (declare (indent 1) (debug t))
  78. `(when (buffer-live-p ,buffer)
  79. (with-current-buffer ,buffer
  80. ,@body)))
  81. (defmacro ein:with-possibly-killed-buffer (buffer &rest body)
  82. "Execute BODY in BUFFER if BUFFER is live.
  83. Execute BODY if BUFFER is not live anyway."
  84. (declare (indent 1) (debug t))
  85. `(if (buffer-live-p ,buffer)
  86. (with-current-buffer ,buffer
  87. ,@body)
  88. ,@body))
  89. (defvar ein:dotty-syntax-table
  90. (let ((table (make-syntax-table c-mode-syntax-table)))
  91. (modify-syntax-entry ?. "w" table)
  92. (modify-syntax-entry ?_ "w" table)
  93. (modify-syntax-entry ?% "w" table)
  94. table)
  95. "Adapted from `python-dotty-syntax-table'.")
  96. (defun ein:beginning-of-object (&optional code-syntax-table)
  97. "Move to the beginning of the dotty.word.at.point. User may
  98. specify a custom syntax table. If one is not supplied `ein:dotty-syntax-table' will
  99. be assumed."
  100. (with-syntax-table (or code-syntax-table ein:dotty-syntax-table)
  101. (while (re-search-backward "\\(\\sw\\|\\s_\\|\\s\\.\\|\\s\\\\|[%@|]\\)\\="
  102. (when (> (point) 2000) (- (point) 2000))
  103. t))
  104. (re-search-forward "\\=#[-+.<|]" nil t)
  105. (when (and (looking-at "@"))
  106. (forward-char))))
  107. (defun ein:end-of-object (&optional code-syntax-table)
  108. "Move to the end of the dotty.word.at.point. User may specify a
  109. custom syntax table. If one is not supplied
  110. `ein:dotty-syntax-table' will be assumed."
  111. (with-syntax-table (or code-syntax-table ein:dotty-syntax-table)
  112. (re-search-forward "\\=\\(\\sw\\|\\s_\\|\\s\\.\\|#:\\|[%|]\\)*")))
  113. (defun ein:object-start-pos ()
  114. "Return the starting position of the symbol under point.
  115. The result is unspecified if there isn't a symbol under the point."
  116. (save-excursion (ein:beginning-of-object) (point)))
  117. (defun ein:object-end-pos ()
  118. (save-excursion (ein:end-of-object) (point)))
  119. (defun ein:object-prefix-at-point ()
  120. "Similar to `ein:object-at-point', but instead of returning the entire object
  121. only returns the string up to the current point. For example, given pd.Series, if the
  122. cursor is at the S then 'pd.S' will be returned."
  123. (if (ein:object-at-point)
  124. (let* ((obj (ein:object-at-point))
  125. (delta (- (point) (ein:object-start-pos))))
  126. (substring obj 0 delta))))
  127. (defun ein:object-at-point ()
  128. "Return dotty.words.at.point.
  129. When region is active, text in region is returned after trimmed
  130. white spaces, newlines and dots.
  131. When object is not found at the point, return the object just
  132. before previous opening parenthesis."
  133. ;; For auto popup tooltip (or something like eldoc), probably it is
  134. ;; better to return function (any word before "("). I should write
  135. ;; another function or add option to this function when the auto
  136. ;; popup tooltip is implemented.
  137. (if (region-active-p)
  138. (ein:trim (buffer-substring (region-beginning) (region-end))
  139. "\\s-\\|\n\\|\\.")
  140. (save-excursion
  141. (with-syntax-table ein:dotty-syntax-table
  142. (ein:aif (thing-at-point 'symbol)
  143. it
  144. (unless (looking-at "(")
  145. (search-backward "(" (point-at-bol) t))
  146. (thing-at-point 'symbol))))))
  147. (defun ein:function-at-point ()
  148. "Similar to `ein:object-at-point', but instead will looking for the function
  149. at point, i.e. any word before then \"(\", if it is present."
  150. (save-excursion
  151. (unless (looking-at "(")
  152. (search-backward "(" (point-at-bol) t))
  153. (ein:object-at-point)))
  154. (defun ein:object-at-point-or-error ()
  155. (or (ein:object-at-point) (error "No object found at the point")))
  156. (defun ein:flatten (tree)
  157. "Traverses the tree in order, collecting non-null leaves into a list."
  158. (let (list)
  159. (cl-labels ((traverse (subtree)
  160. (when subtree
  161. (if (consp subtree)
  162. (progn
  163. (traverse (car subtree))
  164. (traverse (cdr subtree)))
  165. (push subtree list)))))
  166. (traverse tree))
  167. (nreverse list)))
  168. ;;; URL utils
  169. (defvar ein:url-localhost "127.0.0.1")
  170. (defsubst ein:glom-paths (&rest paths)
  171. (cl-loop with result = ""
  172. for p in paths
  173. if (not (zerop (length p)))
  174. do (setq result (concat result (ein:trim-left (directory-file-name p) "/") "/"))
  175. end
  176. finally return (directory-file-name result)))
  177. (defun ein:url (url-or-port &rest paths)
  178. (when url-or-port
  179. (if (or (integerp url-or-port)
  180. (and (stringp url-or-port) (string-match "^[0-9]+$" url-or-port)))
  181. (setq url-or-port (format "http://localhost:%s" url-or-port)))
  182. (let ((parsed-url (url-generic-parse-url url-or-port)))
  183. (when (null (url-host parsed-url))
  184. (setq url-or-port (concat "https://" url-or-port))
  185. (setq parsed-url (url-generic-parse-url url-or-port)))
  186. (when (or (string= (url-host parsed-url) "localhost")
  187. (string= (url-host parsed-url) ein:url-localhost)
  188. (string= (url-host parsed-url) ""))
  189. (setf (url-host parsed-url) ein:url-localhost)
  190. (setf (url-type parsed-url) "http"))
  191. (directory-file-name (concat (file-name-as-directory (url-recreate-url parsed-url))
  192. (apply #'ein:glom-paths paths))))))
  193. (defun ein:url-no-cache (url)
  194. "Imitate `cache=false' of `jQuery.ajax'.
  195. See: http://api.jquery.com/jQuery.ajax/"
  196. (concat url (format-time-string "?_=%s")))
  197. ;;; HTML utils
  198. (defun ein:html-get-data-in-body-tag (key)
  199. "Very ad-hoc parser to get data in body tag."
  200. (ignore-errors
  201. (save-excursion
  202. (goto-char (point-min))
  203. (search-forward "<body")
  204. (search-forward-regexp (format "%s=\\([^[:space:]\n]+\\)" key))
  205. (match-string 1))))
  206. ;;; JSON utils
  207. (defmacro ein:with-json-setting (&rest body)
  208. `(let ((json-object-type 'plist)
  209. (json-array-type 'list))
  210. ,@body))
  211. (defun ein:json-read ()
  212. "Read json from `url-retrieve'-ed buffer.
  213. * `json-object-type' is `plist'. This is mainly for readability.
  214. * `json-array-type' is `list'. Notebook data is edited locally thus
  215. data type must be edit-friendly. `vector' type is not."
  216. (goto-char (point-max))
  217. (backward-sexp)
  218. (ein:with-json-setting
  219. (json-read)))
  220. (defun ein:json-read-from-string (string)
  221. (ein:with-json-setting
  222. (json-read-from-string string)))
  223. (defun ein:json-any-to-bool (obj)
  224. (if (and obj (not (eq obj json-false))) t json-false))
  225. ;; (defun ein:json-encode-char (char)
  226. ;; "Fixed `json-encode-char'."
  227. ;; (setq char (json-encode-char0 char 'ucs))
  228. ;; (let ((control-char (car (rassoc char json-special-chars))))
  229. ;; (cond
  230. ;; ;; Special JSON character (\n, \r, etc.).
  231. ;; (control-char
  232. ;; (format "\\%c" control-char))
  233. ;; ;; ASCIIish printable character.
  234. ;; ((and (> char 31) (< char 127)) ; s/161/127/
  235. ;; (format "%c" char))
  236. ;; ;; Fallback: UCS code point in \uNNNN form.
  237. ;; (t
  238. ;; (format "\\u%04x" char)))))
  239. ;; (defadvice json-encode-char (around ein:json-encode-char (char) activate)
  240. ;; "Replace `json-encode-char' with `ein:json-encode-char'."
  241. ;; (setq ad-return-value (ein:json-encode-char char)))
  242. ;; (defadvice json-encode (around encode-nil-as-json-empty-object activate)
  243. ;; (if (null object)
  244. ;; (setq ad-return-value "{}")
  245. ;; ad-do-it))
  246. ;;; EWOC
  247. (defun ein:ewoc-create (pretty-printer &optional header footer nosep)
  248. "Do nothing wrapper of `ewoc-create' to provide better error message."
  249. (condition-case nil
  250. (ewoc-create pretty-printer header footer nosep)
  251. ((debug wrong-number-of-arguments)
  252. (ein:display-warning "Incompatible EWOC version.
  253. The version of ewoc.el you are using is too old for EIN.
  254. Please install the newer version.
  255. See also: https://github.com/tkf/emacs-ipython-notebook/issues/49")
  256. (error "Incompatible EWOC version."))))
  257. ;;; Text property
  258. (defun ein:propertize-read-only (string &rest properties)
  259. (apply #'propertize string 'read-only t 'front-sticky t properties))
  260. (defun ein:insert-read-only (string &rest properties)
  261. (insert (apply #'ein:propertize-read-only
  262. (ein:maybe-truncate-string-lines string ein:truncate-long-cell-output)
  263. properties)))
  264. ;;; String manipulation
  265. (defun ein:maybe-truncate-string-lines (string nlines)
  266. "Truncate multi-line `string' to the number of lines specified by `nlines'. If actual
  267. number of lines is less than `nlines' then just return the string."
  268. (if nlines
  269. (let ((lines (split-string string "[\n]")))
  270. (if (> (length lines) nlines)
  271. (ein:join-str "\n" (append (butlast lines (- (length lines) nlines))
  272. (list "...")))
  273. string))
  274. string))
  275. (defun ein:trim (string &optional regexp)
  276. (ein:trim-left (ein:trim-right string regexp) regexp))
  277. (defun ein:trim-left (string &optional regexp)
  278. (unless regexp (setq regexp "\\s-\\|\n"))
  279. (ein:trim-regexp string (format "^\\(%s\\)+" regexp)))
  280. (defun ein:trim-right (string &optional regexp)
  281. (unless regexp (setq regexp "\\s-\\|\n"))
  282. (ein:trim-regexp string (format "\\(%s\\)+$" regexp)))
  283. (defun ein:trim-regexp (string regexp)
  284. (if (string-match regexp string)
  285. (replace-match "" t t string)
  286. string))
  287. (defun ein:trim-indent (string)
  288. "Strip uniform amount of indentation from lines in STRING."
  289. (let* ((lines (split-string string "\n"))
  290. (indent
  291. (let ((lens
  292. (cl-loop for line in lines
  293. for stripped = (ein:trim-left line)
  294. unless (equal stripped "")
  295. collect (- (length line) (length stripped)))))
  296. (if lens (apply #'min lens) 0)))
  297. (trimmed
  298. (cl-loop for line in lines
  299. if (> (length line) indent)
  300. collect (ein:trim-right (substring line indent))
  301. else
  302. collect line)))
  303. (ein:join-str "\n" trimmed)))
  304. (defun ein:join-str (sep strings)
  305. (mapconcat 'identity strings sep))
  306. (defun ein:join-path (paths)
  307. (mapconcat 'file-name-as-directory paths ""))
  308. (defun ein:string-fill-paragraph (string &optional justify)
  309. (with-temp-buffer
  310. (erase-buffer)
  311. (insert string)
  312. (goto-char (point-min))
  313. (fill-paragraph justify)
  314. (buffer-string)))
  315. (defmacro ein:case-equal (str &rest clauses)
  316. "Similar to `case' but comparison is done by `equal'.
  317. Adapted from twittering-mode.el's `case-string'."
  318. (declare (indent 1))
  319. `(cond
  320. ,@(mapcar
  321. (lambda (clause)
  322. (let ((keylist (car clause))
  323. (body (cdr clause)))
  324. `(,(if (listp keylist)
  325. `(or ,@(mapcar (lambda (key) `(equal ,str ,key))
  326. keylist))
  327. 't)
  328. ,@body)))
  329. clauses)))
  330. ;;; Text manipulation on buffer
  331. (defun ein:find-leftmot-column (beg end)
  332. "Return the leftmost column in region BEG to END."
  333. (save-excursion
  334. (let (mincol)
  335. (goto-char beg)
  336. (while (< (point) end)
  337. (back-to-indentation)
  338. (unless (= (point) (point-at-eol))
  339. (setq mincol (if mincol
  340. (min mincol (current-column))
  341. (current-column))))
  342. (unless (= (forward-line 1) 0)
  343. (cl-return-from ein:find-leftmot-column mincol)))
  344. mincol)))
  345. ;;; Misc
  346. (defun ein:completing-read (&rest args)
  347. "Wrap for emacs completing read functionality. Unless a more sophisticated completion framework has been installed (like helm or ivy), this function will default to using the slightly more sane ido completion framework. Arguments are the same as for `completing-read'."
  348. (if (eq completing-read-function 'completing-read-default)
  349. (apply #'ido-completing-read args)
  350. (apply completing-read-function args)))
  351. (defun ein:plist-iter (plist)
  352. "Return list of (key . value) in PLIST."
  353. ;; FIXME: this is not needed. See: `ein:plist-exclude'.
  354. (cl-loop for p in plist
  355. for i from 0
  356. for key-p = (= (% i 2) 0)
  357. with key = nil
  358. if key-p do (setq key p)
  359. else collect `(,key . ,p)))
  360. (defun ein:plist-exclude (plist keys)
  361. "Exclude entries specified by KEYS in PLIST.
  362. Example::
  363. (ein:plist-exclude '(:a 1 :b 2 :c 3 :d 4) '(:b :c))"
  364. (cl-loop for (k v) on plist by 'cddr
  365. unless (memq k keys)
  366. nconc (list k v)))
  367. (defun ein:clip-list (list first last)
  368. "Return elements in region of the LIST specified by FIRST and LAST element.
  369. Example::
  370. (ein:clip-list '(1 2 3 4 5 6) 2 4) ;=> (2 3 4)"
  371. (cl-loop for elem in list
  372. with clipped
  373. with in-region-p = nil
  374. when (eq elem first)
  375. do (setq in-region-p t)
  376. when in-region-p
  377. do (push elem clipped)
  378. when (eq elem last)
  379. return (reverse clipped)))
  380. (cl-defun ein:list-insert-after (list pivot new &key (test #'eq))
  381. "Insert NEW after PIVOT in LIST destructively.
  382. Note: do not rely on that `ein:list-insert-after' change LIST in place.
  383. Elements are compared using the function TEST (default: `eq')."
  384. (cl-loop for rest on list
  385. when (funcall test (car rest) pivot)
  386. return (progn (push new (cdr rest)) list)
  387. finally do (error "PIVOT %S is not in LIST %S" pivot list)))
  388. (cl-defun ein:list-insert-before (list pivot new &key (test #'eq))
  389. "Insert NEW before PIVOT in LIST destructively.
  390. Note: do not rely on that `ein:list-insert-before' change LIST in place.
  391. Elements are compared using the function TEST (default: `eq')."
  392. (if (and list (funcall test (car list) pivot))
  393. (cons new list)
  394. (cl-loop for rest on list
  395. when (funcall test (cadr rest) pivot)
  396. return (progn (push new (cdr rest)) list)
  397. finally do (error "PIVOT %S is not in LIST %S" pivot list))))
  398. (cl-defun ein:list-move-left (list elem &key (test #'eq))
  399. "Move ELEM in LIST left. TEST is used to compare elements"
  400. (cl-macrolet ((== (a b) `(funcall test ,a ,b)))
  401. (cond
  402. ((== (car list) elem)
  403. (append (cdr list) (list (car list))))
  404. (t
  405. (cl-loop for rest on list
  406. when (== (cadr rest) elem)
  407. return (let ((prev (car rest)))
  408. (setf (car rest) elem)
  409. (setf (cadr rest) prev)
  410. list)
  411. finally do (error "ELEM %S is not in LIST %S" elem list))))))
  412. (cl-defun ein:list-move-right (list elem &key (test #'eq))
  413. "Move ELEM in LIST right. TEST is used to compare elements"
  414. (cl-loop with first = t
  415. for rest on list
  416. when (funcall test (car rest) elem)
  417. return (if (cdr rest)
  418. (let ((next (cadr rest)))
  419. (setf (car rest) next)
  420. (setf (cadr rest) elem)
  421. list)
  422. (if first
  423. list
  424. (setcdr rest-1 nil)
  425. (cons elem list)))
  426. finally do (error "ELEM %S is not in LIST %S" elem list)
  427. for rest-1 = rest
  428. do (setq first nil)))
  429. (defun ein:get-value (obj)
  430. "Get value from obj if it is a variable or function."
  431. (cond
  432. ((not (symbolp obj)) obj)
  433. ((boundp obj) (symbol-value obj))
  434. ((fboundp obj) (funcall obj))))
  435. (defun ein:choose-setting (symbol value &optional single-p)
  436. "Choose setting in stored in SYMBOL based on VALUE.
  437. The value of SYMBOL can be string, alist or function.
  438. SINGLE-P is a function which takes one argument. It must
  439. return t when the value of SYMBOL can be used as a setting.
  440. SINGLE-P is `stringp' by default."
  441. (let ((setting (symbol-value symbol)))
  442. (cond
  443. ((funcall (or single-p 'stringp) setting) setting)
  444. ((functionp setting) (funcall setting value))
  445. ((listp setting)
  446. (ein:get-value (or (assoc-default value setting)
  447. (assoc-default 'default setting))))
  448. (t (error "Unsupported type of `%s': %s" symbol (type-of setting))))))
  449. (defmacro ein:setf-default (place val)
  450. "Set VAL to PLACE using `setf' if the value of PLACE is `nil'."
  451. `(unless ,place
  452. (setf ,place ,val)))
  453. (defun ein:funcall-packed (func-arg &rest args)
  454. "Call \"packed\" function.
  455. FUNC-ARG is a `cons' of the form: (FUNC ARG).
  456. FUNC is called as (apply FUNC ARG ARGS)."
  457. (apply (car func-arg) (cdr func-arg) args))
  458. (defun ein:eval-if-bound (symbol)
  459. (and (boundp symbol) (symbol-value symbol)))
  460. (defun ein:remove-by-index (list indices)
  461. "Remove elements from LIST if its index is in INDICES.
  462. NOTE: This function creates new list."
  463. (cl-loop for l in list
  464. for i from 0
  465. when (not (memq i indices))
  466. collect l))
  467. (defun ein:ask-choice-char (prompt choices)
  468. "Show PROMPT and read one of acceptable key specified as CHOICES."
  469. (let ((char-list (cl-loop for i from 0 below (length choices)
  470. collect (elt choices i)))
  471. (answer 'recenter))
  472. (while
  473. (let ((key
  474. (let ((cursor-in-echo-area t))
  475. (read-key (propertize (if (eq answer 'recenter)
  476. prompt
  477. (concat "Please choose answer from"
  478. (format " %s. " choices)
  479. prompt))
  480. 'face 'minibuffer-prompt)))))
  481. (setq answer (lookup-key query-replace-map (vector key) t))
  482. (cond
  483. ((memq key char-list) (setq answer key) nil)
  484. ((eq answer 'recenter) (recenter) t)
  485. ((memq answer '(exit-prefix quit)) (signal 'quit nil) t)
  486. (t t)))
  487. (ding)
  488. (discard-input))
  489. answer))
  490. (defun ein:truncate-lines-on ()
  491. "Set `truncate-lines' on (set it to `t')."
  492. (setq truncate-lines t))
  493. (defun ein:wait-until (predicate &optional predargs timeout-seconds)
  494. "Wait until PREDICATE function returns non-`nil'.
  495. PREDARGS is argument list for the PREDICATE function.
  496. Make TIMEOUT-SECONDS larger \(default 5) to wait longer before timeout."
  497. (ein:log 'debug "WAIT-UNTIL start")
  498. (unless timeout-seconds (setq timeout-seconds 5))
  499. (unless (cl-loop repeat (/ timeout-seconds 0.05)
  500. when (apply predicate predargs)
  501. return t
  502. ;; borrowed from `deferred:sync!':
  503. do (sit-for 0.05)
  504. do (sleep-for 0.05))
  505. (warn "Timeout"))
  506. (ein:log 'debug "WAIT-UNTIL end"))
  507. (defun ein:format-time-string (format time)
  508. "Apply format to time.
  509. If `format' is a string, call `format-time-string',
  510. otherwise it should be a function, which is called on `time'."
  511. (cl-etypecase format
  512. (string (format-time-string format time))
  513. (function (funcall format time))))
  514. ;;; Emacs utilities
  515. (defmacro ein:message-whir (mesg &rest body)
  516. "Display MESG with a modest animation until ASYNC-CALL completes."
  517. `(let* (done-p
  518. (done-callback (lambda (&rest _ignore) (setf done-p t)))
  519. (errback (lambda (&rest _ignore) (setf done-p 'error))))
  520. (ignore done-callback)
  521. (ignore errback)
  522. (ein:message-whir-subr ,mesg (lambda () done-p))
  523. ,@body))
  524. (defun ein:message-whir-subr (mesg doneback)
  525. "Display MESG with a modest animation until done-p returns t.
  526. DONEBACK returns t or 'error when calling process is done, and nil if not done."
  527. (let* ((mesg mesg)
  528. (doneback doneback)
  529. (count -1))
  530. (message "%s%s" mesg (make-string (1+ (% (cl-incf count) 3)) ?.))
  531. ;; https://github.com/kiwanami/emacs-deferred/issues/28
  532. ;; "complicated timings of macro expansion lexical-let, deferred:lambda"
  533. ;; using deferred:loop instead
  534. (deferred:$
  535. (deferred:loop (cl-loop for i from 1 below 30 by 1 collect i)
  536. (lambda ()
  537. (deferred:$
  538. (deferred:next
  539. (lambda ()
  540. (ein:aif (funcall doneback) it
  541. (message "%s%s" mesg (make-string (1+ (% (cl-incf count) 3)) ?.))
  542. (sleep-for 0 365)))))))
  543. (deferred:nextc it
  544. (lambda (status)
  545. (message "%s... %s" mesg
  546. (if (or (null status) (eq status 'error)) "failed" "done")))))))
  547. (defun ein:display-warning (message &optional level)
  548. "Simple wrapper around `display-warning'.
  549. LEVEL must be one of :emergency, :error or :warning (default).
  550. This must be used only for notifying user.
  551. Use `ein:log' for debugging and logging."
  552. ;; FIXME: Probably set BUFFER-NAME per notebook?
  553. ;; FIXME: Call `ein:log' here (but do not display in minibuffer).
  554. (display-warning 'ein message level))
  555. (defvar ein:display-warning-once--db
  556. (make-hash-table :test 'equal))
  557. (defun ein:display-warning-once (message &optional level)
  558. "Call `ein:display-warning' once for same MESSAGE and LEVEL."
  559. (let ((key (list message level)))
  560. (unless (gethash key ein:display-warning-once--db)
  561. (ein:display-warning message level)
  562. (puthash key t ein:display-warning-once--db))))
  563. (defun ein:get-docstring (function)
  564. "Return docstring of FUNCTION."
  565. ;; Borrowed from `ac-symbol-documentation'.
  566. (with-temp-buffer
  567. ;; import help-xref-following
  568. (require 'help-mode)
  569. (erase-buffer)
  570. (let ((standard-output (current-buffer))
  571. (help-xref-following t)
  572. (major-mode 'help-mode)) ; avoid error in Emacs 24
  573. (describe-function-1 function))
  574. (buffer-string)))
  575. (defun ein:generate-menu (list-name-callback)
  576. (mapcar (lambda (name-callback)
  577. (cl-destructuring-bind (name callback &rest args) name-callback
  578. `[,name ,callback :help ,(ein:get-docstring callback) ,@args]))
  579. list-name-callback))
  580. (defcustom ein:enable-gc-adjust t
  581. "When t, EIN will set the `gc-cons-threshold' to an arbitrarily large value when opening notebookes. In some cases this adjustment will improve emacs performance, particularly when loading large notebooks."
  582. :type 'boolean
  583. :group 'ein)
  584. (let ((current-gc-cons-threshold gc-cons-threshold))
  585. (defun ein:gc-prepare-operation ()
  586. (ein:log 'debug "[GC-PREPARE-OPERATION] Setting cons threshold to %s." (* current-gc-cons-threshold 10000) )
  587. (when ein:enable-gc-adjust
  588. (setq gc-cons-threshold (* current-gc-cons-threshold 10000))))
  589. (defun ein:gc-complete-operation ()
  590. (ein:log 'debug "[GC-COMPLETE-OPERATION] Reverting cons threshold to %s." current-gc-cons-threshold)
  591. (when ein:enable-gc-adjust
  592. (setq gc-cons-threshold current-gc-cons-threshold))))
  593. ;;; Git utilities
  594. (defun ein:call-process (command &optional args)
  595. "Call COMMAND with ARGS and return its stdout as string or
  596. `nil' if COMMAND fails. It also checks if COMMAND executable
  597. exists or not."
  598. (with-temp-buffer
  599. (erase-buffer)
  600. (and (executable-find command)
  601. (= (apply #'call-process command nil t nil args) 0)
  602. (buffer-string))))
  603. (defun ein:git-root-p (&optional dir)
  604. "Return `t' when DIR is root of git repository."
  605. (file-directory-p (expand-file-name ".git" (or dir default-directory))))
  606. (defun ein:git-dirty-p ()
  607. "Return `t' if the current directory is in git repository and it is dirty."
  608. (not (equal (ein:call-process
  609. "git" '("--no-pager" "status" "--porcelain"))
  610. "")))
  611. (defun ein:git-revision ()
  612. "Return abbreviated git revision if the current directory is in
  613. git repository."
  614. (ein:call-process "git" '("--no-pager" "log" "-n1" "--format=format:%h")))
  615. (defun ein:git-revision-dirty ()
  616. "Return `ein:git-revision' + \"-dirty\" suffix if the current
  617. directory is in a dirty git repository."
  618. (ein:aand (ein:git-revision)
  619. (concat it (if (ein:git-dirty-p) "-dirty" ""))))
  620. ;;; utils.js compatible
  621. (defun ein:utils-uuid ()
  622. "Return string with random (version 4) UUID.
  623. Adapted from org-mode's `org-id-uuid'."
  624. (let ((rnd (md5 (format "%s%s%s%s%s%s%s"
  625. (random t)
  626. (current-time)
  627. (user-uid)
  628. (emacs-pid)
  629. (user-full-name)
  630. user-mail-address
  631. (recent-keys)))))
  632. (format "%s-%s-4%s-%s%s-%s"
  633. (substring rnd 0 8)
  634. (substring rnd 8 12)
  635. (substring rnd 13 16)
  636. (format "%x"
  637. (logior
  638. #b10000000
  639. (logand
  640. #b10111111
  641. (string-to-number
  642. (substring rnd 16 18) 16))))
  643. (substring rnd 18 20)
  644. (substring rnd 20 32))))
  645. (provide 'ein-utils)
  646. ;;; ein-utils.el ends here