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