621 lines
23 KiB
EmacsLisp
621 lines
23 KiB
EmacsLisp
![]() |
;;; 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 <wellons@nullprogram.com>
|
||
|
;; 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
|