Browse Source

Decouple a generic backend from the chrome specific one

workspaces
Nicolas Petton 6 years ago
parent
commit
c93da650a8
Signed by: nico GPG Key ID: 233587A47C207910
6 changed files with 613 additions and 458 deletions
  1. +140
    -0
      jade-backend.el
  2. +429
    -0
      jade-chrome.el
  3. +21
    -16
      jade-debugger.el
  4. +7
    -5
      jade-inspector.el
  5. +14
    -15
      jade-repl.el
  6. +2
    -422
      jade.el

+ 140
- 0
jade-backend.el View File

@ -0,0 +1,140 @@
;;; jade-backend.el --- Backend for jade.el -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Nicolas Petton
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords: internal
;; 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:
;; Generic backend implementation.
;; Backends should define a new backend symbol using `jade-register-backend',
;;; Code:
(require 'map)
(require 'jade-repl)
(defvar jade-connections (list) "List of connections.")
(defvar jade-connection nil
"Current connection to the browser tab.
A connection should be an alist with the following required keys:
`backend' and `url'. Other backend-specific keys might be used
by backends.")
(make-variable-buffer-local 'jade-connection)
(defvar jade-backend nil
"Current backend used.")
(make-variable-buffer-local 'jade-backend)
(defvar jade-backends nil "List of registered backends.")
(defmacro jade-with-connection (connection &rest body)
"Set the value of `jade-connection' to CONNECTION and evaluate BODY."
`(let ((jade-connection ,connection))
,@body))
(defun jade-register-backend (backend)
"Register a new BACKEND.
BACKEND should be a symbol."
(add-to-list 'jade-backends backend))
(defun jade-connect (backend host port)
"Open a connection to BACKEND on HOST:PORT."
(interactive (list (completing-read "Connect to backend: " jade-backends nil t)
(read-from-minibuffer "Host: " "127.0.0.1")
(read-from-minibuffer "Port: " "9222")))
(jade-backend-connect (intern backend) host port))
(cl-defgeneric jade-backend-connect (backend host port)
"Open a connection to BACKEND on HOST:PORT.
A connection is an alist representing a browser connection. Keys
can vary depending on backend requirements, the only mandatory
key is `url'.
Once created, the connection should be added to
`jade-connections'.")
(defun jade-quit ()
"Close the current connection and kill its REPL buffer if any."
(interactive)
(unless jade-connection
(user-error "No active connection to close"))
(when (y-or-n-p (format "Do you really want to close the connection to %s ? "
(map-elt jade-connection 'url)))
(jade-backend-close-connection jade-backend jade-connection)
(setq jade-connections (remq jade-connection jade-connections))
(kill-buffer (jade-repl-get-buffer))))
;;; jade-connection methods
(cl-defgeneric jade-backend-close-connection (backend connection)
"Close CONNECTION.")
(cl-defgeneric jade-backend-evaluate (backend 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.
The value should be an alist with a the following required keys:
`type', `value' and `description'. If the value represents a
remote object that can be inspected, it should also have an
`objectid' key.")
(cl-defgeneric jade-backend-get-completions (backend expression prefix callback)
"Get the completion list using CONNECTION for EXPRESSION that match PREFIX.
Evaluate CALLBACK on the filtered candidates.
EXPRESSION should be a valid JavaScript expression string.")
(cl-defgeneric jade-backend-get-properties (backend reference &optional callback all-properties)
"Request the properties of the remote object represented by REFERENCE.
REFERENCE must be the id of a remote object.
CALLBACK is called with the fetched list of properties.
If ALL-PROPERTIES is non-nil, get all the properties from the
prototype chain of the remote object.")
(cl-defgeneric jade-backend-get-script-source (backend frame callback)
"Get the source of the script for FRAME.
Evaluate CALLBACK with the result.")
(cl-defgeneric jade-backend-resume (backend &optional callback)
"Resume the debugger and evaluate CALLBACK if non-nil.")
(cl-defgeneric jade-backend-step-into (backend &optional callback)
"Step into the current stack frame and evaluate CALLBACK if non-nil.")
(cl-defgeneric jade-backend-step-out (backend &optional callback)
"Step out the current stack frame and evaluate CALLBACK if non-nil.")
(cl-defgeneric jade-backend-step-over (backend &optional callback)
"Step over the current stack frame and evaluate CALLBACK if non-nil.")
(cl-defgeneric jade-backend-continue-to-location (backend location &optional callback)
"Continue to LOCATION and evaluate CALLBACK if non-nil.
Location should be an alist with a `column' and `row' key.")
(defun jade-backend-object-reference-p (value)
"Return non-nil if VALUE is a reference to a remote object."
(map-elt value 'objectid))
(provide 'jade-backend)
;;; jade-backend.el ends here

+ 429
- 0
jade-chrome.el View File

@ -0,0 +1,429 @@
;;; jade-chrome.el --- Chrom{e|ium} 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 Chrome and Chromium. Currently supports the
;; REPL, code completion, object inspection and the debugger.
;;; Code:
(require 'websocket)
(require 'url)
(require 'json)
(require 'map)
(require 'seq)
(require 'jade-backend)
(require 'jade-repl)
(require 'jade-debugger)
(defvar jade-chrome-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 'chrome)
(cl-defmethod jade-backend-connect ((backend (eql chrome)) host port)
"Open a connection to a chrome tab on HOST:PORT."
(jade-chrome--get-tabs-data host port #'jade-chrome--connect-to-tab))
(cl-defmethod jade-backend-close-connection ((backend (eql chrome)) connection)
"Close the websocket associated with CONNECTION."
(websocket-close (map-elt connection 'ws)))
(cl-defmethod jade-backend-evaluate ((backend (eql chrome)) 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-chrome--send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,string)
(generatePreview . t))))
(lambda (response)
(jade-chrome--handle-evaluation-response response callback))))
(cl-defmethod jade-backend-get-completions ((backend (eql chrome)) expression prefix callback)
"Get the completion candidates for EXPRESSION that match PREFIX.
Evaluate CALLBACK on the filtered candidates."
(let ((expression (jade-chrome--completion-expression expression)))
(jade-chrome--send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,expression)
(objectGroup . "completion"))))
(lambda (response)
(jade-chrome--handle-completions-response response prefix callback)))))
(cl-defmethod jade-backend-get-properties ((backend (eql chrome)) 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-chrome--send-request
`((method . "Runtime.getProperties")
(params . ((objectId . ,reference)
(ownProperties . ,(not all-properties)))))
(lambda (response)
(funcall callback
(jade-chrome--properties
(map-nested-elt response '(result result)))))))
(cl-defmethod jade-backend-get-script-source ((backend (eql chrome)) frame callback)
(let ((script-id (map-nested-elt frame '(location scriptId))))
(jade-chrome--send-request
`((method . "Debugger.getScriptSource")
(params . ((scriptId . ,script-id))))
callback)))
(cl-defmethod jade-backend-resume ((backend (eql chrome)) &optional callback)
"Resume the debugger and evaluate CALLBACK if non-nil."
(jade-chrome--send-request
`((method . "Debugger.resume"))
callback))
(cl-defmethod jade-backend-step-into ((backend (eql chrome)) &optional callback)
"Step into the current stack frame and evaluate CALLBACK if non-nil."
(jade-chrome--send-request
`((method . "Debugger.stepInto"))
callback))
(cl-defmethod jade-backend-step-out ((backend (eql chrome)) &optional callback)
"Step out the current stack frame and evaluate CALLBACK if non-nil."
(jade-chrome--send-request
`((method . "Debugger.stepOut"))
callback))
(cl-defmethod jade-backend-step-over ((backend (eql chrome)) &optional callback)
"Step over the current stack frame and evaluate CALLBACK if non-nil."
(jade-chrome--send-request
`((method . "Debugger.stepOver"))
callback))
(cl-defmethod jade-backend-continue-to-location ((backend (eql chrome)) location &optional callback)
"Continue to LOCATION and evaluate CALLBACK if non-nil.
Location should be an alist with a `column' and `row' key."
(jade-chrome--send-request
`((method . "Debugger.continueToLocation")
(params . ((location . ,location))))
callback))
(defun jade-chrome--get-tabs-data (host port callback)
"Get the list of open tabs on HOST:PORT and evaluate CALLBACK with it."
(url-retrieve (format "http://%s:%s/json" host port)
(lambda (status)
;; TODO: handle errors
(funcall callback (jade-chrome--read-tab-data)))))
(defun jade-chrome--connect-to-tab (tabs)
"Ask the user for a tab in the list TABS and connects to it."
(let* ((titles (seq-map (lambda (tab)
(map-elt tab 'title))
tabs))
(title (completing-read "Tab: " titles nil t)))
(jade-chrome--open-ws-connection (seq-find (lambda (tab)
(string= (map-elt tab 'title) title))
tabs))))
(defun jade-chrome--read-tab-data ()
"Return the JSON tabs data in the current buffer."
(when (save-match-data
(looking-at "^HTTP/1\\.1 200 OK$"))
(goto-char (point-min))
(search-forward "\n\n")
(delete-region (point-min) (point))
(json-read)))
(defun jade-chrome--open-ws-connection (tab)
"Open a websocket connection to the `webSocketDebuggerUrl' of TAB.
If TAB does not have a `webSocketDebuggerUrl', throw a user
error. This might happen when trying to connect to a tab twice,
or if an inspector is open on that tab."
(let ((url (map-elt tab 'url))
(debugger-url (map-elt tab 'webSocketDebuggerUrl)))
(unless debugger-url
(user-error "Cannot open connection, another devtools instance might be open"))
(websocket-open debugger-url
:on-open (lambda (ws) (jade-chrome--handle-ws-open ws url))
:on-message #'jade-chrome--handle-ws-message
:on-close #'jade-chrome--handle-ws-closed
:on-error #'jade-chrome--handle-ws-error)))
(defun jade-chrome--make-connection (ws url)
"Return a new connection for WS and URL."
(let ((connection `((ws . ,ws)
(url . ,url)
(backend . chrome)
(callbacks . ,(make-hash-table)))))
(add-to-list 'jade-connections connection)
connection))
(defun jade-chrome--callbacks ()
"Return the callbacks associated with the current connection."
(map-elt jade-connection 'callbacks))
(defun jade-chrome--connection-for-ws (ws)
"Return the chrome connection associated with the websocket WS."
(seq-find (lambda (connection)
(eq (map-elt connection 'ws) ws))
jade-connections))
(defun jade-chrome--handle-ws-open (ws url)
(let* ((connection (jade-chrome--make-connection ws url)))
(jade-with-connection connection
(jade-chrome--enable-tools))
(switch-to-buffer (jade-repl-get-buffer-create 'chrome connection))))
(defun jade-chrome--handle-ws-message (ws frame)
(jade-with-connection (jade-chrome--connection-for-ws ws)
(let* ((message (jade-chrome--read-ws-message frame))
(error (map-elt message 'error))
(method (map-elt message 'method))
(request-id (map-elt message 'id))
(callback (map-elt (jade-chrome--callbacks) request-id)))
(cond
(error (message (map-elt error 'message)))
(request-id (when callback
(funcall callback message)))
(t (pcase method
("Console.messageAdded" (jade-chrome--handle-console-message message))
("Debugger.paused" (jade-chrome--handle-debugger-paused message))
("Debugger.resumed" (jade-chrome--handle-debugger-resumed message))))))))
(defun jade-chrome--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-chrome--handle-debugger-paused (message)
(let ((frames (map-nested-elt message '(params callFrames))))
(jade-debugger-paused 'chrome (jade-chrome--frames frames))))
(defun jade-chrome--handle-debugger-resumed (_message)
(jade-debugger-resumed))
(defun jade-chrome--handle-ws-closed (_ws)
)
(defun jade-chrome--handle-ws-error (ws action error)
(message "WS Error! %s %s" action error))
(defun jade-chrome--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-chrome--connected-p))
(jade-repl-emit-console-message "Socket connection closed" "error"))
(let ((id (jade-chrome--next-request-id))
(callbacks (jade-chrome--callbacks)))
(when callback
(map-put callbacks id callback))
(websocket-send-text (map-elt jade-connection 'ws)
(json-encode (cons `(id . ,id) request)))))
(defun jade-chrome--read-ws-message (frame)
(with-temp-buffer
(insert (websocket-frame-payload frame))
(goto-char (point-min))
(json-read)))
(defun jade-chrome--enable-tools ()
"Enable developer tools for the current tab.
There is currently no support for the DOM inspector and network
inspectors."
(jade-chrome--enable-console)
(jade-chrome--enable-runtime)
(jade-chrome--enable-debugger))
(defun jade-chrome--enable-console ()
"Enable the console on the current tab."
(jade-chrome--send-request '((method . "Console.enable"))))
(defun jade-chrome--enable-runtime ()
"Enable the runtime on the current tab."
(jade-chrome--send-request '((method . "Runtime.enable"))))
(defun jade-chrome--enable-debugger ()
"Enable the debugger on the current tab."
(jade-chrome--send-request '((method . "Debugger.enable"))))
(defun jade-chrome--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-chrome--value result) error)))
(defun jade-chrome--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-chrome--get-completion-list-by-reference objectid prefix callback)
(jade-chrome--get-completion-list-by-type type prefix callback))))
(defun jade-chrome--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-chrome--send-request
`((method . "Runtime.callFunctionOn")
(params . ((objectId . ,objectid)
(functionDeclaration . ,jade-chrome-completion-function)
(returnByValue . t))))
(lambda (response)
(jade-chrome--handle-completion-list-response response prefix callback))))
(defun jade-chrome--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-chrome--get-completion-list-by-reference' for getting
completions using references to remote objects (including
arrays)."
(let ((expression (format "(%s)(\"%s\")" jade-chrome-completion-function type)))
(jade-chrome--send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,expression)
(returnByValue . t))))
(lambda (response)
(jade-chrome--handle-completion-list-response response prefix callback)))))
(defun jade-chrome--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-chrome--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-chrome--connected-p ()
"Return non-nil if the current connction is open."
(websocket-openp (map-elt jade-connection 'ws)))
(defun jade-chrome--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-chrome--preview result))
(description (jade-chrome--description result)))
`((objectid . ,objectid)
(description . ,description)
(type . ,type)
(preview . ,preview)
(value . ,value))))
(defun jade-chrome--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-chrome--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-chrome--preview-array preview)
(jade-chrome--preview-object preview))))
(defun jade-chrome--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-chrome--description prop)))
(map-elt preview 'properties)
", ")
(if (eq (map-elt preview 'lossless) :json-false)
", … }"
" }")))
(defun jade-chrome--preview-array (preview)
"Return a preview string from the elements of the array PREVIEW."
(concat " [ "
(mapconcat (lambda (prop)
(format "%s" (jade-chrome--description prop)))
(map-elt preview 'properties)
", ")
(if (eq (map-elt preview 'lossless) :json-false)
"… ]"
" ]")))
(defun jade-chrome--properties (result)
"Return a list of object properties built from RESULT."
(seq-map (lambda (prop)
`((name . ,(map-elt prop 'name))
(value . ,(jade-chrome--value (or (map-elt prop 'value)
(map-elt prop 'get))))))
result))
(defun jade-chrome--frames (list)
"Return a list of frames built from LIST."
(seq-map (lambda (frame)
`((scope-chain . ,(seq-map (lambda (scope)
`((object . ,(jade-chrome--value (map-elt scope 'object)))
(name . ,(map-elt scope 'name))
(type . ,(map-elt scope 'type))))
(map-elt frame 'scopeChain)))
(location . ,(map-elt frame 'location))))
list))
(let ((id 0))
(defun jade-chrome--next-request-id ()
"Return the next unique identifier to be used in a request."
(setq id (1+ id))
id))
(provide 'jade-chrome)
;;; jade.el ends here

+ 21
- 16
jade-debugger.el View File

@ -50,13 +50,15 @@
(declare 'jade-backend-debugger-get-script-source)
(defun jade-debugger-paused (frames)
(defun jade-debugger-paused (backend frames)
(setq jade-debugger-frames frames)
(jade-backend-get-script-source (jade-debugger-top-frame)
(jade-backend-get-script-source backend
(jade-debugger-top-frame)
(lambda (source)
(jade-debugger-switch-to-frame
(jade-debugger-top-frame)
(map-nested-elt source '(result scriptSource))))))
(let ((jade-backend backend))
(jade-debugger-switch-to-frame
(jade-debugger-top-frame)
(map-nested-elt source '(result scriptSource)))))))
(defun jade-debugger-resumed (&rest _args)
(let ((buf (jade-debugger-get-buffer)))
@ -103,33 +105,33 @@
(defun jade-debugger-step-into ()
(interactive)
(jade-backend-step-into))
(jade-backend-step-into jade-backend))
(defun jade-debugger-step-over ()
(interactive)
(jade-backend-step-over))
(jade-backend-step-over jade-backend))
(defun jade-debugger-step-out ()
(interactive)
(jade-backend-step-out))
(jade-backend-step-out jade-backend))
(defun jade-debugger-resume ()
(interactive)
(jade-backend-resume #'jade-debugger-resumed)
(jade-backend-resume jade-backend #'jade-debugger-resumed)
(kill-buffer (jade-debugger-locals-get-buffer))
(kill-buffer (jade-debugger-get-buffer)))
(defun jade-debugger-here ()
(interactive)
;; TODO
(jade-backend-continue-to-location '()))
(jade-backend-continue-to-location jade-backend '()))
(defun jade-debugger-get-buffer-create ()
"Create a debugger buffer unless one exists, and return it."
(let ((buf (jade-debugger-get-buffer)))
(unless buf
(setq buf (get-buffer-create (jade-debugger-buffer-name)))
(jade-debugger-setup-buffer buf jade-connection))
(jade-debugger-setup-buffer buf jade-backend jade-connection))
buf))
(defun jade-debugger-buffer-name ()
@ -138,10 +140,11 @@
(defun jade-debugger-get-buffer ()
(get-buffer (jade-debugger-buffer-name)))
(defun jade-debugger-setup-buffer (buffer connection)
(defun jade-debugger-setup-buffer (buffer backend connection)
(with-current-buffer buffer
(funcall jade-debugger-major-mode)
(setq-local jade-connection connection)
(setq-local jade-backend backend)
(jade-debugger-mode)
(read-only-mode)))
@ -175,6 +178,7 @@ Unless NO-POP is non-nil, pop the locals buffer."
(erase-buffer)))
(seq-do (lambda (scope)
(jade-backend-get-properties
jade-backend
(map-nested-elt scope '(object objectid))
(lambda (properties)
(jade-debugger-locals-render-properties properties scope no-pop))))
@ -218,15 +222,16 @@ Unless NO-POP is non-nil, pop the locals buffer."
"Create a locals buffer unless one exists, and return it."
(let ((buf (jade-debugger-locals-get-buffer)))
(unless buf
(setq buf (get-buffer-create (jade-debugger-locals-buffer-name)))
(jade-debugger-locals-setup-buffer buf jade-connection))
(setq buf (generate-new-buffer (jade-debugger-locals-buffer-name)))
(jade-debugger-locals-setup-buffer buf jade-backend jade-connection))
buf))
(defun jade-debugger-locals-setup-buffer (buffer connection)
(defun jade-debugger-locals-setup-buffer (buffer backend connection)
(with-current-buffer buffer
(jade-debugger-locals-mode)
(read-only-mode)
(setq-local jade-connection connection)))
(setq-local jade-connection connection)
(setq-local jade-backend backend)))
(defvar jade-debugger-locals-mode-map
(let ((map (copy-keymap jade-inspector-mode-map)))


+ 7
- 5
jade-inspector.el View File

@ -37,7 +37,8 @@
"Open an inspector on the remote object REFERENCE."
(let ((objectid (map-elt reference 'objectid)))
(if objectid
(jade-backend-get-properties objectid
(jade-backend-get-properties jade-backend
objectid
(lambda (properties)
(jade-inspector-push-to-history reference)
(jade-inspector-render-properties properties reference)))
@ -112,14 +113,15 @@ If no buffer exists, create one."
(let ((buf (jade-inspector-get-buffer)))
(unless buf
(setq buf (get-buffer-create (jade-inspector-buffer-name)))
(jade-inspector-setup-buffer buf jade-connection))
(jade-inspector-setup-buffer buf jade-backend jade-connection))
buf))
(defun jade-inspector-setup-buffer (buffer connection)
"Setup the inspector BUFFER for CONNECTION."
(defun jade-inspector-setup-buffer (buffer backend connection)
"Setup the inspector BUFFER for BACKEND and CONNECTION."
(with-current-buffer buffer
(jade-inspector-mode)
(setq-local jade-connection connection)))
(setq-local jade-connection connection)
(setq-local jade-backend backend)))
(defun jade-inspector-buffer-name ()
"Return the inspector buffer name for the current connection."


+ 14
- 15
jade-repl.el View File

@ -54,15 +54,13 @@
(prog1 (progn . ,body)
(set-marker ,marker ,pos)))))
(defun jade-repl-get-buffer-create (connection)
(defun jade-repl-get-buffer-create (backend connection)
"Return a REPL buffer for CONNECTION.
If no buffer exists, create one."
(let* ((ws (map-elt connection 'ws))
(url (map-elt connection 'url))
(buf (get-buffer (jade-repl-buffer-name url))))
(unless buf
(setq buf (get-buffer-create (jade-repl-buffer-name url)))
(jade-repl-setup-buffer buf connection))
(buf (get-buffer-create (jade-repl-buffer-name url))))
(jade-repl-setup-buffer buf backend connection)
buf))
(defun jade-repl-get-buffer ()
@ -74,19 +72,19 @@ If no buffer exists, create one."
If URL is nil, use the current connection."
(concat "*JS REPL " (or url (map-elt jade-connection 'url)) "*"))
(defun jade-repl-setup-buffer (buffer connection)
"Setup the REPL BUFFER for CONNECTION."
(defun jade-repl-setup-buffer (buffer backend connection)
"Setup the REPL BUFFER for BACKEND and CONNECTION."
(with-current-buffer buffer
(jade-repl-mode)
(setq-local jade-connection connection)
;; (cursor-intangible-mode)
(setq-local jade-backend backend)
(jade-repl-setup-markers)
(jade-repl-mark-output-start)
(jade-repl-mark-input-start)
(jade-repl-insert-prompt))
(jade-repl-emit-console-message
(format "Welcome to Jade!\nConnected to %s\n"
(map-elt jade-connection 'url))))
(jade-repl-insert-prompt)
(jade-repl-emit-console-message
(format "Welcome to Jade!\nConnected to %s\n"
(map-elt jade-connection 'url)))))
(defun jade-repl-setup-markers ()
"Setup the initial markers for the current REPL buffer."
@ -139,7 +137,8 @@ If URL is nil, use the current connection."
(defun jade-repl-inspect ()
"Inspect the result of the evaluation of the input at point."
(interactive)
(jade-backend-evaluate (jade-repl--input-content)
(jade-backend-evaluate jade-backend
(jade-repl--input-content)
(lambda (result error)
(when error
(jade-repl-emit-value result error))
@ -158,7 +157,7 @@ If URL is nil, use the current connection."
(defun jade-repl-evaluate (string)
"Evaluate STRING in the browser tab and emit the output."
(push string jade-repl-history)
(jade-backend-evaluate string #'jade-repl-emit-value)
(jade-backend-evaluate jade-backend string #'jade-repl-emit-value)
;; move the output markers so that output is put after the current prompt
(save-excursion
(goto-char (point-max))
@ -266,7 +265,7 @@ See `company-backends' for more info about COMMAND and ARG."
Evaluate CALLBACK with the completion candidates."
(let ((expression (buffer-substring-no-properties jade-repl-input-start-marker
(point-max-marker))))
(jade-backend-get-completions expression arg callback)))
(jade-backend-get-completions jade-backend expression arg callback)))
(defun jade-repl-company-prefix ()
"Prefix for company."


+ 2
- 422
jade.el View File

@ -24,428 +24,8 @@
;;; Code:
(require 'websocket)
(require 'url)
(require 'json)
(require 'map)
(require 'seq)
(require 'jade-repl)
(require 'jade-debugger)
(defvar jade-connections (list) "List of connections.")
(defvar jade-connection nil
"Current connection to the browser tab.
A connection should be an alist with the following required keys:
`backend' and `url'. Other backend-specific keys might be used
by backends.")
(make-variable-buffer-local 'jade-connection)
(defvar jade-backend-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;}")
(defun jade-connect (host port)
"Open a connection to a browser tab on HOST:PORT."
(interactive (list (read-from-minibuffer "Host: " "127.0.0.1")
(read-from-minibuffer "Port: " "9222")))
(jade-backend-get-tabs-data host port #'jade-backend-connect-to-tab))
(defun jade-quit ()
"Close the current connection and kill its REPL buffer if any."
(interactive)
(unless jade-connection
(user-error "No active connection to close"))
(when (y-or-n-p (format "Do you really want to close the connection to %s ? "
(map-elt jade-connection 'url)))
(when (jade-connected-p)
(websocket-close (map-elt jade-connection 'ws)))
(map-put jade-connection 'callbacks nil)
(setq jade-connections (remq jade-connection jade-connections))
(kill-buffer (jade-repl-get-buffer))))
(defun jade-backend-get-tabs-data (host port callback)
"Get the list of open tabs on HOST:PORT and evaluate CALLBACK with it."
(url-retrieve (format "http://%s:%s/json" host port)
(lambda (status)
;; TODO: handle errors
(funcall callback (jade-backend--read-tab-data)))))
(defun jade-backend-connect-to-tab (tabs)
"Ask the user for a tab in the list TABS and connects to it."
(let* ((titles (seq-map (lambda (tab)
(map-elt tab 'title))
tabs))
(title (completing-read "Tab: " titles nil t)))
(jade-backend--open-ws-connection (seq-find (lambda (tab)
(string= (map-elt tab 'title) title))
tabs))))
(defun jade-backend--read-tab-data ()
"Return the JSON tabs data in the current buffer."
(when (save-match-data
(looking-at "^HTTP/1\\.1 200 OK$"))
(goto-char (point-min))
(search-forward "\n\n")
(delete-region (point-min) (point))
(json-read)))
(defun jade-backend--open-ws-connection (tab)
"Open a websocket connection to the `webSocketDebuggerUrl' of TAB.
If TAB does not have a `webSocketDebuggerUrl', throw a user
error. This might happen when trying to connect to a tab twice,
or if an inspector is open on that tab."
(let ((url (map-elt tab 'url))
(debugger-url (map-elt tab 'webSocketDebuggerUrl)))
(unless debugger-url
(user-error "Cannot open connection, another devtools instance might be open"))
(websocket-open debugger-url
:on-open (lambda (ws) (jade-backend-handle-ws-open ws url))
:on-message #'jade-backend-handle-ws-message
:on-close #'jade-backend-handle-ws-closed
:on-error #'jade-backend-handle-ws-error)))
(defun jade-backend--make-connection (ws url)
"Return a new connection for WS and URL."
(let ((connection `((ws . ,ws)
(url . ,url)
(backend . chrome)
(callbacks . ,(make-hash-table)))))
(add-to-list 'jade-connections connection)
connection))
(defun jade-backend--callbacks ()
"Return the callbacks associated with the current connection."
(map-elt jade-connection 'callbacks))
(defun jade-backend--connection-for-ws (ws)
"Return the connection associated with the websocket WS."
(seq-find (lambda (connection)
(eq (map-elt connection 'ws) ws))
jade-connections))
(defun jade-backend-handle-ws-open (ws url)
(let* ((connection (jade-backend--make-connection ws url))
(jade-connection connection))
(jade-backend-enable-tools)
(switch-to-buffer (jade-repl-get-buffer-create connection))))
(defun jade-backend-handle-ws-message (ws frame)
(let* ((jade-connection (jade-backend--connection-for-ws ws))
(message (jade-backend--read-ws-message frame))
(error (map-elt message 'error))
(method (map-elt message 'method))
(request-id (map-elt message 'id))
(callback (map-elt (jade-backend--callbacks) request-id)))
(cond
(error (message (map-elt error 'message)))
(request-id (when callback
(funcall callback message)))
(t (pcase method
("Console.messageAdded" (jade-backend-handle-console-message message))
("Debugger.paused" (jade-backend-handle-debugger-paused message))
("Debugger.resumed" (jade-backend-handle-debugger-resumed message)))))))
(defun jade-backend-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-backend-handle-debugger-paused (message)
(let ((frames (map-nested-elt message '(params callFrames))))
(jade-debugger-paused (jade-backend-frames frames))))
(defun jade-backend-handle-debugger-resumed (_message)
(jade-debugger-resumed))
(defun jade-backend-handle-ws-closed (_ws)
)
(defun jade-backend-handle-ws-error (ws action error)
(message "WS Error! %s %s" action error))
(defun jade-backend-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-connected-p))
(jade-repl-emit-console-message "Socket connection closed" "error"))
(let ((id (jade-next-request-id))
(callbacks (jade-backend--callbacks)))
(when callback
(map-put callbacks id callback))
(websocket-send-text (map-elt jade-connection 'ws)
(json-encode (cons `(id . ,id) request)))))
(defun jade-backend--read-ws-message (frame)
(with-temp-buffer
(insert (websocket-frame-payload frame))
(goto-char (point-min))
(json-read)))
(defun jade-backend-enable-tools ()
"Enable developer tools for the current tab.
There is currently no support for the DOM inspector and network
inspectors."
(jade-backend-enable-console)
(jade-backend-enable-runtime)
(jade-backend-enable-debugger))
(defun jade-backend-enable-console ()
"Enable the console on the current tab."
(jade-backend-send-request '((method . "Console.enable"))))
(defun jade-backend-enable-runtime ()
"Enable the runtime on the current tab."
(jade-backend-send-request '((method . "Runtime.enable"))))
(defun jade-backend-enable-debugger ()
"Enable the debugger on the current tab."
(jade-backend-send-request '((method . "Debugger.enable"))))
(defun jade-backend-evaluate (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-backend-send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,string)
(generatePreview . t))))
(lambda (response)
(jade-backend-handle-evaluation-response response callback))))
(defun jade-backend-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-backend-value result) error)))
(defun jade-backend-get-completions (expression prefix callback)
"Get the completion candidates for EXPRESSION that match PREFIX.
Evaluate CALLBACK on the filtered candidates."
(let ((expression (jade-backend-completion-expression expression)))
(jade-backend-send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,expression)
(objectGroup . "completion"))))
(lambda (response)
(jade-backend-handle-completions-response response prefix callback)))))
(defun jade-backend-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-backend-get-completion-list-by-reference objectid prefix callback)
(jade-backend-get-completion-list-by-type type prefix callback))))
(defun jade-backend-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-backend-send-request
`((method . "Runtime.callFunctionOn")
(params . ((objectId . ,objectid)
(functionDeclaration . ,jade-backend-completion-function)
(returnByValue . t))))
(lambda (response)
(jade-backend-handle-completion-list-response response prefix callback))))
(defun jade-backend-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-backend-get-completion-list-by-reference' for getting
completions using references to remote objects (including
arrays)."
(let ((expression (format "(%s)(\"%s\")" jade-backend-completion-function type)))
(jade-backend-send-request
`((method . "Runtime.evaluate")
(params . ((expression . ,expression)
(returnByValue . t))))
(lambda (response)
(jade-backend-handle-completion-list-response response prefix callback)))))
(defun jade-backend-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-backend-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)))))
(defun jade-backend-get-properties (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-backend-send-request
`((method . "Runtime.getProperties")
(params . ((objectId . ,reference)
(ownProperties . ,(not all-properties)))))
(lambda (response)
(funcall callback
(jade-backend-properties
(map-nested-elt response '(result result)))))))
(defun jade-backend-get-script-source (frame callback)
(let ((script-id (map-nested-elt frame '(location scriptId))))
(jade-backend-send-request
`((method . "Debugger.getScriptSource")
(params . ((scriptId . ,script-id))))
callback)))
(defun jade-backend-resume (&optional callback)
"Resume the debugger and evaluate CALLBACK if non-nil."
(jade-backend-send-request
`((method . "Debugger.resume"))
callback))
(defun jade-backend-step-into (&optional callback)
"Step into the current stack frame and evaluate CALLBACK if non-nil."
(jade-backend-send-request
`((method . "Debugger.stepInto"))
callback))
(defun jade-backend-step-out (&optional callback)
"Step out the current stack frame and evaluate CALLBACK if non-nil."
(jade-backend-send-request
`((method . "Debugger.stepOut"))
callback))
(defun jade-backend-step-over (&optional callback)
"Step over the current stack frame and evaluate CALLBACK if non-nil."
(jade-backend-send-request
`((method . "Debugger.stepOver"))
callback))
(defun jade-backend-continue-to-location (location &optional callback)
"Continue to LOCATION and evaluate CALLBACK if non-nil.
Location should be an alist with a `column' and `row' key."
(jade-backend-send-request
`((method . "Debugger.continueToLocation")
(params . ((location . ,location))))
callback))
(defun jade-backend-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-backend-preview result))
(description (jade-backend-description result)))
`((objectid . ,objectid)
(description . ,description)
(type . ,type)
(preview . ,preview)
(value . ,value))))
(defun jade-backend-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-backend-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-backend--preview-array preview)
(jade-backend--preview-object preview))))
(defun jade-backend--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-backend-description prop)))
(map-elt preview 'properties)
", ")
(if (eq (map-elt preview 'lossless) :json-false)
", … }"
" }")))
(defun jade-backend--preview-array (preview)
"Return a preview string from the elements of the array PREVIEW."
(concat " [ "
(mapconcat (lambda (prop)
(format "%s" (jade-backend-description prop)))
(map-elt preview 'properties)
", ")
(if (eq (map-elt preview 'lossless) :json-false)
"… ]"
" ]")))
(defun jade-backend-properties (result)
"Return a list of object properties built from RESULT."
(seq-map (lambda (prop)
`((name . ,(map-elt prop 'name))
(value . ,(jade-backend-value (or (map-elt prop 'value)
(map-elt prop 'get))))))
result))
(defun jade-backend-frames (list)
"Return a list of frames built from LIST."
(seq-map (lambda (frame)
`((scope-chain . ,(seq-map (lambda (scope)
`((object . ,(jade-backend-value (map-elt scope 'object)))
(name . ,(map-elt scope 'name))
(type . ,(map-elt scope 'type))))
(map-elt frame 'scopeChain)))
(location . ,(map-elt frame 'location))))
list))
(defun jade-backend-object-reference-p (value)
"Return non-nil if VALUE is a reference to a remote object."
(map-elt value 'objectid))
(defun jade-connected-p ()
"Return non-nil if the current connction is open."
(websocket-openp (map-elt jade-connection 'ws)))
(let ((id 0))
(defun jade-next-request-id ()
"Return the next unique identifier to be used in a request."
(setq id (1+ id))
id))
(require 'jade-backend)
(require 'jade-chrome)
(provide 'jade)
;;; jade.el ends here

Loading…
Cancel
Save