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.

668 lines
28 KiB

  1. ;;; indium-webkit.el --- Webkit/Blink backend for indium -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2016-2017 Nicolas Petton
  3. ;; Author: Nicolas Petton <nicolas@petton.fr>
  4. ;; Keywords: tools, javascript
  5. ;; This program is free software; you can redistribute it and/or modify
  6. ;; it under the terms of the GNU General Public License as published by
  7. ;; the Free Software Foundation, either version 3 of the License, or
  8. ;; (at your option) any later version.
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; Indium backend implementation for Webkit and Blink. Connection is handled in
  17. ;; indium-chrome.el. This backend currently supports the REPL, code completion,
  18. ;; object inspection and the debugger.
  19. ;;
  20. ;; The protocol supports both Chrome/Chromium and Nodejs.
  21. ;; The protocol is documented at
  22. ;; https://chromedevtools.github.io/debugger-protocol-viewer/1-2/.
  23. ;;; Code:
  24. (require 'websocket)
  25. (require 'json)
  26. (require 'map)
  27. (require 'seq)
  28. (require 'indium-backend)
  29. (require 'indium-structs)
  30. (require 'indium-repl)
  31. (require 'indium-debugger)
  32. (require 'indium-workspace)
  33. (require 'indium-script)
  34. (defvar indium-webkit-cache-disabled nil
  35. "Network cache disabled state. If non-nil disable cache when Indium starts.")
  36. (defvar indium-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;}")
  37. (indium-register-backend 'webkit)
  38. (defun indium-connection-ws (connection)
  39. "Return the websocket associated to CONNECTION."
  40. (map-elt (indium-connection-props connection) 'ws))
  41. (cl-defmethod (setf indium-connection-ws) (ws (connection indium-connection))
  42. (map-put (indium-connection-props connection) 'ws ws))
  43. (defun indium-connection-nodejs-p (connection)
  44. "Return non-nil if CONNECTION is for Nodejs."
  45. (and connection
  46. (map-elt (indium-connection-props connection) 'nodejs)))
  47. (cl-defmethod indium-backend-active-connection-p ((_backend (eql webkit)))
  48. "Return non-nil if the current connection is active."
  49. (when-indium-connected
  50. (websocket-openp (indium-connection-ws indium-current-connection))))
  51. (cl-defmethod indium-backend-close-connection ((_backend (eql webkit)))
  52. "Close the websocket associated with the current connection."
  53. (websocket-close (indium-connection-ws indium-current-connection)))
  54. (cl-defmethod indium-backend-reconnect ((_backend (eql webkit)))
  55. (indium-webkit--open-ws-connection
  56. (indium-current-connection-url)
  57. (websocket-url (indium-connection-ws indium-current-connection))
  58. ;; close all buffers related to the closed
  59. ;; connection the first
  60. #'indium-quit))
  61. (cl-defmethod indium-backend-evaluate ((_backend (eql webkit)) string &optional callback)
  62. "Evaluate STRING then call CALLBACK.
  63. CALLBACK is called with two arguments, the value returned by the
  64. evaluation and non-nil if the evaluation threw an error."
  65. (let* ((current-frame (indium-current-connection-current-frame))
  66. (callFrameId (and current-frame (indium-frame-id current-frame))))
  67. (indium-webkit--send-request
  68. `((method . ,(if callFrameId
  69. "Debugger.evaluateOnCallFrame"
  70. "Runtime.evaluate"))
  71. (params . ((expression . ,string)
  72. (callFrameId . ,callFrameId)
  73. (generatePreview . t))))
  74. (when callback
  75. (lambda (response)
  76. (indium-webkit--handle-evaluation-response response callback))))))
  77. (cl-defmethod indium-backend-get-completions ((_backend (eql webkit)) expression prefix callback)
  78. "Get the completion candidates for EXPRESSION that match PREFIX.
  79. Evaluate CALLBACK on the filtered candidates."
  80. (let ((expression (indium-webkit--completion-expression expression)))
  81. (indium-webkit--send-request
  82. `((method . "Runtime.evaluate")
  83. (params . ((expression . ,expression)
  84. (objectGroup . "completion"))))
  85. (lambda (response)
  86. (indium-webkit--handle-completions-response response prefix callback)))))
  87. (cl-defmethod indium-backend-add-breakpoint ((_backend (eql webkit)) location &optional callback condition)
  88. "Request the addition of a breakpoint.
  89. The breakpoint is set at LOCATION. When CALLBACK is
  90. non-nil, evaluate it with the breakpoint's location and id."
  91. (let ((url (indium-workspace-make-url (indium-location-file location)))
  92. (condition (or condition "")))
  93. (unless url
  94. (user-error "No URL associated with the current buffer. Setup an Indium workspace first"))
  95. (indium-webkit--send-request
  96. `((method . "Debugger.setBreakpointByUrl")
  97. (params . ((url . ,url)
  98. (lineNumber . ,(indium-location-line location))
  99. (columnNumber . ,(indium-location-column location))
  100. (condition . ,condition))))
  101. (lambda (response)
  102. (let* ((breakpoint (map-elt response 'result))
  103. (id (map-elt breakpoint 'breakpointId))
  104. (locations (map-elt breakpoint 'locations))
  105. (line (map-elt (seq--elt-safe locations 0) 'lineNumber)))
  106. (when line
  107. (indium-backend-register-breakpoint id line (indium-location-file location) condition))
  108. (when callback
  109. (unless line
  110. (message "Cannot get breakpoint location"))
  111. (funcall callback id)))))))
  112. (cl-defmethod indium-backend-remove-breakpoint ((_backend (eql webkit)) id)
  113. "Request the removal of the breakpoint with id ID."
  114. (indium-webkit--send-request
  115. `((method . "Debugger.removeBreakpoint")
  116. (params . ((breakpointId . ,id))))
  117. (lambda (_response)
  118. (indium-backend-unregister-breakpoint id))))
  119. (cl-defmethod indium-backend-deactivate-breakpoints ((_backend (eql webkit)))
  120. "Deactivate all breakpoints.
  121. The runtime will not pause on any breakpoint."
  122. (indium-webkit--send-request
  123. `((method . "Debugger.setBreakpointsActive")
  124. (params . ((active . :json-false))))))
  125. (cl-defmethod indium-backend-activate-breakpoints ((_backend (eql webkit)))
  126. "Deactivate all breakpoints.
  127. The runtime will not pause on any breakpoint."
  128. (indium-webkit--send-request
  129. `((method . "Debugger.setBreakpointsActive")
  130. (params . ((active . t))))))
  131. (cl-defmethod indium-backend-get-properties ((_backend (eql webkit)) reference &optional callback all-properties)
  132. "Get the properties of the remote object represented by REFERENCE.
  133. CALLBACK is evaluated with the list of properties.
  134. If ALL-PROPERTIES is non-nil, get all the properties from the
  135. prototype chain of the remote object."
  136. (indium-webkit--send-request
  137. `((method . "Runtime.getProperties")
  138. (params . ((objectId . ,reference)
  139. (generatePreview . t)
  140. (ownProperties . ,(or all-properties :json-false)))))
  141. (lambda (response)
  142. (funcall callback
  143. (indium-webkit--properties
  144. (map-nested-elt response '(result result)))))))
  145. (cl-defmethod indium-backend-set-script-source ((_backend (eql webkit)) url source &optional callback)
  146. (when-let ((script (indium-script-find-from-url url)))
  147. (indium-webkit--send-request
  148. `((method . "Runtime.compileScript")
  149. (params . ((expression . ,source)
  150. (sourceURL . ,url)
  151. (persistScript . :json-false))))
  152. (lambda (_)
  153. (indium-webkit--send-request
  154. `((method . "Debugger.setScriptSource")
  155. (params . ((scriptId . ,(indium-script-id script))
  156. (scriptSource . ,source))))
  157. (lambda (_)
  158. (when callback
  159. (funcall callback))))))))
  160. (cl-defmethod indium-backend-get-script-source ((_backend (eql webkit)) frame callback)
  161. (let ((script (indium-frame-script frame)))
  162. (indium-webkit--send-request
  163. `((method . "Debugger.getScriptSource")
  164. (params . ((scriptId . ,(indium-script-id script)))))
  165. callback)))
  166. (cl-defmethod indium-backend-resume ((_backend (eql webkit)) &optional callback)
  167. "Resume the debugger and evaluate CALLBACK if non-nil."
  168. (indium-webkit--send-request
  169. `((method . "Debugger.resume"))
  170. callback))
  171. (cl-defmethod indium-backend-step-into ((_backend (eql webkit)) &optional callback)
  172. "Step into the current stack frame and evaluate CALLBACK if non-nil."
  173. (indium-webkit--send-request
  174. `((method . "Debugger.stepInto"))
  175. callback))
  176. (cl-defmethod indium-backend-step-out ((_backend (eql webkit)) &optional callback)
  177. "Step out the current stack frame and evaluate CALLBACK if non-nil."
  178. (indium-webkit--send-request
  179. `((method . "Debugger.stepOut"))
  180. callback))
  181. (cl-defmethod indium-backend-step-over ((_backend (eql webkit)) &optional callback)
  182. "Step over the current stack frame and evaluate CALLBACK if non-nil."
  183. (indium-webkit--send-request
  184. `((method . "Debugger.stepOver"))
  185. callback))
  186. (cl-defmethod indium-backend-continue-to-location ((_backend (eql webkit)) location &optional callback)
  187. "Continue to LOCATION and evaluate CALLBACK if non-nil.
  188. Location should be an alist with a `limeNumber' and `scriptId' key."
  189. (indium-webkit--send-request
  190. `((method . "Debugger.continueToLocation")
  191. (params . ((location . ,(indium-webkit--convert-to-webkit-location location)))))
  192. callback))
  193. (defun indium-webkit-set-overlay-message (string)
  194. "Set the debugger page overlay to STRING."
  195. (indium-webkit--send-request
  196. `((method . "Page.configureOverlay")
  197. (params . ((suspended . :json-false)
  198. (message . ,string))))
  199. #'ignore))
  200. (defun indium-webkit-remove-overlay-message ()
  201. "Remove any overlay message displayed on the page."
  202. (indium-webkit--send-request
  203. `((method . "Page.configureOverlay")
  204. (params . ((suspended . :json-false))))))
  205. (defun indium-webkit-set-pause-on-exceptions (state)
  206. "Defines on which STATE to pause.
  207. Can be set to stop on all exceptions, uncaught exceptions or no
  208. exceptions. Initial pause on exceptions state is set by Indium to
  209. `\"uncaught\"'.
  210. Allowed states: `\"none\"', `\"uncaught\"', `\"all\"'."
  211. (interactive (list (completing-read "Pause on exceptions: "
  212. '("none" "uncaught" "all")
  213. nil
  214. t)))
  215. (indium-webkit--send-request `((method . "Debugger.setPauseOnExceptions")
  216. (params . ((state . ,state))))))
  217. (defun indium-webkit--set-cache-disabled (disabled)
  218. "Toggle ignoring cache for each request. If DISABLED, cache will not be used."
  219. (indium-webkit--send-request `((method . "Network.setCacheDisabled")
  220. (params . ((cacheDisabled . ,(if disabled t :json-false)))))))
  221. (defun indium-webkit-enable-cache ()
  222. "Enabled network cache for each request."
  223. (interactive)
  224. (setq indium-webkit-cache-disabled nil)
  225. (indium-webkit--set-cache-disabled nil))
  226. (defun indium-webkit-disable-cache ()
  227. "Disable network cache for each request."
  228. (interactive)
  229. (setq indium-webkit-cache-disabled t)
  230. (indium-webkit--set-cache-disabled t))
  231. (defun indium-webkit--open-ws-connection (url websocket-url &optional on-open nodejs workspace)
  232. "Open a websocket connection to URL using WEBSOCKET-URL.
  233. Evaluate ON-OPEN when the websocket is open, before setting up
  234. the connection and buffers.
  235. In a Chrom{e|ium} session, URL corresponds to the url of a tab,
  236. and WEBSOCKET-URL to its associated `webSocketDebuggerUrl'.
  237. If NODEJS is non-nil, add a `nodejs' flag to the
  238. `indium-current-connection' to handle special cases.
  239. If WORKSPACE is non-nil, make it the workspace directory for that
  240. connection."
  241. (unless websocket-url
  242. (user-error "Cannot open connection, another devtools instance might be open"))
  243. (websocket-open websocket-url
  244. :on-open (lambda (ws)
  245. (when on-open
  246. (funcall on-open))
  247. (indium-webkit--handle-ws-open ws url nodejs workspace))
  248. :on-message #'indium-webkit--handle-ws-message
  249. :on-close #'indium-webkit--handle-ws-closed
  250. :on-error #'indium-webkit--handle-ws-error))
  251. (defun indium-webkit--make-connection (ws url &optional nodejs)
  252. "Return a new connection for WS and URL.
  253. If NODEJS is non-nil, add a `nodejs' extra property to the
  254. connection."
  255. (let ((conn (make-indium-connection
  256. :backend 'webkit
  257. :url url)))
  258. (setf (indium-connection-ws conn) ws)
  259. (when nodejs
  260. (map-put (indium-connection-props conn) 'nodejs t))
  261. conn))
  262. (defun indium-webkit--handle-ws-open (ws url nodejs workspace)
  263. "Setup indium for a new connection for the websocket WS.
  264. URL points to the browser tab.
  265. If NODEJS is non-nil, set an extra property in the connection.
  266. If WORKSPACE is non-nil, make it the workspace used for the connection."
  267. (setq indium-current-connection (indium-webkit--make-connection ws url nodejs))
  268. (indium-webkit--enable-tools)
  269. (switch-to-buffer (indium-repl-buffer-create))
  270. (when workspace (cd workspace))
  271. (indium-breakpoint-restore-breakpoints)
  272. (run-hooks 'indium-connection-open-hook))
  273. (defun indium-webkit--handle-ws-message (_ws frame)
  274. "Handle a websocket message FRAME."
  275. (let* ((message (indium-webkit--read-ws-message frame))
  276. (error (map-elt message 'error))
  277. (method (map-elt message 'method))
  278. (request-id (map-elt message 'id))
  279. (callback (map-elt (indium-current-connection-callbacks)
  280. request-id)))
  281. (cond
  282. (error (message (map-elt error 'message)))
  283. (request-id (when callback
  284. (funcall callback message)))
  285. (t (pcase method
  286. ("Inspector.detached" (indium-webkit--handle-inspector-detached message))
  287. ("Log.entryAdded" (indium-webkit--handle-log-entry message))
  288. ("Runtime.consoleAPICalled" (indium-webkit--handle-console-message message))
  289. ("Runtime.exceptionThrown" (indium-webkit--handle-exception-thrown message))
  290. ("Debugger.paused" (indium-webkit--handle-debugger-paused message))
  291. ("Debugger.scriptParsed" (indium-webkit--handle-script-parsed message))
  292. ("Debugger.resumed" (indium-webkit--handle-debugger-resumed message)))))))
  293. (defun indium-webkit--handle-inspector-detached (message)
  294. "Handle a closed connection event.
  295. MESSAGE explains why the connection has been closed."
  296. (let ((msg (map-nested-elt message '(params reason))))
  297. (indium-backend-close-connection 'webkit)
  298. (message "Indium connection closed: %s" msg)))
  299. (defun indium-webkit--handle-log-entry (message)
  300. "Handle a log entry event with MESSAGE."
  301. (let ((entry (map-nested-elt message '(params entry))))
  302. ;; unify console message and entry logs
  303. (map-put entry 'line (map-elt entry 'lineNumber))
  304. (indium-repl-emit-console-message entry)))
  305. (defun indium-webkit--handle-console-message (message)
  306. "Handle a console message event with MESSAGE."
  307. (let* ((msg (map-elt message 'params))
  308. (args (map-elt msg 'args)))
  309. (setf (map-elt msg 'values) (seq-map #'indium-webkit--value args))
  310. (indium-repl-emit-console-message msg)))
  311. (defun indium-webkit--handle-exception-thrown (message)
  312. "Handle an exception event MESSAGE."
  313. (let ((exception (map-nested-elt message '(params exceptionDetails))))
  314. (indium-repl-emit-console-message (indium-webkit--exception exception) t)))
  315. (defun indium-webkit--handle-debugger-paused (message)
  316. "Handle a debugger paused event with MESSAGE."
  317. (let* ((frames (map-nested-elt message '(params callFrames)))
  318. (exception (equal (map-nested-elt message '(params reason)) "exception"))
  319. (reason (if exception "Exception occured" "Breakpoint hit"))
  320. (description (map-nested-elt message '(params data description))))
  321. (unless (indium-connection-nodejs-p indium-current-connection)
  322. (indium-webkit-set-overlay-message "Paused in Emacs debugger"))
  323. (indium-debugger-paused (indium-webkit--frames frames) reason description)))
  324. (defun indium-webkit--handle-debugger-resumed (_message)
  325. "Handle a runtime execution resumed event."
  326. (unless (indium-connection-nodejs-p indium-current-connection)
  327. (indium-webkit-remove-overlay-message))
  328. (indium-debugger-resumed))
  329. (defun indium-webkit--handle-script-parsed (message)
  330. "Handle a script parsed event with MESSAGE."
  331. (let* ((id (map-nested-elt message '(params scriptId)))
  332. (url (map-nested-elt message '(params url)))
  333. (sourcemap-url (map-nested-elt message '(params sourceMapURL))))
  334. (indium-script-add-script-parsed id url sourcemap-url)))
  335. (defun indium-webkit--handle-ws-closed (_ws)
  336. "Cleanup function called when the connection socket is closed."
  337. (run-hooks 'indium-connection-closed-hook)
  338. (indium-repl--handle-connection-closed))
  339. (defun indium-webkit--handle-ws-error (_ws _action error)
  340. "Display an error message for an exception in a websocket callback handling.
  341. ERROR should be a description of the exception."
  342. (message "Exception in websocket callback! %s" error))
  343. (defun indium-webkit--send-request (request &optional callback)
  344. "Send REQUEST to the current connection.
  345. Evaluate CALLBACK with the response.
  346. If the current connection is closed, display a message."
  347. (if (indium-webkit--connected-p)
  348. (let ((id (indium-webkit--next-request-id)))
  349. (when callback
  350. (map-put (indium-current-connection-callbacks)
  351. id
  352. callback))
  353. (websocket-send-text (indium-connection-ws indium-current-connection)
  354. (json-encode (cons `(id . ,id) request))))
  355. (message "Socket connection closed")))
  356. (defun indium-webkit--read-ws-message (frame)
  357. "Parse the payload from the websocket FRAME."
  358. (json-read-from-string (websocket-frame-payload frame)))
  359. (defun indium-webkit--enable-tools ()
  360. "Enable developer tools for the current tab.
  361. There is currently no support for the DOM inspector and network
  362. inspectors."
  363. (indium-webkit--enable-runtime)
  364. (unless (indium-connection-nodejs-p indium-current-connection)
  365. (indium-webkit--enable-page)
  366. (indium-webkit--enable-network)
  367. (indium-webkit--enable-log))
  368. (indium-webkit--enable-debugger))
  369. (defun indium-webkit--enable-log ()
  370. "Enable the log on the current tab."
  371. (indium-webkit--send-request '((method . "Log.enable"))))
  372. (defun indium-webkit--enable-page ()
  373. "Enable the page API on the current tab."
  374. (indium-webkit--send-request '((method . "Page.enable"))))
  375. (defun indium-webkit--enable-runtime ()
  376. "Enable the runtime on the current tab."
  377. (indium-webkit--send-request '((method . "Runtime.enable")))
  378. (indium-webkit--send-request '((method . "Runtime.runIfWaitingForDebugger"))))
  379. (defun indium-webkit--enable-network ()
  380. "Enable the runtime on the current tab."
  381. (indium-webkit--send-request '((method . "Network.enable"))
  382. (lambda (_)
  383. (when indium-webkit-cache-disabled
  384. (indium-webkit--set-cache-disabled t)))))
  385. (defun indium-webkit--enable-debugger ()
  386. "Enable the debugger on the current tab."
  387. (indium-webkit--send-request '((method . "Debugger.enable"))
  388. (lambda (&rest _)
  389. (indium-webkit-set-pause-on-exceptions "uncaught"))))
  390. (defun indium-webkit--handle-evaluation-response (response callback)
  391. "Get the result of an evaluation in RESPONSE and evaluate CALLBACK with it."
  392. (let* ((result (map-nested-elt response '(result result)))
  393. (error (eq (map-nested-elt response '(result wasThrown)) t)))
  394. (funcall callback (indium-webkit--value result) error)))
  395. (defun indium-webkit--handle-completions-response (response prefix callback)
  396. "Request a completion list for the object in RESPONSE.
  397. The completion list is filtered using the PREFIX string, then
  398. CALLBACK is evaluated with it."
  399. (let ((objectid (map-nested-elt response '(result result objectId)))
  400. (type (map-nested-elt response '(result result type))))
  401. (if objectid
  402. (indium-webkit--get-completion-list-by-reference objectid prefix callback)
  403. (indium-webkit--get-completion-list-by-type type prefix callback))))
  404. (defun indium-webkit--get-completion-list-by-reference (objectid prefix callback)
  405. "Request the completion list for a remote object referenced by OBJECTID.
  406. The completion list is filtered using the PREFIX string, then
  407. CALLBACK is evaluated with it."
  408. (indium-webkit--send-request
  409. `((method . "Runtime.callFunctionOn")
  410. (params . ((objectId . ,objectid)
  411. (functionDeclaration . ,indium-webkit-completion-function)
  412. (returnByValue . t))))
  413. (lambda (response)
  414. (indium-webkit--handle-completion-list-response response prefix callback))))
  415. (defun indium-webkit--get-completion-list-by-type (type prefix callback)
  416. "Request the completion list for an object of type TYPE.
  417. The completion list is filtered using the PREFIX string, then
  418. CALLBACK is evaluated with it.
  419. This method is used for strings, numbers and booleans. See
  420. `indium-webkit--get-completion-list-by-reference' for getting
  421. completions using references to remote objects (including
  422. arrays)."
  423. (let ((expression (format "(%s)(\"%s\")" indium-webkit-completion-function type)))
  424. (indium-webkit--send-request
  425. `((method . "Runtime.evaluate")
  426. (params . ((expression . ,expression)
  427. (returnByValue . t))))
  428. (lambda (response)
  429. (indium-webkit--handle-completion-list-response response prefix callback)))))
  430. (defun indium-webkit--completion-expression (string)
  431. "Return the completion expression to be requested from STRING."
  432. (if (string-match-p "\\." string)
  433. (replace-regexp-in-string "\\.[^\\.]*$" "" string)
  434. "this"))
  435. (defun indium-webkit--handle-completion-list-response (response prefix callback)
  436. "Filter candidates from RESPONSE matching PREFIX.
  437. Evaluate CALLBACK on the result."
  438. (let ((candidates (map-nested-elt response '(result result value))))
  439. (funcall callback (seq-filter (lambda (candidate)
  440. (string-prefix-p prefix candidate))
  441. (seq-map (lambda (candidate)
  442. (symbol-name (car candidate)))
  443. candidates)))))
  444. (cl-defmethod indium-webkit--connected-p ()
  445. "Return non-nil if the current connection is open."
  446. (indium-backend-active-connection-p 'webkit))
  447. (defun indium-webkit--value (result)
  448. "Return an alist representing the value of RESULT.
  449. The returned value can be a reference to a remote object, in
  450. which case the value associated to the `objectid' key is
  451. non-nil."
  452. (let* ((value (map-elt result 'value))
  453. (type (intern (map-elt result 'type)))
  454. (objectid (map-elt result 'objectId))
  455. (preview (indium-webkit--preview result))
  456. (description (indium-webkit--description result)))
  457. `((objectid . ,objectid)
  458. (description . ,description)
  459. (type . ,type)
  460. (preview . ,preview)
  461. (value . ,value))))
  462. (defun indium-webkit--exception (result)
  463. "Return an exception built from RESULT."
  464. (setf (map-elt result 'values)
  465. (list (indium-webkit--value
  466. (map-elt result 'exception)))))
  467. (defun indium-webkit--description (result)
  468. "Return a description string built from RESULT.
  469. RESULT should be a reference to a remote object."
  470. (let ((value (map-elt result 'value))
  471. (type (intern (map-elt result 'type))))
  472. (or (map-elt result 'description)
  473. (pcase type
  474. (`undefined "undefined")
  475. (`function "function")
  476. (`number (number-to-string value))
  477. (`string (format "\"%s\"" value))
  478. (`boolean (if (eq value t)
  479. "true"
  480. "false"))
  481. (_ (or value "null"))))))
  482. (defun indium-webkit--preview (result)
  483. "Return a preview string built from RESULT.
  484. RESULT should be a reference to a remote object."
  485. (let* ((preview (map-elt result 'preview))
  486. (subtype (map-elt preview 'subtype)))
  487. (if (string= subtype "array")
  488. (indium-webkit--preview-array preview)
  489. (indium-webkit--preview-object preview))))
  490. (defun indium-webkit--preview-object (preview)
  491. "Return a preview string from the properties of the object PREVIEW."
  492. (concat "{ "
  493. (mapconcat (lambda (prop)
  494. (format "%s: %s"
  495. (map-elt prop 'name)
  496. (indium-webkit--preview-property prop)))
  497. (map-elt preview 'properties)
  498. ", ")
  499. (if (eq (map-elt preview 'lossless) :json-false)
  500. ", … }"
  501. " }")))
  502. (defun indium-webkit--preview-array (preview)
  503. "Return a preview string from the elements of the array PREVIEW."
  504. (concat "[ "
  505. (mapconcat (lambda (prop)
  506. (indium-webkit--preview-property prop))
  507. (map-elt preview 'properties)
  508. ", ")
  509. (if (eq (map-elt preview 'lossless) :json-false)
  510. "… ]"
  511. " ]")))
  512. (defun indium-webkit--preview-property (property)
  513. "Return the string for a single object or array PROPERTY preview."
  514. (if (equal (map-elt property 'type) "string")
  515. (format "\"%s\"" (map-elt property 'value))
  516. (let ((preview (map-elt property 'value)))
  517. (if (seq-empty-p preview)
  518. (map-elt property 'type)
  519. preview))))
  520. (defun indium-webkit--properties (result)
  521. "Return a list of object properties built from RESULT."
  522. (seq-map (lambda (prop)
  523. `((name . ,(map-elt prop 'name))
  524. (value . ,(indium-webkit--value (or (map-elt prop 'value)
  525. (map-elt prop 'get))))))
  526. result))
  527. (defun indium-webkit--scope-chain (frame)
  528. "Return a scope chain for a FRAME."
  529. (let ((scope-chain (seq-map (lambda (scope)
  530. `((object . ,(indium-webkit--value (map-elt scope 'object)))
  531. (name . ,(map-elt scope 'name))
  532. (type . ,(map-elt scope 'type))))
  533. (map-elt frame 'scopeChain)))
  534. (this `((object . ,(indium-webkit--value (map-elt frame 'this)))
  535. (name . "this")
  536. (type . "local"))))
  537. (cl-loop for scope in scope-chain
  538. collect scope
  539. when (string= (map-elt scope 'type) "local")
  540. collect this)))
  541. (defun indium-webkit--convert-to-webkit-location (location)
  542. "Return an alist representing a Webkit location from LOCATION."
  543. (let ((result '()))
  544. (when-let ((line (indium-location-line location)))
  545. (map-put result 'lineNumber line))
  546. (when-let ((column (indium-location-column location)))
  547. (map-put result 'columnNumber column))
  548. (when-let ((file (indium-location-file location))
  549. (script (indium-script-find-from-file file)))
  550. (map-put result 'scriptId (indium-script-id script)))
  551. result))
  552. (defun indium-webkit--convert-from-webkit-location (location)
  553. "Return a location struct built from a webkit LOCATION."
  554. (make-indium-location-from-script-id :line (map-elt location 'lineNumber)
  555. :column (map-elt location 'columnNumber)
  556. :script-id (map-elt location 'scriptId)))
  557. (defun indium-webkit--frames (list)
  558. "Return a list of frames built from LIST."
  559. (seq-map (lambda (frame)
  560. (make-indium-frame
  561. :scope-chain (indium-webkit--scope-chain frame)
  562. :location (indium-webkit--convert-from-webkit-location (map-elt frame 'location))
  563. :type (map-elt frame 'type)
  564. :script (indium-script-find-by-id (map-nested-elt frame '(location scriptId)))
  565. :function-name (map-elt frame 'functionName)
  566. :id (map-elt frame 'callFrameId)))
  567. list))
  568. (defvar indium-webkit--request-id 0)
  569. (defun indium-webkit--next-request-id ()
  570. "Return the next unique identifier to be used in a request."
  571. (cl-incf indium-webkit--request-id))
  572. (provide 'indium-webkit)
  573. ;;; indium-webkit.el ends here