A JavaScript development environment for Emacs https://indium.readthedocs.io
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.
 
 
 
 

448 lines
18 KiB

;;; jade-webkit.el --- Webkit/Blink backend for jade -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Nicolas Petton
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords: tools, javascript
;; This program 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.
;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Jade backend implementation for Webkit and Blink. Connection is handled in
;; jade-chrome.el and jade-nodejs.el. This backend currently supports the REPL,
;; code completion, object inspection and the debugger.
;;
;; The protocol is documented at
;; https://chromedevtools.github.io/debugger-protocol-viewer/1-1/.
;;; Code:
(require 'websocket)
(require 'json)
(require 'map)
(require 'seq)
(require 'jade-backend)
(require 'jade-repl)
(require 'jade-debugger)
(defvar jade-webkit-completion-function "function getCompletions(type)\n{var object;if(type==='string')\nobject=new String('');else if(type==='number')\nobject=new Number(0);else if(type==='boolean')\nobject=new Boolean(false);else\nobject=this;var resultSet={};for(var o=object;o;o=o.__proto__){try{if(type==='array'&&o===object&&ArrayBuffer.isView(o)&&o.length>9999)\ncontinue;var names=Object.getOwnPropertyNames(o);for(var i=0;i<names.length;++i)\nresultSet[names[i]]=true;}catch(e){}}\nreturn resultSet;}")
(jade-register-backend 'webkit)
(cl-defmethod jade-backend-close-connection ((backend (eql webkit)) connection)
"Close the websocket associated with CONNECTION."
(websocket-close (map-elt connection 'ws)))
(cl-defmethod jade-backend-reconnect ((backend (eql webkit)))
(let* ((connection jade-connection)
(url (map-elt connection 'url))
(websocket-url (websocket-url (map-elt connection 'ws))))
(jade-webkit--open-ws-connection url
websocket-url
;; close all buffers related to the closed
;; connection the first
#'jade-quit)))
(cl-defmethod jade-backend-evaluate ((backend (eql webkit)) string &optional callback)
"Evaluate STRING then call CALLBACK.
CALLBACK is called with two arguments, the value returned by the
evaluation and non-nil if the evaluation threw an error."
(jade-webkit--send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,string)
(generatePreview . t))))
(lambda (response)
(when callback
(jade-webkit--handle-evaluation-response response callback)))))
(cl-defmethod jade-backend-evaluate-on-frame ((backend (eql webkit)) string frame &optional callback)
"Evaluate STRING on the call frame FRAME then call CALLBACK.
CALLBACK is called with two arguments, the value returned by the
evaluation and non-nil if the evaluation threw an error."
(jade-webkit--send-request
`((method . "Debugger.evaluateOnCallFrame")
(params . ((expression . ,string)
(callFrameId . ,(map-elt frame 'callFrameId))
(generatePreview . t))))
(lambda (response)
(jade-webkit--handle-evaluation-response response callback))))
(cl-defmethod jade-backend-get-completions ((backend (eql webkit)) expression prefix callback)
"Get the completion candidates for EXPRESSION that match PREFIX.
Evaluate CALLBACK on the filtered candidates."
(let ((expression (jade-webkit--completion-expression expression)))
(jade-webkit--send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,expression)
(objectGroup . "completion"))))
(lambda (response)
(jade-webkit--handle-completions-response response prefix callback)))))
(cl-defmethod jade-backend-get-properties ((backend (eql webkit)) reference &optional callback all-properties)
"Get the properties of the remote object represented by REFERENCE.
CALLBACK is evaluated with the list of properties.
If ALL-PROPERTIES is non-nil, get all the properties from the
prototype chain of the remote object."
(jade-webkit--send-request
`((method . "Runtime.getProperties")
(params . ((objectId . ,reference)
(ownProperties . ,(not all-properties)))))
(lambda (response)
(funcall callback
(jade-webkit--properties
(map-nested-elt response '(result result)))))))
(cl-defmethod jade-backend-get-script-source ((backend (eql webkit)) frame callback)
(let ((script-id (map-nested-elt frame '(location scriptId))))
(jade-webkit--send-request
`((method . "Debugger.getScriptSource")
(params . ((scriptId . ,script-id))))
callback)))
(cl-defmethod jade-backend-resume ((backend (eql webkit)) &optional callback)
"Resume the debugger and evaluate CALLBACK if non-nil."
(jade-webkit--send-request
`((method . "Debugger.resume"))
callback))
(cl-defmethod jade-backend-step-into ((backend (eql webkit)) &optional callback)
"Step into the current stack frame and evaluate CALLBACK if non-nil."
(jade-webkit--send-request
`((method . "Debugger.stepInto"))
callback))
(cl-defmethod jade-backend-step-out ((backend (eql webkit)) &optional callback)
"Step out the current stack frame and evaluate CALLBACK if non-nil."
(jade-webkit--send-request
`((method . "Debugger.stepOut"))
callback))
(cl-defmethod jade-backend-step-over ((backend (eql webkit)) &optional callback)
"Step over the current stack frame and evaluate CALLBACK if non-nil."
(jade-webkit--send-request
`((method . "Debugger.stepOver"))
callback))
(cl-defmethod jade-backend-continue-to-location ((backend (eql webkit)) location &optional callback)
"Continue to LOCATION and evaluate CALLBACK if non-nil.
Location should be an alist with a `limeNumber' and `scriptId' key."
(jade-webkit--send-request
`((method . "Debugger.continueToLocation")
(params . ((location . ,location))))
callback))
(defun jade-webkit-set-pause-on-exceptions (state)
" Defines on which STATE to pause.
Can be set to stop on all exceptions, uncaught exceptions or no
exceptions. Initial pause on exceptions state is set by Jade to
`\"uncaught\"'.
Allowed states: `\"none\"', `\"uncaught\"', `\"all\"'."
(interactive (list (completing-read "Pause on exceptions: "
'("none" "uncaught" "all")
nil
t)))
(jade-webkit--send-request `((method . "Debugger.setPauseOnExceptions")
(params . ((state . ,state))))))
(defun jade-webkit--open-ws-connection (url websocket-url &optional on-open)
"Open a websocket connection to URL using WEBSOCKET-URL.
Evaluate ON-OPEN when the websocket is open, before setting up
the connection and buffers.
In a Chrom{e|ium} session, URL corresponds to the url of a tab,
and WEBSOCKET-URL to its associated `webSocketDebuggerUrl'.
In a NodeJS session, URL and WEBSOCKET-URL should point to the
same url."
(unless websocket-url
(user-error "Cannot open connection, another devtools instance might be open"))
(websocket-open websocket-url
:on-open (lambda (ws)
(when on-open
(funcall on-open))
(jade-webkit--handle-ws-open ws url))
:on-message #'jade-webkit--handle-ws-message
:on-close #'jade-webkit--handle-ws-closed
:on-error #'jade-webkit--handle-ws-error))
(defun jade-webkit--make-connection (ws url)
"Return a new connection for WS and URL."
(let ((connection (make-hash-table)))
(map-put connection 'ws ws)
(map-put connection 'url url)
(map-put connection 'backend 'webkit)
(map-put connection 'callbacks (make-hash-table))
(add-to-list 'jade-connections connection)
connection))
(defun jade-webkit--callbacks ()
"Return the callbacks associated with the current connection."
(map-elt jade-connection 'callbacks))
(defun jade-webkit--connection-for-ws (ws)
"Return the webkit connection associated with the websocket WS."
(seq-find (lambda (connection)
(eq (map-elt connection 'ws) ws))
jade-connections))
(defun jade-webkit--handle-ws-open (ws url)
(let* ((connection (jade-webkit--make-connection ws url)))
(jade-with-connection connection
(jade-webkit--enable-tools))
(switch-to-buffer (jade-repl-get-buffer-create connection))))
(defun jade-webkit--handle-ws-message (ws frame)
(jade-with-connection (jade-webkit--connection-for-ws ws)
(let* ((message (jade-webkit--read-ws-message frame))
(error (map-elt message 'error))
(method (map-elt message 'method))
(request-id (map-elt message 'id))
(callback (map-elt (jade-webkit--callbacks) request-id)))
(cond
(error (message (map-elt error 'message)))
(request-id (when callback
(funcall callback message)))
(t (pcase method
("Console.messageAdded" (jade-webkit--handle-console-message message))
("Debugger.paused" (jade-webkit--handle-debugger-paused message))
("Debugger.resumed" (jade-webkit--handle-debugger-resumed message))))))))
(defun jade-webkit--handle-console-message (message)
(let* ((level (map-nested-elt message '(params message level)))
(text (map-nested-elt message '(params message text))))
(jade-repl-emit-console-message text level)))
(defun jade-webkit--handle-debugger-paused (message)
(let ((frames (map-nested-elt message '(params callFrames))))
(jade-debugger-paused 'webkit (jade-webkit--frames frames))))
(defun jade-webkit--handle-debugger-resumed (_message)
(jade-debugger-resumed))
(defun jade-webkit--handle-ws-closed (_ws)
(jade-repl--handle-connection-closed))
(defun jade-webkit--handle-ws-error (ws action error)
(message "WS Error! %s %s" action error))
(defun jade-webkit--send-request (request &optional callback)
"Send REQUEST to the current connection.
Evaluate CALLBACK with the response.
If the current connection is closed, display an error message in
the REPL buffer."
(when (not (jade-webkit--connected-p))
(jade-repl-emit-console-message "Socket connection closed" "error"))
(let ((id (jade-webkit--next-request-id))
(callbacks (jade-webkit--callbacks)))
(when callback
(map-put callbacks id callback))
(websocket-send-text (map-elt jade-connection 'ws)
(json-encode (cons `(id . ,id) request)))))
(defun jade-webkit--read-ws-message (frame)
(json-read-from-string (websocket-frame-payload frame)))
(defun jade-webkit--enable-tools ()
"Enable developer tools for the current tab.
There is currently no support for the DOM inspector and network
inspectors."
(jade-webkit--enable-console)
(jade-webkit--enable-runtime)
(jade-webkit--enable-debugger))
(defun jade-webkit--enable-console ()
"Enable the console on the current tab."
(jade-webkit--send-request '((method . "Console.enable"))))
(defun jade-webkit--enable-runtime ()
"Enable the runtime on the current tab."
(jade-webkit--send-request '((method . "Runtime.enable")))
(jade-webkit--send-request '((method . "Runtime.run"))))
(defun jade-webkit--enable-debugger ()
"Enable the debugger on the current tab."
(jade-webkit--send-request '((method . "Debugger.enable"))
(lambda (&rest _)
(jade-webkit-set-pause-on-exceptions "uncaught"))))
(defun jade-webkit--handle-evaluation-response (response callback)
"Get the result of an evaluation in RESPONSE and evaluate CALLBACK with it."
(let* ((result (map-nested-elt response '(result result)))
(error (eq (map-nested-elt response '(result wasThrown)) t)))
(funcall callback (jade-webkit--value result) error)))
(defun jade-webkit--handle-completions-response (response prefix callback)
"Request a completion list for the object in RESPONSE.
The completion list is filtered using the PREFIX string, then
CALLBACK is evaluated with it."
(let ((objectid (map-nested-elt response '(result result objectId)))
(type (map-nested-elt response '(result result type))))
(if objectid
(jade-webkit--get-completion-list-by-reference objectid prefix callback)
(jade-webkit--get-completion-list-by-type type prefix callback))))
(defun jade-webkit--get-completion-list-by-reference (objectid prefix callback)
"Request the completion list for a remote object referenced by OBJECTID.
The completion list is filtered using the PREFIX string, then
CALLBACK is evaluated with it."
(jade-webkit--send-request
`((method . "Runtime.callFunctionOn")
(params . ((objectId . ,objectid)
(functionDeclaration . ,jade-webkit-completion-function)
(returnByValue . t))))
(lambda (response)
(jade-webkit--handle-completion-list-response response prefix callback))))
(defun jade-webkit--get-completion-list-by-type (type prefix callback)
"Request the completion list for an object of type TYPE.
The completion list is filtered using the PREFIX string, then
CALLBACK is evaluated with it.
This method is used for strings, numbers and booleans. See
`jade-webkit--get-completion-list-by-reference' for getting
completions using references to remote objects (including
arrays)."
(let ((expression (format "(%s)(\"%s\")" jade-webkit-completion-function type)))
(jade-webkit--send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,expression)
(returnByValue . t))))
(lambda (response)
(jade-webkit--handle-completion-list-response response prefix callback)))))
(defun jade-webkit--completion-expression (string)
"Return the completion expression to be requested from STRING."
(if (string-match-p "\\." string)
(replace-regexp-in-string "\\.[^\\.]*$" "" string)
"this"))
(defun jade-webkit--handle-completion-list-response (response prefix callback)
"Evauate CALLBACK on the completion candidates from RESPONSE.
Candidates are filtered using the PREFIX string."
(let ((candidates (map-nested-elt response '(result result value))))
(funcall callback (seq-filter (lambda (candidate)
(string-prefix-p prefix candidate))
(seq-map (lambda (candidate)
(symbol-name (car candidate)))
candidates)))))
(cl-defmethod jade-webkit--connected-p ()
"Return non-nil if the current connction is open."
(websocket-openp (map-elt jade-connection 'ws)))
(defun jade-webkit--value (result)
"Return an alist representing the value of RESULT.
The returned value can be a reference to a remote object, in
which case the value associated to the `objectid' key is
non-nil."
(let* ((value (map-elt result 'value))
(type (intern (map-elt result 'type)))
(objectid (map-elt result 'objectId))
(preview (jade-webkit--preview result))
(description (jade-webkit--description result)))
`((objectid . ,objectid)
(description . ,description)
(type . ,type)
(preview . ,preview)
(value . ,value))))
(defun jade-webkit--description (result)
"Return a description string built from RESULT.
RESULT should be a reference to a remote object."
(let ((value (map-elt result 'value))
(type (intern (map-elt result 'type))))
(or (map-elt result 'description)
(pcase type
(`undefined "undefined")
(`function "function")
(`number (if (numberp value)
(number-to-string value)
value))
(`string (format "\"%s\"" value))
(`boolean (pcase value
(`t "true")
(_ "false")))
(_ (or value "null"))))))
(defun jade-webkit--preview (result)
"Return a preview string built from RESULT.
RESULT should be a reference to a remote object."
(let* ((preview (map-elt result 'preview))
(subtype (map-elt preview 'subtype)))
(if (string= subtype "array")
(jade-webkit--preview-array preview)
(jade-webkit--preview-object preview))))
(defun jade-webkit--preview-object (preview)
"Return a preview string from the properties of the object PREVIEW."
(concat " { "
(mapconcat (lambda (prop)
(format "%s: %s"
(map-elt prop 'name)
(jade-webkit--description prop)))
(map-elt preview 'properties)
", ")
(if (eq (map-elt preview 'lossless) :json-false)
", … }"
" }")))
(defun jade-webkit--preview-array (preview)
"Return a preview string from the elements of the array PREVIEW."
(concat " [ "
(mapconcat (lambda (prop)
(format "%s" (jade-webkit--description prop)))
(map-elt preview 'properties)
", ")
(if (eq (map-elt preview 'lossless) :json-false)
"… ]"
" ]")))
(defun jade-webkit--properties (result)
"Return a list of object properties built from RESULT."
(seq-map (lambda (prop)
`((name . ,(map-elt prop 'name))
(value . ,(jade-webkit--value (or (map-elt prop 'value)
(map-elt prop 'get))))))
result))
(defun jade-webkit--frames (list)
"Return a list of frames built from LIST."
(seq-map (lambda (frame)
`((scope-chain . ,(seq-map (lambda (scope)
`((object . ,(jade-webkit--value (map-elt scope 'object)))
(name . ,(map-elt scope 'name))
(type . ,(map-elt scope 'type))))
(map-elt frame 'scopeChain)))
(location . ,(map-elt frame 'location))
(callFrameId . ,(map-elt frame 'callFrameId))))
list))
(let ((id 0))
(defun jade-webkit--next-request-id ()
"Return the next unique identifier to be used in a request."
(setq id (1+ id))
id))
(provide 'jade-webkit)
;;; jade-webkit.el ends here