ein-utils.el 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. ;;; ein-utils.el --- Utility module
  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. (eval-when-compile (require 'cl))
  19. (require 'cc-mode)
  20. (require 'json)
  21. ;;; Macros and core functions/variables
  22. (defmacro ein:aif (test-form then-form &rest else-forms)
  23. "Anaphoric IF. Adapted from `e2wm:aif'."
  24. (declare (debug (form form &rest form)))
  25. `(let ((it ,test-form))
  26. (if it ,then-form ,@else-forms)))
  27. (put 'ein:aif 'lisp-indent-function 2)
  28. (defmacro ein:aand (test &rest rest)
  29. "Anaphoric AND. Adapted from `e2wm:aand'."
  30. (declare (debug (form &rest form)))
  31. `(let ((it ,test))
  32. (if it ,(if rest (macroexpand-all `(ein:aand ,@rest)) 'it))))
  33. (defmacro ein:and-let* (bindings &rest form)
  34. "Gauche's `and-let*'."
  35. (declare (debug ((&rest &or symbolp (form) (gate symbolp &optional form))
  36. body))
  37. ;; See: (info "(elisp) Specification List")
  38. (indent 1))
  39. (if (null bindings)
  40. `(progn ,@form)
  41. (let* ((head (car bindings))
  42. (tail (cdr bindings))
  43. (rest (macroexpand-all `(ein:and-let* ,tail ,@form))))
  44. (cond
  45. ((symbolp head) `(if ,head ,rest))
  46. ((= (length head) 1) `(if ,(car head) ,rest))
  47. (t `(let (,head) (if ,(car head) ,rest)))))))
  48. (defmacro ein:deflocal (name &optional initvalue docstring)
  49. "Define permanent buffer local variable named NAME.
  50. INITVALUE and DOCSTRING are passed to `defvar'."
  51. (declare (indent defun)
  52. (doc-string 3))
  53. `(progn
  54. (defvar ,name ,initvalue ,docstring)
  55. (make-variable-buffer-local ',name)
  56. (put ',name 'permanent-local t)))
  57. (defmacro ein:with-read-only-buffer (buffer &rest body)
  58. (declare (indent 1))
  59. `(with-current-buffer ,buffer
  60. (setq buffer-read-only t)
  61. (save-excursion
  62. (let ((inhibit-read-only t))
  63. ,@body))))
  64. (defmacro ein:with-live-buffer (buffer &rest body)
  65. "Execute BODY in BUFFER if BUFFER is alive."
  66. (declare (indent 1) (debug t))
  67. `(when (buffer-live-p ,buffer)
  68. (with-current-buffer ,buffer
  69. ,@body)))
  70. (defmacro ein:with-possibly-killed-buffer (buffer &rest body)
  71. "Execute BODY in BUFFER if BUFFER is live.
  72. Execute BODY if BUFFER is not live anyway."
  73. (declare (indent 1) (debug t))
  74. `(if (buffer-live-p ,buffer)
  75. (with-current-buffer ,buffer
  76. ,@body)
  77. ,@body))
  78. (defvar ein:dotty-syntax-table
  79. (let ((table (make-syntax-table c-mode-syntax-table)))
  80. (modify-syntax-entry ?. "w" table)
  81. (modify-syntax-entry ?_ "w" table)
  82. table)
  83. "Adapted from `python-dotty-syntax-table'.")
  84. (defun ein:object-at-point ()
  85. "Return dotty.words.at.point.
  86. When region is active, text in region is returned after trimmed
  87. white spaces, newlines and dots.
  88. When object is not found at the point, return the object just
  89. before previous opening parenthesis."
  90. ;; For auto popup tooltip (or something like eldoc), probably it is
  91. ;; better to return function (any word before "("). I should write
  92. ;; another function or add option to this function when the auto
  93. ;; popup tooltip is implemented.
  94. (if (region-active-p)
  95. (ein:trim (buffer-substring (region-beginning) (region-end))
  96. "\\s-\\|\n\\|\\.")
  97. (save-excursion
  98. (with-syntax-table ein:dotty-syntax-table
  99. (ein:aif (thing-at-point 'word)
  100. it
  101. (unless (looking-at "(")
  102. (search-backward "(" (point-at-bol) t))
  103. (thing-at-point 'word))))))
  104. (defun ein:object-at-point-or-error ()
  105. (or (ein:object-at-point) (error "No object found at the point")))
  106. ;;; URL utils
  107. (defvar ein:url-localhost "127.0.0.1")
  108. (defvar ein:url-localhost-template "http://127.0.0.1:%s")
  109. (defun ein:url (url-or-port &rest paths)
  110. (loop with url = (if (integerp url-or-port)
  111. (format ein:url-localhost-template url-or-port)
  112. url-or-port)
  113. for p in paths
  114. do (setq url (concat (ein:trim-right url "/")
  115. "/"
  116. (ein:trim-left p "/")))
  117. finally return url))
  118. (defun ein:url-no-cache (url)
  119. "Imitate `cache=false' of `jQuery.ajax'.
  120. See: http://api.jquery.com/jQuery.ajax/"
  121. (concat url (format-time-string "?_=%s")))
  122. ;;; HTML utils
  123. (defun ein:html-get-data-in-body-tag (key)
  124. "Very ad-hoc parser to get data in body tag."
  125. (ignore-errors
  126. (save-excursion
  127. (goto-char (point-min))
  128. (search-forward "<body")
  129. (search-forward-regexp (format "%s=\\([^[:space:]\n]+\\)" key))
  130. (match-string 1))))
  131. ;;; JSON utils
  132. (defmacro ein:with-json-setting (&rest body)
  133. `(let ((json-object-type 'plist)
  134. (json-array-type 'list))
  135. ,@body))
  136. (defun ein:json-read ()
  137. "Read json from `url-retrieve'-ed buffer.
  138. * `json-object-type' is `plist'. This is mainly for readability.
  139. * `json-array-type' is `list'. Notebook data is edited locally thus
  140. data type must be edit-friendly. `vector' type is not."
  141. (goto-char (point-max))
  142. (backward-sexp)
  143. (ein:with-json-setting
  144. (json-read)))
  145. (defun ein:json-read-from-string (string)
  146. (ein:with-json-setting
  147. (json-read-from-string string)))
  148. (defun ein:json-any-to-bool (obj)
  149. (if (and obj (not (eq obj json-false))) t json-false))
  150. (defun ein:json-encode-char (char)
  151. "Fixed `json-encode-char'."
  152. (setq char (json-encode-char0 char 'ucs))
  153. (let ((control-char (car (rassoc char json-special-chars))))
  154. (cond
  155. ;; Special JSON character (\n, \r, etc.).
  156. (control-char
  157. (format "\\%c" control-char))
  158. ;; ASCIIish printable character.
  159. ((and (> char 31) (< char 127)) ; s/161/127/
  160. (format "%c" char))
  161. ;; Fallback: UCS code point in \uNNNN form.
  162. (t
  163. (format "\\u%04x" char)))))
  164. (defadvice json-encode-char (around ein:json-encode-char (char) activate)
  165. "Replace `json-encode-char' with `ein:json-encode-char'."
  166. (setq ad-return-value (ein:json-encode-char char)))
  167. ;;; EWOC
  168. (defun ein:ewoc-create (pretty-printer &optional header footer nosep)
  169. "Do nothing wrapper of `ewoc-create' to provide better error message."
  170. (condition-case nil
  171. (ewoc-create pretty-printer header footer nosep)
  172. ((debug wrong-number-of-arguments)
  173. (ein:display-warning "Incompatible EOWC version.
  174. The version of ewoc.el you are using is too old for EIN.
  175. Please install the newer version.
  176. See also: https://github.com/tkf/emacs-ipython-notebook/issues/49")
  177. (error "Incompatible EOWC version."))))
  178. ;;; Text property
  179. (defun ein:propertize-read-only (string &rest properties)
  180. (apply #'propertize string 'read-only t 'front-sticky t properties))
  181. (defun ein:insert-read-only (string &rest properties)
  182. (insert (apply #'ein:propertize-read-only string properties)))
  183. ;;; String manipulation
  184. (defun ein:trim (string &optional regexp)
  185. (ein:trim-left (ein:trim-right string regexp) regexp))
  186. (defun ein:trim-left (string &optional regexp)
  187. (unless regexp (setq regexp "\\s-\\|\n"))
  188. (ein:trim-regexp string (format "^\\(%s\\)+" regexp)))
  189. (defun ein:trim-right (string &optional regexp)
  190. (unless regexp (setq regexp "\\s-\\|\n"))
  191. (ein:trim-regexp string (format "\\(%s\\)+$" regexp)))
  192. (defun ein:trim-regexp (string regexp)
  193. (if (string-match regexp string)
  194. (replace-match "" t t string)
  195. string))
  196. (defun ein:trim-indent (string)
  197. "Strip uniform amount of indentation from lines in STRING."
  198. (let* ((lines (split-string string "\n"))
  199. (indent
  200. (let ((lens
  201. (loop for line in lines
  202. for stripped = (ein:trim-left line)
  203. unless (equal stripped "")
  204. collect (- (length line) (length stripped)))))
  205. (if lens (apply #'ein:min lens) 0)))
  206. (trimmed
  207. (loop for line in lines
  208. if (> (length line) indent)
  209. collect (ein:trim-right (substring line indent))
  210. else
  211. collect line)))
  212. (ein:join-str "\n" trimmed)))
  213. (defun ein:join-str (sep strings)
  214. (mapconcat 'identity strings sep))
  215. (defun ein:join-path (paths)
  216. (mapconcat 'file-name-as-directory paths ""))
  217. (defun ein:string-fill-paragraph (string &optional justify)
  218. (with-temp-buffer
  219. (erase-buffer)
  220. (insert string)
  221. (goto-char (point-min))
  222. (fill-paragraph justify)
  223. (buffer-string)))
  224. (defmacro ein:case-equal (str &rest clauses)
  225. "Similar to `case' but comparison is done by `equal'.
  226. Adapted from twittering-mode.el's `case-string'."
  227. (declare (indent 1))
  228. `(cond
  229. ,@(mapcar
  230. (lambda (clause)
  231. (let ((keylist (car clause))
  232. (body (cdr clause)))
  233. `(,(if (listp keylist)
  234. `(or ,@(mapcar (lambda (key) `(equal ,str ,key))
  235. keylist))
  236. 't)
  237. ,@body)))
  238. clauses)))
  239. ;;; Text manipulation on buffer
  240. (defun ein:find-leftmot-column (beg end)
  241. "Return the leftmost column in region BEG to END."
  242. (save-excursion
  243. (let (mincol)
  244. (goto-char beg)
  245. (while (< (point) end)
  246. (back-to-indentation)
  247. (unless (= (point) (point-at-eol))
  248. (setq mincol (if mincol
  249. (min mincol (current-column))
  250. (current-column))))
  251. (unless (= (forward-line 1) 0)
  252. (return-from ein:find-leftmot-column mincol)))
  253. mincol)))
  254. ;;; Misc
  255. (defun ein:plist-iter (plist)
  256. "Return list of (key . value) in PLIST."
  257. ;; FIXME: this is not needed. See: `ein:plist-exclude'.
  258. (loop for p in plist
  259. for i from 0
  260. for key-p = (= (% i 2) 0)
  261. with key = nil
  262. if key-p do (setq key p)
  263. else collect `(,key . ,p)))
  264. (defun ein:plist-exclude (plist keys)
  265. "Exclude entries specified by KEYS in PLIST.
  266. Example::
  267. (ein:plist-exclude '(:a 1 :b 2 :c 3 :d 4) '(:b :c))"
  268. (loop for (k v) on plist by 'cddr
  269. unless (memq k keys)
  270. nconc (list k v)))
  271. (defun ein:hash-keys (table)
  272. (let (keys)
  273. (maphash (lambda (k v) (push k keys)) table)
  274. keys))
  275. (defun ein:hash-vals (table)
  276. (let (vals)
  277. (maphash (lambda (k v) (push v vals)) table)
  278. vals))
  279. (defun ein:filter (predicate sequence)
  280. (loop for item in sequence
  281. when (funcall predicate item)
  282. collect item))
  283. (defun ein:clip-list (list first last)
  284. "Return elements in region of the LIST specified by FIRST and LAST element.
  285. Example::
  286. (ein:clip-list '(1 2 3 4 5 6) 2 4) ;=> (2 3 4)"
  287. (loop for elem in list
  288. with clipped
  289. with in-region-p = nil
  290. when (eq elem first)
  291. do (setq in-region-p t)
  292. when in-region-p
  293. do (push elem clipped)
  294. when (eq elem last)
  295. return (reverse clipped)))
  296. (defun* ein:list-insert-after (list pivot new &key (test #'eq))
  297. "Insert NEW after PIVOT in LIST destructively.
  298. Note: do not rely on that `ein:list-insert-after' change LIST in place.
  299. Elements are compared using the function TEST (default: `eq')."
  300. (loop for rest on list
  301. when (funcall test (car rest) pivot)
  302. return (progn (push new (cdr rest)) list)
  303. finally do (error "PIVOT %S is not in LIST %S" pivot list)))
  304. (defun* ein:list-insert-before (list pivot new &key (test #'eq))
  305. "Insert NEW before PIVOT in LIST destructively.
  306. Note: do not rely on that `ein:list-insert-before' change LIST in place.
  307. Elements are compared using the function TEST (default: `eq')."
  308. (if (and list (funcall test (car list) pivot))
  309. (cons new list)
  310. (loop for rest on list
  311. when (funcall test (cadr rest) pivot)
  312. return (progn (push new (cdr rest)) list)
  313. finally do (error "PIVOT %S is not in LIST %S" pivot list))))
  314. (defun* ein:list-move-left (list elem &key (test #'eq))
  315. "Move ELEM in LIST left. TEST is used to compare elements"
  316. (macrolet ((== (a b) `(funcall test ,a ,b)))
  317. (cond
  318. ((== (car list) elem)
  319. (append (cdr list) (list (car list))))
  320. (t
  321. (loop for rest on list
  322. when (== (cadr rest) elem)
  323. return (let ((prev (car rest)))
  324. (setf (car rest) elem)
  325. (setf (cadr rest) prev)
  326. list)
  327. finally do (error "ELEM %S is not in LIST %S" elem list))))))
  328. (defun* ein:list-move-right (list elem &key (test #'eq))
  329. "Move ELEM in LIST right. TEST is used to compare elements"
  330. (loop with first = t
  331. for rest on list
  332. when (funcall test (car rest) elem)
  333. return (if (cdr rest)
  334. (let ((next (cadr rest)))
  335. (setf (car rest) next)
  336. (setf (cadr rest) elem)
  337. list)
  338. (if first
  339. list
  340. (setcdr rest-1 nil)
  341. (cons elem list)))
  342. finally do (error "ELEM %S is not in LIST %S" elem list)
  343. for rest-1 = rest
  344. do (setq first nil)))
  345. (defun ein:get-value (obj)
  346. "Get value from obj if it is a variable or function."
  347. (cond
  348. ((not (symbolp obj)) obj)
  349. ((boundp obj) (eval obj))
  350. ((fboundp obj) (funcall obj))))
  351. (defun ein:choose-setting (symbol value &optional single-p)
  352. "Choose setting in stored in SYMBOL based on VALUE.
  353. The value of SYMBOL can be string, alist or function.
  354. SINGLE-P is a function which takes one argument. It must
  355. return t when the value of SYMBOL can be used as a setting.
  356. SINGLE-P is `stringp' by default."
  357. (let ((setting (eval symbol)))
  358. (cond
  359. ((funcall (or single-p 'stringp) setting) setting)
  360. ((functionp setting) (funcall setting value))
  361. ((listp setting)
  362. (ein:get-value (or (assoc-default value setting)
  363. (assoc-default 'default setting))))
  364. (t (error "Unsupported type of `%s': %s" symbol (type-of setting))))))
  365. (defmacro ein:setf-default (place val)
  366. "Set VAL to PLACE using `setf' if the value of PLACE is `nil'."
  367. `(unless ,place
  368. (setf ,place ,val)))
  369. (defun ein:funcall-packed (func-arg &rest args)
  370. "Call \"packed\" function.
  371. FUNC-ARG is a `cons' of the form: (FUNC ARG).
  372. FUNC is called as (apply FUNC ARG ARGS)."
  373. (apply (car func-arg) (cdr func-arg) args))
  374. (defun ein:eval-if-bound (symbol)
  375. (if (boundp symbol) (eval symbol)))
  376. (defun ein:remove-by-index (list indices)
  377. "Remove elements from LIST if its index is in INDICES.
  378. NOTE: This function creates new list."
  379. (loop for l in list
  380. for i from 0
  381. when (not (memq i indices))
  382. collect l))
  383. (defun ein:min (x &rest xs)
  384. (loop for y in xs if (< y x) do (setq x y))
  385. x)
  386. (defun ein:do-nothing (&rest -ignore-)
  387. "A function which can take any number of variables and do nothing.")
  388. (defun ein:ask-choice-char (prompt choices)
  389. "Show PROMPT and read one of acceptable key specified as CHOICES."
  390. (let ((char-list (loop for i from 0 below (length choices)
  391. collect (elt choices i)))
  392. (answer 'recenter))
  393. (while
  394. (let ((key
  395. (let ((cursor-in-echo-area t))
  396. (read-key (propertize (if (eq answer 'recenter)
  397. prompt
  398. (concat "Please choose answer from"
  399. (format " %s. " choices)
  400. prompt))
  401. 'face 'minibuffer-prompt)))))
  402. (setq answer (lookup-key query-replace-map (vector key) t))
  403. (cond
  404. ((memq key char-list) (setq answer key) nil)
  405. ((eq answer 'recenter) (recenter) t)
  406. ((memq answer '(exit-prefix quit)) (signal 'quit nil) t)
  407. (t t)))
  408. (ding)
  409. (discard-input))
  410. answer))
  411. (defun ein:truncate-lines-on ()
  412. "Set `truncate-lines' on (set it to `t')."
  413. (setq truncate-lines t))
  414. ;;; Emacs utilities
  415. (defun ein:display-warning (message &optional level)
  416. "Simple wrapper around `display-warning'.
  417. LEVEL must be one of :emergency, :error or :warning (default).
  418. This must be used only for notifying user.
  419. Use `ein:log' for debugging and logging."
  420. ;; FIXME: Probably set BUFFER-NAME per notebook?
  421. ;; FIXME: Call `ein:log' here (but do not display in minibuffer).
  422. (display-warning 'ein message level))
  423. (defvar ein:display-warning-once--db
  424. (make-hash-table :test 'equal))
  425. (defun ein:display-warning-once (message &optional level)
  426. "Call `ein:display-warning' once for same MESSAGE and LEVEL."
  427. (let ((key (list message level)))
  428. (unless (gethash key ein:display-warning-once--db)
  429. (ein:display-warning message level)
  430. (puthash key t ein:display-warning-once--db))))
  431. (defun ein:get-docstring (function)
  432. "Return docstring of FUNCTION."
  433. ;; Borrowed from `ac-symbol-documentation'.
  434. (with-temp-buffer
  435. ;; import help-xref-following
  436. (require 'help-mode)
  437. (erase-buffer)
  438. (let ((standard-output (current-buffer))
  439. (help-xref-following t)
  440. (major-mode 'help-mode)) ; avoid error in Emacs 24
  441. (describe-function-1 function))
  442. (buffer-string)))
  443. (defun ein:generate-menu (list-name-callback)
  444. (mapcar (lambda (name-callback)
  445. (destructuring-bind (name callback &rest args) name-callback
  446. `[,name ,callback :help ,(ein:get-docstring callback) ,@args]))
  447. list-name-callback))
  448. ;;; Git utilities
  449. (defun ein:call-process (command &optional args)
  450. "Call COMMAND with ARGS and return its stdout as string or
  451. `nil' if COMMAND fails. It also checks if COMMAND executable
  452. exists or not."
  453. (with-temp-buffer
  454. (erase-buffer)
  455. (and (executable-find command)
  456. (= (apply #'call-process command nil t nil args) 0)
  457. (buffer-string))))
  458. (defun ein:git-root-p (&optional dir)
  459. "Return `t' when DIR is root of git repository."
  460. (file-directory-p (expand-file-name ".git" (or dir default-directory))))
  461. (defun ein:git-dirty-p ()
  462. "Return `t' if the current directory is in git repository and it is dirty."
  463. (not (equal (ein:call-process
  464. "git" '("--no-pager" "status" "--porcelain"))
  465. "")))
  466. (defun ein:git-revision ()
  467. "Return abbreviated git revision if the current directory is in
  468. git repository."
  469. (ein:call-process "git" '("--no-pager" "log" "-n1" "--format=format:%h")))
  470. (defun ein:git-revision-dirty ()
  471. "Return `ein:git-revision' + \"-dirty\" suffix if the current
  472. directory is in a dirty git repository."
  473. (ein:aand (ein:git-revision)
  474. (concat it (if (ein:git-dirty-p) "-dirty" ""))))
  475. ;;; utils.js compatible
  476. (defun ein:utils-uuid ()
  477. "Return string with random (version 4) UUID.
  478. Adapted from org-mode's `org-id-uuid'."
  479. (let ((rnd (md5 (format "%s%s%s%s%s%s%s"
  480. (random t)
  481. (current-time)
  482. (user-uid)
  483. (emacs-pid)
  484. (user-full-name)
  485. user-mail-address
  486. (recent-keys)))))
  487. (format "%s-%s-4%s-%s%s-%s"
  488. (substring rnd 0 8)
  489. (substring rnd 8 12)
  490. (substring rnd 13 16)
  491. (format "%x"
  492. (logior
  493. #b10000000
  494. (logand
  495. #b10111111
  496. (string-to-number
  497. (substring rnd 16 18) 16))))
  498. (substring rnd 18 20)
  499. (substring rnd 20 32))))
  500. (provide 'ein-utils)
  501. ;;; ein-utils.el ends here