-
Notifications
You must be signed in to change notification settings - Fork 57
/
skewer-mode.el
620 lines (524 loc) · 22.6 KB
/
skewer-mode.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
;;; skewer-mode.el --- live browser JavaScript, CSS, and HTML interaction -*- lexical-binding: t; -*-
;; This is free and unencumbered software released into the public domain.
;; Author: Christopher Wellons <[email protected]>
;; URL: https://github.com/skeeto/skewer-mode
;;; Commentary:
;; Quick start (without package.el):
;; 1. Put this directory in your `load-path'
;; 2. Load skewer-mode.el
;; 3. M-x `run-skewer' to attach a browser to Emacs
;; 4. From a `js2-mode' buffer with `skewer-mode' minor mode enabled,
;; send forms to the browser to evaluate
;; The function `skewer-setup' can be used to configure all of mode
;; hooks (previously this was the default). This can also be done
;; manually like so,
;; (add-hook 'js2-mode-hook 'skewer-mode)
;; (add-hook 'css-mode-hook 'skewer-css-mode)
;; (add-hook 'html-mode-hook 'skewer-html-mode)
;; The keybindings for evaluating expressions in the browser are just
;; like the Lisp modes. These are provided by the minor mode
;; `skewer-mode'.
;; * C-x C-e -- `skewer-eval-last-expression'
;; * C-M-x -- `skewer-eval-defun'
;; * C-c C-k -- `skewer-load-buffer'
;; The result of the expression is echoed in the minibuffer.
;; Additionally, `css-mode' and `html-mode' get a similar set of
;; bindings for modifying the CSS rules and updating HTML on the
;; current page.
;; Note: `run-skewer' uses `browse-url' to launch the browser. This
;; may require further setup depending on your operating system and
;; personal preferences.
;; Multiple browsers and browser tabs can be attached to Emacs at
;; once. JavaScript forms are sent to all attached clients
;; simultaneously, and each will echo back the result
;; individually. Use `list-skewer-clients' to see a list of all
;; currently attached clients.
;; Sometimes Skewer's long polls from the browser will timeout after a
;; number of hours of inactivity. If you find the browser disconnected
;; from Emacs for any reason, use the browser's console to call
;; skewer() to reconnect. This avoids a page reload, which would lose
;; any fragile browser state you might care about.
;; To skewer your own document rather than the provided blank page,
;; 1. Load the dependencies
;; 2. Load skewer-mode.el
;; 3. Start the HTTP server (`httpd-start')
;; 4. Include "http://localhost:8080/skewer" as a script
;; (see `example.html' and check your `httpd-port')
;; 5. Visit the document from your browser
;; Skewer fully supports CORS, so the document need not be hosted by
;; Emacs itself. A Greasemonkey userscript and a bookmarklet are
;; provided for injecting Skewer into any arbitrary page you're
;; visiting without needing to modify the page on the host.
;; With skewer-repl.el loaded, a REPL into the browser can be created
;; with M-x `skewer-repl', or C-c C-z. This should work like a console
;; within the browser. Messages can be logged to this REPL with
;; skewer.log() (just like console.log()).
;; Extending Skewer:
;; Skewer is flexible and open to extension. The REPL and the CSS and
;; HTML minor modes are a partial examples of this. You can extend
;; skewer.js with your own request handlers and talk to them from
;; Emacs using `skewer-eval' (or `skewer-eval-synchronously') with
;; your own custom :type. The :type string chooses the dispatch
;; function under the skewer.fn object. To inject your own JavaScript
;; into skewer.js, use `skewer-js-hook'.
;; You can also catch messages sent from the browser not in response
;; to an explicit request. Use `skewer-response-hook' to see all
;; incoming objects.
;;; History:
;; Version 1.8.0: features
;; * Work around XMLHttpRequest tampering in userscript
;; * Add Makefile "run" target for testing
;; Version 1.7.0: features and fixes
;; * Support for other major modes (including web-mode) in skewer-html-mode
;; * Opportunistic support for company-mode completions
;; * Always serve content as UTF-8
;; * Improve skewer-everything.js portability
;; Version 1.6.2: fixes
;; * skewer.log() takes multiple arguments
;; * comint and encoding fixes
;; Version 1.6.1: fixes
;; * Add `skewer-css-clear-all'
;; * Better IE8 compatibility
;; * User interface tweaks
;; Version 1.6.0: fixes
;; * Bring up to speed with Emacs 24.3
;; * Switch to cl-lib from cl
;; Version 1.5.3: features
;; * Add `skewer-run-phantomjs'
;; Version 1.5.2: small cleanup
;; * Add `skewer-apply' and `skewer-funall'
;; * Improved safeStringify
;; Version 1.5.1: features
;; * No more automatic hook setup (see `skewer-setup')
;; * Support for HTML interaction
;; * Support for loading Bower packages
;; * Drop jQuery dependency
;; * Many small improvements
;; Version 1.4: features
;; * Full CSS interaction
;; * Greasemonkey userscript for injection
;; * Full, working CORS support
;; * Better browser presence detection
;; Version 1.3: features and fixes
;; * Full offline support
;; * No more callback registering
;; * Fix 64-bit support
;; * Two new hooks for improved extension support
;; * More uniform keybindings with other interactive modes
;; Version 1.2: features
;; * Add a skewer-eval-print-last-expression
;; * Display evaluation time when it's long
;; * Flash the region on eval
;; * Improve JS stringification
;; Version 1.1: features and fixes
;; * Added `list-skewer-clients'
;; * Reduce the number of HTTP requests needed
;; * Fix stringification issues
;; Version 1.0: initial release
;;; Code:
(require 'cl-lib)
(require 'json)
(require 'url-util)
(require 'simple-httpd)
(require 'js2-mode)
(require 'cache-table)
(defgroup skewer nil
"Live browser JavaScript interaction."
:group 'languages)
(defvar skewer-mode-map
(let ((map (make-sparse-keymap)))
(prog1 map
(define-key map (kbd "C-x C-e") 'skewer-eval-last-expression)
(define-key map (kbd "C-M-x") 'skewer-eval-defun)
(define-key map (kbd "C-c C-k") 'skewer-load-buffer)))
"Keymap for skewer-mode.")
(defvar skewer-data-root (file-name-directory load-file-name)
"Location of data files needed by impatient-mode.")
(defvar skewer-js-hook ()
"Hook to run when skewer.js is being served to the browser.
When hook functions are called, the current buffer is the buffer
to be served to the client (a defservlet), with skewer.js script
already inserted. This is the chance for other packages to insert
their own JavaScript to extend skewer in the browser, such as
adding a new type handler.")
(defvar skewer-response-hook ()
"Hook to run when a response arrives from the browser. Used for
catching messages from the browser with no associated
callback. The response object is passed to the hook function.")
(defvar skewer-timeout 3600
"Maximum time to wait on the browser to respond, in seconds.")
(defvar skewer-clients ()
"Browsers awaiting JavaScript snippets.")
(defvar skewer-callbacks (cache-table-create skewer-timeout :test 'equal)
"Maps evaluation IDs to local callbacks.")
(defvar skewer-queue ()
"Queued messages for the browser.")
(defvar skewer--last-timestamp 0
"Timestamp of the last browser response. Use
`skewer-last-seen-seconds' to access this.")
(cl-defstruct skewer-client
"A client connection awaiting a response."
proc agent)
(defun skewer-process-queue ()
"Send all queued messages to clients."
(when (and skewer-queue skewer-clients)
(let ((message (pop skewer-queue))
(sent nil))
(while skewer-clients
(ignore-errors
(progn
(let ((proc (skewer-client-proc (pop skewer-clients))))
(with-temp-buffer
(insert (json-encode message))
(httpd-send-header proc "text/plain" 200
:Cache-Control "no-cache"
:Access-Control-Allow-Origin "*")))
(setq skewer--last-timestamp (float-time))
(setq sent t))))
(if (not sent) (push message skewer-queue)))
(skewer-process-queue)))
(defun skewer-clients-tabulate ()
"Prepare client list for tabulated-list-mode."
(cl-loop for client in skewer-clients collect
(let ((proc (skewer-client-proc client))
(agent (skewer-client-agent client)))
(cl-destructuring-bind (host port) (process-contact proc)
`(,client [,host ,(format "%d" port) ,agent])))))
(define-derived-mode skewer-clients-mode tabulated-list-mode "skewer-clients"
"Mode for listing browsers attached to Emacs for skewer-mode."
(setq tabulated-list-format [("Host" 12 t)
("Port" 5 t)
("User Agent" 0 t)])
(setq tabulated-list-entries #'skewer-clients-tabulate)
(tabulated-list-init-header))
(define-key skewer-clients-mode-map (kbd "g")
(lambda ()
(interactive)
(skewer-ping)
(revert-buffer)))
(defun skewer-update-list-buffer ()
"Revert the client list, due to an update."
(save-window-excursion
(let ((list-buffer (get-buffer "*skewer-clients*")))
(when list-buffer
(with-current-buffer list-buffer
(revert-buffer))))))
;;;###autoload
(defun list-skewer-clients ()
"List the attached browsers in a buffer."
(interactive)
(pop-to-buffer (get-buffer-create "*skewer-clients*"))
(skewer-clients-mode)
(tabulated-list-print))
(defun skewer-queue-client (proc req)
"Add a client to the queue, given the HTTP header."
(let ((agent (cl-second (assoc "User-Agent" req))))
(push (make-skewer-client :proc proc :agent agent) skewer-clients))
(skewer-update-list-buffer)
(skewer-process-queue))
;; Servlets
(defservlet skewer "text/javascript; charset=UTF-8" ()
(insert-file-contents (expand-file-name "skewer.js" skewer-data-root))
(goto-char (point-max))
(run-hooks 'skewer-js-hook))
(defun httpd/skewer/get (proc _path _query req &rest _args)
(skewer-queue-client proc req))
(defun httpd/skewer/post (proc _path _query req &rest _args)
(let* ((result (json-read-from-string (cadr (assoc "Content" req))))
(id (cdr (assoc 'id result)))
(callback (cache-table-get id skewer-callbacks)))
(setq skewer--last-timestamp (float-time))
(when callback
(funcall callback result))
(if id
(skewer-queue-client proc req)
(with-temp-buffer
(httpd-send-header proc "text/plain" 200
:Access-Control-Allow-Origin "*")))
(dolist (hook skewer-response-hook)
(funcall hook result))))
(defvar skewer-demo-source
(expand-file-name "example.html" skewer-data-root)
"Source file name or buffer for `httpd/skewer/demo' servlet.")
(defservlet skewer/demo "text/html; charset=UTF-8" ()
(cl-etypecase skewer-demo-source
(buffer (insert-buffer-substring skewer-demo-source))
(string (insert-file-contents skewer-demo-source))))
;; Minibuffer display
(defun skewer-success-p (result)
"Return T if result was a success."
(equal "success" (cdr (assoc 'status result))))
(define-derived-mode skewer-error-mode special-mode "skewer-error"
:group 'skewer
"Mode for displaying JavaScript errors returned by skewer-mode."
(setq truncate-lines t))
(defface skewer-error-face
'((((class color) (background light))
:foreground "red" :underline t)
(((class color) (background dark))
:foreground "red" :underline t))
"Face for JavaScript errors."
:group 'skewer)
(defun skewer--error (string)
"Return STRING propertized as an error message."
(propertize (or string "<unknown>") 'font-lock-face 'skewer-error-face))
(defun skewer-post-minibuffer (result)
"Report results in the minibuffer or the error buffer."
(if (skewer-success-p result)
(let ((value (cdr (assoc 'value result)))
(time (cdr (assoc 'time result))))
(if (and time (> time 1.0))
(message "%s (%.3f seconds)" value time)
(message "%s" value)))
(with-current-buffer (pop-to-buffer (get-buffer-create "*skewer-error*"))
(let ((inhibit-read-only t)
(error (cdr (assoc 'error result))))
(erase-buffer)
(skewer-error-mode)
(insert (skewer--error (cdr (assoc 'name error))) ": ")
(insert (or (cdr (assoc 'message error)) "") "\n\n")
(insert (or (cdr (assoc 'stack error)) "") "\n\n")
(insert (format "Expression: %s\n\n"
(if (cdr (assoc 'strict result)) "(strict)" ""))
(cdr (assoc 'eval error)))
(goto-char (point-min))))))
;; Evaluation functions
(cl-defun skewer-eval (string &optional callback
&key verbose strict (type "eval") extra)
"Evaluate STRING in the waiting browsers, giving the result to CALLBACK.
:VERBOSE -- if T, the return will try to be JSON encoded
:STRICT -- if T, expression is evaluated with 'use strict'
:TYPE -- chooses the JavaScript handler (default: eval)
:EXTRA -- additional alist keys to append to the request object"
(let* ((id (format "%x" (random most-positive-fixnum)))
(request `((type . ,type)
(eval . ,string)
(id . ,id)
(verbose . ,verbose)
(strict . ,strict)
,@extra)))
(prog1 request
(setf (cache-table-get id skewer-callbacks) callback)
(setq skewer-queue (append skewer-queue (list request)))
(skewer-process-queue))))
(defun skewer-eval-synchronously (string &rest args)
"Just like `skewer-eval' but synchronously, so don't provide a
callback. Use with caution."
(let ((result nil))
(apply #'skewer-eval string (lambda (v) (setq result v)) args)
(cl-loop until result
do (accept-process-output nil 0.01)
finally (return result))))
(defun skewer-apply (function args)
"Synchronously apply FUNCTION in the browser with the supplied
arguments, returning the result. All ARGS must be printable by
`json-encode'. For example,
(skewer-apply \"Math.atan2\" '(1 -2)) ; => 2.677945044588987
Uncaught exceptions propagate to Emacs as an error."
(let ((specials '(("undefined" . nil)
("NaN" . 0.0e+NaN)
("Infinity" . 1.0e+INF)
("-Infinity" . -1.0e+INF))))
(let* ((expr (concat function "(" (mapconcat #'json-encode args ", ") ")"))
(result (skewer-eval-synchronously expr :verbose t))
(value (cdr (assoc 'value result))))
(if (skewer-success-p result)
(if (assoc value specials)
(cdr (assoc value specials))
(condition-case _
(json-read-from-string value)
(json-readtable-error value)))
(signal 'javascript
(list (cdr (assoc 'message (cdr (assoc'error result))))))))))
(defun skewer-funcall (function &rest args)
"Synchronously call FUNCTION with the supplied ARGS. All ARGS
must be printable by `json-read-from-string. For example,
(skewer-funcall \"Math.sin\" 0.5) ; => 0.479425538604203
Uncaught exceptions propagate to Emacs as an error."
(skewer-apply function args))
(defun skewer--save-point (f &rest args)
"Return a function that calls F with point at the current point."
(let ((saved-point (point)))
(lambda (&rest more)
(save-excursion
(goto-char saved-point)
(apply f (append args more))))))
(defun skewer-ping ()
"Ping the browser to test that it's still alive."
(unless (null skewer-clients) ; don't queue pings
(skewer-eval (prin1-to-string (float-time)) nil :type "ping")))
(defun skewer-last-seen-seconds ()
"Return the number of seconds since the browser was last seen."
(skewer-ping) ; make sure it's still alive next request
(- (float-time) skewer--last-timestamp))
(defun skewer-mode-strict-p ()
"Return T if buffer contents indicates strict mode."
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(js2-forward-sws)
(forward-char 1)
(let* ((stricts '("\"use strict\"" "'use strict'"))
(node (js2-node-at-point))
(code (buffer-substring-no-properties (js2-node-abs-pos node)
(js2-node-abs-end node))))
(and (member code stricts) t)))))
(defun skewer-flash-region (start end &optional timeout)
"Temporarily highlight region from START to END."
(let ((overlay (make-overlay start end)))
(overlay-put overlay 'face 'secondary-selection)
(run-with-timer (or timeout 0.2) nil 'delete-overlay overlay)))
(defun skewer-get-last-expression ()
"Return the JavaScript expression before the point as a
list: (string start end)."
(save-excursion
(js2-backward-sws)
(backward-char)
(let ((node (js2-node-at-point nil t)))
(when (eq js2-FUNCTION (js2-node-type (js2-node-parent node)))
(setq node (js2-node-parent node)))
(when (js2-ast-root-p node)
(error "no expression found"))
(let ((start (js2-node-abs-pos node))
(end (js2-node-abs-end node)))
(list (buffer-substring-no-properties start end) start end)))))
(defun skewer-eval-last-expression (&optional prefix)
"Evaluate the JavaScript expression before the point in the
waiting browser. If invoked with a prefix argument, insert the
result into the current buffer."
(interactive "P")
(if prefix
(skewer-eval-print-last-expression)
(if js2-mode-buffer-dirty-p
(js2-mode-wait-for-parse
(skewer--save-point #'skewer-eval-last-expression))
(cl-destructuring-bind (string start end) (skewer-get-last-expression)
(skewer-flash-region start end)
(skewer-eval string #'skewer-post-minibuffer)))))
(defun skewer-get-defun ()
"Return the toplevel JavaScript expression around the point as
a list: (string start end)."
(save-excursion
(js2-backward-sws)
(backward-char)
(let ((node (js2-node-at-point nil t)))
(when (js2-ast-root-p node)
(error "no expression found"))
(while (and (js2-node-parent node)
(not (js2-ast-root-p (js2-node-parent node))))
(setf node (js2-node-parent node)))
(let ((start (js2-node-abs-pos node))
(end (js2-node-abs-end node)))
(list (buffer-substring-no-properties start end) start end)))))
(defun skewer-eval-defun ()
"Evaluate the JavaScript expression before the point in the
waiting browser."
(interactive)
(if js2-mode-buffer-dirty-p
(js2-mode-wait-for-parse (skewer--save-point #'skewer-eval-defun))
(cl-destructuring-bind (string start end) (skewer-get-defun)
(skewer-flash-region start end)
(skewer-eval string #'skewer-post-minibuffer))))
;; Print last expression
(defvar skewer-eval-print-map (cache-table-create skewer-timeout :test 'equal)
"A mapping of evaluation IDs to insertion points.")
(defun skewer-post-print (result)
"Insert the result after its source expression."
(if (not (skewer-success-p result))
(skewer-post-minibuffer result)
(let* ((id (cdr (assoc 'id result)))
(pos (cache-table-get id skewer-eval-print-map)))
(when pos
(with-current-buffer (car pos)
(goto-char (cdr pos))
(insert (cdr (assoc 'value result)) "\n"))))))
(defun skewer-eval-print-last-expression ()
"Evaluate the JavaScript expression before the point in the
waiting browser and insert the result in the buffer at point."
(interactive)
(if js2-mode-buffer-dirty-p
(js2-mode-wait-for-parse
(skewer--save-point #'skewer-eval-print-last-expression))
(cl-destructuring-bind (string start end) (skewer-get-defun)
(skewer-flash-region start end)
(insert "\n")
(let* ((request (skewer-eval string #'skewer-post-print :verbose t))
(id (cdr (assoc 'id request)))
(pos (cons (current-buffer) (point))))
(setf (cache-table-get id skewer-eval-print-map) pos)))))
;; Script loading
(defvar skewer-hosted-scripts (cache-table-create skewer-timeout)
"Map of hosted scripts to IDs.")
(defun skewer-host-script (string)
"Host script STRING from the script servlet, returning the script ID."
(let ((id (random most-positive-fixnum)))
(prog1 id
(setf (cache-table-get id skewer-hosted-scripts) string))))
(defun skewer-load-buffer ()
"Load the entire current buffer into the browser. A snapshot of
the buffer is hosted so that browsers visiting late won't see an
inconsistent buffer."
(interactive)
(let ((id (skewer-host-script (buffer-string)))
(buffer-name (buffer-name)))
(skewer-eval (format "/skewer/script/%d/%s"
id (url-hexify-string buffer-name))
(lambda (_) (message "%s loaded" buffer-name))
:type "script")))
(defservlet skewer/script "text/javascript; charset=UTF-8" (path)
(let ((id (string-to-number (nth 3 (split-string path "/")))))
(insert (cache-table-get id skewer-hosted-scripts ""))))
;; Define the minor mode
;;;###autoload
(define-minor-mode skewer-mode
"Minor mode for interacting with a browser."
:lighter " skewer"
:keymap skewer-mode-map
:group 'skewer)
;;;###autoload
(defun run-skewer (&optional arg)
"Attach a browser to Emacs for a skewer JavaScript REPL. Uses
`browse-url' to launch a browser.
With a prefix arugment (C-u), it will ask the filename of the
root document. With two prefix arguments (C-u C-u), it will use
the contents of the current buffer as the root document."
(interactive "p")
(cl-case arg
(4 (setf skewer-demo-source (read-file-name "Skewer filename: ")))
(16 (setf skewer-demo-source (current-buffer))))
(httpd-start)
(browse-url (format "http://127.0.0.1:%d/skewer/demo" httpd-port)))
;; PhantomJS
(defvar phantomjs-program-name "/usr/bin/phantomjs"
"Path to the phantomjs executable.")
(defvar skewer-phantomjs-processes ()
"List of phantomjs processes connected to Skewer.")
(defun skewer-phantomjs-sentinel (proc event)
"Cleanup after phantomjs exits."
(when (cl-some (lambda (s) (string-match-p s event))
'("finished" "abnormal" "killed"))
(delete-file (process-get proc 'tempfile))))
;;;###autoload
(defun skewer-run-phantomjs ()
"Connect an inferior PhantomJS process to Skewer, returning the process."
(interactive)
(httpd-start)
(let ((script (make-temp-file "phantomjs-"))
(url (format "http://0:%d/skewer/demo" httpd-port)))
(with-temp-buffer
(insert (format "require('webpage').create().open('%s')" url))
(write-region nil nil script nil 0)
(let ((proc (start-process "phantomjs" nil
phantomjs-program-name script)))
(prog1 proc
(push proc skewer-phantomjs-processes)
(process-put proc 'tempfile script)
(set-process-sentinel proc 'skewer-phantomjs-sentinel))))))
(defun skewer-phantomjs-kill ()
"Kill all inferior phantomjs processes connected to Skewer."
(interactive)
(mapc #'delete-process skewer-phantomjs-processes)
(setf skewer-phantomjs-processes nil))
(provide 'skewer-mode)
;;; skewer-mode.el ends here