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.

491 lines
20 KiB

  1. ;;; tiny.el --- Quickly generate linear ranges in Emacs
  2. ;; Copyright (C) 2013-2015, 2017 Free Software Foundation, Inc.
  3. ;; Author: Oleh Krehel <ohwoeowho@gmail.com>
  4. ;; URL: https://github.com/abo-abo/tiny
  5. ;; Version: 0.2.1
  6. ;; Keywords: convenience
  7. ;; This file is part of GNU Emacs.
  8. ;; GNU Emacs is free software: you can redistribute it and/or modify
  9. ;; it under the terms of the GNU General Public License as published by
  10. ;; the Free Software Foundation, either version 3 of the License, or
  11. ;; (at your option) any later version.
  12. ;; GNU Emacs is distributed in the hope that it will be useful,
  13. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;; GNU General Public License for more details.
  16. ;; You should have received a copy of the GNU General Public License
  17. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>
  18. ;;; Commentary:
  19. ;;
  20. ;; To set it up, just bind e.g.:
  21. ;;
  22. ;; (global-set-key (kbd "C-;") #'tiny-expand)
  23. ;;
  24. ;; Usage:
  25. ;; This extension's main command is `tiny-expand'.
  26. ;; It's meant to quickly generate linear ranges, e.g. 5, 6, 7, 8.
  27. ;; Some elisp proficiency is an advantage, since you can transform
  28. ;; your numeric range with an elisp expression.
  29. ;;
  30. ;; There's also some emphasis on the brevity of the expression to be
  31. ;; expanded: e.g. instead of typing (+ x 2), you can do +x2.
  32. ;; You can still do the full thing, but +x2 would save you some
  33. ;; key strokes.
  34. ;;
  35. ;; You can test out the following snippets by positioning the point at
  36. ;; the end of the expression and calling `tiny-expand':
  37. ;;
  38. ;; m10
  39. ;; m5 10
  40. ;; m5,10
  41. ;; m5 10*xx
  42. ;; m5 10*xx%x
  43. ;; m5 10*xx|0x%x
  44. ;; m25+x?a%c
  45. ;; m25+x?A%c
  46. ;; m97,122(string x)
  47. ;; m97,122stringxx
  48. ;; m97,120stringxupcasex
  49. ;; m97,120stringxupcasex)x
  50. ;; m\n;; 10|%(+ x x) and %(* x x) and %s
  51. ;; m10*2+3x
  52. ;; m\n;; 10expx
  53. ;; m5\n;; 20expx%014.2f
  54. ;; m7|%(expt 2 x)
  55. ;; m, 7|0x%02x
  56. ;; m10|%0.2f
  57. ;; m1\n14|*** TODO http://emacsrocks.com/e%02d.html
  58. ;; m1\n10|convert img%s.jpg -monochrome -resize 50%% -rotate 180 img%s_mono.pdf
  59. ;; (setq foo-list '(m1 11+x96|?%c))
  60. ;; m1\n10listx+x96|convert img%s.jpg -monochrome -resize 50%% -rotate 180 img%c_mono.pdf
  61. ;; m1\n10listxnthxfoo-list|convert img%s.jpg -monochrome -resize 50%% -rotate 180 img%c_mono.pdf
  62. ;; m\n;; 16list*xxx)*xx%s:%s:%s
  63. ;; m\n8|**** TODO Learning from Data Week %(+ x 2) \nSCHEDULED: <%(date "Oct 7" (* x 7))> DEADLINE: <%(date "Oct 14" (* x 7))>
  64. ;;
  65. ;; As you might have guessed, the syntax is as follows:
  66. ;;
  67. ;; m[<range start:=0>][<separator:= >]<range end>[Lisp expr]|[format expr]
  68. ;;
  69. ;; x is the default var in the elisp expression. It will take one by one
  70. ;; the value of all numbers in the range.
  71. ;;
  72. ;; | means that elisp expr has ended and format expr has begun.
  73. ;; It can be omitted if the format expr starts with %.
  74. ;; The keys are the same as for format.
  75. ;; In addition %(sexp) forms are allowed. The sexp can depend on x.
  76. ;;
  77. ;; Note that multiple % can be used in the format expression.
  78. ;; In that case:
  79. ;; - if the Lisp expression returns a list, the members of this list
  80. ;; are used in the appropriate place.
  81. ;; - otherwise, it's just the result of the expression repeated as
  82. ;; many times as necessary.
  83. ;;
  84. ;; Alternatively, if user does not want to type in the "tiny
  85. ;; expressions", they can call the `tiny-helper' command that helps
  86. ;; construct the "tiny expression", and then expands that.
  87. ;;
  88. ;; For example, the below two are equivalent:
  89. ;;
  90. ;; - Type "m2_9+1*x2"
  91. ;; - M-x tiny-expand
  92. ;;
  93. ;; OR
  94. ;;
  95. ;; - M-x tiny-helper
  96. ;; - 9 RET 2 RET _ RET +1*x2 RET RET (user entry in the interactive prompts)
  97. ;;; Code:
  98. (eval-when-compile
  99. (require 'cl))
  100. (require 'help-fns)
  101. (require 'org)
  102. (defvar tiny-beg nil
  103. "Last matched snippet start position.")
  104. (defvar tiny-end nil
  105. "Last matched snippet end position.")
  106. ;;;###autoload
  107. (defun tiny-expand ()
  108. "Expand current snippet.
  109. It polls the expander functions one by one
  110. if they can expand the thing at point.
  111. First one to return a string succeeds.
  112. These functions are expected to set `tiny-beg' and `tiny-end'
  113. to the bounds of the snippet that they matched.
  114. At the moment, only `tiny-mapconcat' is supported.
  115. `tiny-mapconcat2' should be added to expand rectangles."
  116. (interactive)
  117. (let ((e (tiny-mapconcat)))
  118. (when e
  119. (goto-char tiny-beg)
  120. (delete-region tiny-beg tiny-end)
  121. (insert (eval e t)))))
  122. (defun tiny-setup-default ()
  123. "Setup shortcuts."
  124. (global-set-key (kbd "C-;") 'tiny-expand))
  125. (defun tiny--strip-\n (str)
  126. (replace-regexp-in-string "\\\\n" "\n" str))
  127. (defun tiny-mapconcat ()
  128. "Format output of `tiny-mapconcat-parse'.
  129. Defaults are used in place of null values."
  130. (let ((parsed (tiny-mapconcat-parse)))
  131. (when parsed
  132. (let* ((n0 (string-to-number (or (nth 0 parsed) "0")))
  133. (n1 (nth 1 parsed))
  134. (s1 (cond ((null n1)
  135. " ")
  136. ((equal n1 "m")
  137. "")
  138. (t
  139. n1)))
  140. (n2 (read (nth 2 parsed)))
  141. (expr (or (nth 3 parsed) "x"))
  142. (lexpr (read expr))
  143. (n-have (if (and (listp lexpr) (eq (car lexpr) 'list))
  144. (1- (length lexpr))
  145. 0))
  146. (expr (if (zerop n-have) `(list ,lexpr) lexpr))
  147. (n-have (if (zerop n-have) 1 n-have))
  148. (tes (tiny-extract-sexps (or (nth 4 parsed) "%s")))
  149. (fmt (car tes))
  150. (idx -1)
  151. (seq (number-sequence n0 n2 (if (>= n0 n2) -1 1))))
  152. `(mapconcat (lambda (x)
  153. (let ((lst ,expr))
  154. (format ,(tiny--strip-\n fmt)
  155. ,@(mapcar (lambda (x)
  156. (if x
  157. (read x)
  158. (if (>= (1+ idx) n-have)
  159. 'x
  160. `(nth ,(incf idx) lst))))
  161. (cdr tes)))))
  162. ',seq
  163. ,(tiny--strip-\n s1))))))
  164. (defconst tiny-format-str
  165. (let ((flags "[+ #-0]\\{0,1\\}")
  166. (width "[0-9]*")
  167. (precision "\\(?:\\.[0-9]+\\)?")
  168. (character "[sdoxXefgcS]?"))
  169. (format "\\(%s%s%s%s\\)("
  170. flags width precision character)))
  171. (defun tiny-extract-sexps (str)
  172. "Return (STR & FORMS).
  173. Each element of FORMS corresponds to a `format'-style % form in STR.
  174. * %% forms are skipped
  175. * %(sexp) is replaced with %s in STR, and put in FORMS
  176. * the rest of forms are untouched in STR, and put as nil in FORMS"
  177. (let ((start 0)
  178. forms beg fexp)
  179. (condition-case nil
  180. (while (setq beg (string-match "%" str start))
  181. (setq start (1+ beg))
  182. (cond ((= ?% (aref str (1+ beg)))
  183. (incf start))
  184. ((and (eq beg (string-match tiny-format-str str beg))
  185. (setq fexp (match-string-no-properties 1 str)))
  186. (incf beg (length fexp))
  187. (destructuring-bind (_sexp . end)
  188. (read-from-string str beg)
  189. (push
  190. (replace-regexp-in-string "(date" "(tiny-date"
  191. (substring str beg end))
  192. forms)
  193. (setq str (concat (substring str 0 beg)
  194. (if (string= fexp "%") "s" "")
  195. (substring str end)))))
  196. (t (push nil forms))))
  197. (error (message "Malformed sexp: %s" (substring str beg))))
  198. (cons str (nreverse forms))))
  199. (defun tiny-mapconcat-parse ()
  200. "Try to match a snippet of this form:
  201. m[START][SEPARATOR]END[EXPR]|[FORMAT]
  202. * START - integer (defaults to 0)
  203. * SEPARATOR - string (defaults to \" \")
  204. * END - integer (required)
  205. * EXPR - Lisp expression: function body with argument x (defaults to x)
  206. Parens are optional if it's unambiguous:
  207. - `(* 2 (+ x 3))' <-> *2+x3
  208. - `(exp x)' <-> expx
  209. A closing paren may be added to resolve ambiguity:
  210. - `(* 2 (+ x 3) x)' <-> *2+x3)
  211. * FORMAT - string, `format'-style (defaults to \"%s\")
  212. | separator can be omitted if FORMAT starts with %.
  213. Return nil if nothing was matched, otherwise
  214. (START SEPARATOR END EXPR FORMAT)"
  215. (let ((case-fold-search nil)
  216. n1 s1 n2 expr fmt str)
  217. (when (catch 'done
  218. (cond
  219. ;; either start with a number
  220. ((looking-back "\\bm\\(-?[0-9]+\\)\\(.*?\\)"
  221. (line-beginning-position))
  222. (setq n1 (match-string-no-properties 1)
  223. str (match-string-no-properties 2)
  224. tiny-beg (match-beginning 0)
  225. tiny-end (match-end 0))
  226. (when (zerop (length str))
  227. (setq n2 n1
  228. n1 nil)
  229. (throw 'done t)))
  230. ;; else capture the whole thing
  231. ((looking-back "\\bm\\([^%|\n]*[0-9][^\n]*\\)"
  232. (line-beginning-position))
  233. (setq str (match-string-no-properties 1)
  234. tiny-beg (match-beginning 0)
  235. tiny-end (match-end 0))
  236. (when (zerop (length str))
  237. (throw 'done nil)))
  238. (t (throw 'done nil)))
  239. ;; at this point, `str' should be either [sep]<num>[expr][fmt]
  240. ;; or [expr][fmt]
  241. ;;
  242. ;; First, try to match [expr][fmt]
  243. (string-match "^\\(.*?\\)|?\\(%.*\\)?$" str)
  244. (setq expr (match-string-no-properties 1 str))
  245. (setq fmt (match-string-no-properties 2 str))
  246. ;; If it's a valid expression, we're done
  247. (when (setq expr (tiny-tokenize expr))
  248. (setq n2 n1
  249. n1 nil)
  250. (throw 'done t))
  251. ;; at this point, `str' is [sep]<num>[expr][fmt]
  252. (if (string-match "^\\([^\n0-9]*?\\)\\(-?[0-9]+\\)\\(.*\\)?$" str)
  253. (setq s1 (match-string-no-properties 1 str)
  254. n2 (match-string-no-properties 2 str)
  255. str (match-string-no-properties 3 str))
  256. ;; here there's only n2 that was matched as n1
  257. (setq n2 n1
  258. n1 nil))
  259. ;; match expr_fmt
  260. (unless (zerop (length str))
  261. (if (or (string-match "^\\([^\n%|]*?\\)|\\([^\n]*\\)?$" str)
  262. (string-match "^\\([^\n%|]*?\\)\\(%[^\n]*\\)?$" str))
  263. (progn
  264. (setq expr (tiny-tokenize (match-string-no-properties 1 str)))
  265. (setq fmt (match-string-no-properties 2 str)))
  266. (error "Couldn't match %s" str)))
  267. t)
  268. (when (equal expr "")
  269. (setq expr nil))
  270. (list n1 s1 n2 expr fmt))))
  271. ;; TODO: check for arity: this doesn't work: exptxy
  272. (defun tiny-tokenize (str)
  273. "Transform shorthand Lisp expression STR to proper Lisp."
  274. (if (equal str "")
  275. ""
  276. (ignore-errors
  277. (let ((i 0)
  278. (j 1)
  279. (len (length str))
  280. sym s out allow-spc
  281. (n-paren 0)
  282. (expect-fun t))
  283. (while (< i len)
  284. (setq s (substring str i j))
  285. (when (cond
  286. ((string= s "x")
  287. (push s out)
  288. (push " " out))
  289. ((string= s " ")
  290. (if allow-spc
  291. t
  292. (error "Unexpected \" \"")))
  293. ;; special syntax to read chars
  294. ((string= s "?")
  295. (setq s (format "%s" (read (substring str i (incf j)))))
  296. (push s out)
  297. (push " " out))
  298. ((string= s ")")
  299. ;; expect a close paren only if it's necessary
  300. (if (>= n-paren 0)
  301. (decf n-paren)
  302. (error "Unexpected \")\""))
  303. (when (string= (car out) " ")
  304. (pop out))
  305. (push ")" out)
  306. (push " " out))
  307. ((string= s "(")
  308. ;; open paren is used sometimes
  309. ;; when there are numbers in the expression
  310. (setq expect-fun t)
  311. (incf n-paren)
  312. (push "(" out))
  313. ((progn (setq sym (intern-soft s))
  314. (cond
  315. ;; general functionp
  316. ((and (not (eq t (help-function-arglist sym)))
  317. (not (eq sym '\,)))
  318. (setq expect-fun nil)
  319. (setq allow-spc t)
  320. ;; (when (zerop n-paren) (push "(" out))
  321. (unless (equal (car out) "(")
  322. (push "(" out)
  323. (incf n-paren))
  324. t)
  325. ((and sym (boundp sym) (not expect-fun))
  326. t)))
  327. (push s out)
  328. (push " " out))
  329. ((numberp (read s))
  330. (let* ((num (string-to-number (substring str i)))
  331. (num-s (format "%s" num)))
  332. (push num-s out)
  333. (push " " out)
  334. (setq j (+ i (length num-s)))))
  335. (t
  336. (incf j)
  337. nil))
  338. (setq i j)
  339. (setq j (1+ i))))
  340. ;; last space
  341. (when (string= (car out) " ")
  342. (pop out))
  343. (concat
  344. (apply #'concat (nreverse out))
  345. (make-string n-paren ?\)))))))
  346. (defun tiny-date (s &optional shift)
  347. "Return date representation of S.
  348. `org-mode' format is used.
  349. Optional SHIFT argument is the integer amount of days to shift."
  350. (let* ((ct (decode-time (current-time)))
  351. (time (apply 'encode-time
  352. (org-read-date-analyze
  353. s nil
  354. ct)))
  355. (formatter
  356. (if (equal (cl-subseq ct 1 3)
  357. (cl-subseq (decode-time time) 1 3))
  358. "%Y-%m-%d %a"
  359. "%Y-%m-%d %a %H:%M")))
  360. (when shift
  361. (setq time (time-add time (days-to-time shift))))
  362. (format-time-string formatter time)))
  363. ;;;###autoload
  364. (defun tiny-helper (&optional end-val begin-val sep op fmt)
  365. "Helper function for `tiny-expand'.
  366. The arguments to this function construct a tiny expression
  367. \"mBSEO|F\" where
  368. E is the end value (END-VAL) - defaults to 0 internally if nil or \"\",
  369. or 9 if BEGIN-VAL is nil or \"\" too.
  370. B is the begin value (BEGIN-VAL) - defaults to 0 internally if nil or \"\".
  371. S is the separator (SEP) - defaults to \" \" if nil or \"\".
  372. O is the elisp operation (OP) - defaults to \"\" if nil.
  373. F is the format (FMT) - defaults to \"\" if nil.
  374. If `tiny' expansion is possible at point, do it.
  375. Otherwise activate the helper to generate a valid tiny
  376. expression and expand that.
  377. Usage: Call TINY-HELPER, -> 0 1 2 3 4 5 6 7 8 9
  378. Call TINY-HELPER, 92_+1*x2 -> 5_7_9_11_13_15_17_19
  379. Call TINY-HELPER, 151-30*2x%x -> 1c 1a 18 16 14 12 10 e c a 8 6 4 2 0"
  380. (interactive
  381. (unless (or
  382. ;; Use the helper only if tiny expansion is not
  383. ;; possible at point and if the buffer is editable.d
  384. (barf-if-buffer-read-only)
  385. (tiny-mapconcat))
  386. (let ((prompt (propertize "tiny-helper: " 'face 'font-lock-function-name-face)))
  387. (list (read-string (concat prompt
  388. "END value "
  389. "[Hit RET for default=0; "
  390. "Auto-set to 9 if both begin and end values are 0]: "))
  391. (read-string (concat prompt
  392. "BEGIN value "
  393. "[Hit RET for default=0; "
  394. "Has to be *smaller* than the end value]: "))
  395. (read-string (concat prompt
  396. "Separator "
  397. "[Hit RET for default=Space; "
  398. "eg: \\n; No math operators like - or = allowed]: "))
  399. (read-string (concat prompt
  400. "Lisp Operation "
  401. "[Hit RET for default=\"\" (no Lisp operation); "
  402. "Parentheses are optional; eg: *xx | (+ x ?A) | *2+3x]: "))
  403. (read-string (concat prompt
  404. "Format "
  405. "[Hit RET for default=\"\" (%0d); "
  406. "eg: %x | 0x%x | %c | %s | %(+ x x) | "
  407. "%014.2f | %03d; Parentheses required here for sexps]: "))))))
  408. (barf-if-buffer-read-only) ;Proceed only if the buffer is editable.
  409. ;; Use the helper to derive a "tiny expression" if tiny expansion is
  410. ;; not possible at point.
  411. (when (null (tiny-mapconcat))
  412. (let* ((tiny-key-binding (or (substitute-command-keys "\\[tiny-helper]")
  413. (substitute-command-keys "\\[tiny-expand]")))
  414. (end-val (if (null end-val) "" end-val)) ;Initialize to empty strings for non-interactive use.
  415. (begin-val (if (null begin-val) "" begin-val))
  416. (sep (if (null sep) "" sep))
  417. (op (if (null op) "" op))
  418. (fmt (if (null fmt) "" fmt))
  419. (end-val-num (string-to-number end-val)) ;Note that (string-to-number "") -> 0
  420. (begin-val-num (string-to-number begin-val))
  421. tiny-expr)
  422. ;; BEGIN-VAL and END-VAL sanity check.
  423. (cond
  424. ((= end-val-num begin-val-num)
  425. (if (zerop end-val-num)
  426. ;; If both are zero, set the end value to 9 (arbitrarily chosen).
  427. (setq end-val "9")
  428. (user-error (format "Begin value (%s) and End value (%s) cannot be the same"
  429. begin-val end-val))))
  430. ((< end-val-num begin-val-num)
  431. (user-error (format "End value (%s) has to be greater than the begin value (%s)"
  432. begin-val end-val))))
  433. ;; SEP cannot be an empty string if BEGIN-VAL is a non-empty string.
  434. ;; It is OK to not specify BEGIN-VAL if it is 0.
  435. (when (and (not (string= begin-val ""))
  436. (string= sep ""))
  437. (setq sep " "))
  438. ;; When non-empty, prefix FMT with the | char for reading clarity.
  439. (when (not (string= fmt ""))
  440. (setq fmt (concat "|" fmt)))
  441. (setq tiny-expr (concat "m" begin-val sep end-val op fmt))
  442. (message (format "This %s expansion can also be done by typing %s and then %s"
  443. (propertize "tiny"
  444. 'face 'font-lock-function-name-face)
  445. (propertize tiny-expr
  446. 'face 'font-lock-keyword-face)
  447. (if (stringp tiny-key-binding)
  448. (propertize tiny-key-binding
  449. 'face 'font-lock-keyword-face)
  450. (concat
  451. (propertize "M-x tiny-helper"
  452. 'face 'font-lock-keyword-face)
  453. " or "
  454. (propertize "M-x tiny-expand"
  455. 'face 'font-lock-keyword-face)))))
  456. (insert tiny-expr)
  457. (undo-boundary)))
  458. (tiny-expand))
  459. (provide 'tiny)
  460. ;;; tiny.el ends here