Browse Source

Rewrite Indium to work as a client for the new server

tags/2.0.0
Nicolas Petton 2 years ago
parent
commit
216635e134
No known key found for this signature in database GPG Key ID: E8BCD7866AFCF978
43 changed files with 1459 additions and 4934 deletions
  1. +0
    -156
      indium-backend.el
  2. +46
    -68
      indium-breakpoint.el
  3. +12
    -97
      indium-chrome.el
  4. +425
    -0
      indium-client.el
  5. +0
    -136
      indium-debugger-frames.el
  6. +28
    -15
      indium-debugger-litable.el
  7. +5
    -5
      indium-debugger-locals.el
  8. +164
    -59
      indium-debugger.el
  9. +24
    -26
      indium-inspector.el
  10. +71
    -95
      indium-interaction.el
  11. +0
    -67
      indium-list-scripts.el
  12. +98
    -0
      indium-list-sources.el
  13. +42
    -126
      indium-nodejs.el
  14. +57
    -76
      indium-render.el
  15. +70
    -99
      indium-repl.el
  16. +1
    -5
      indium-scratch.el
  17. +0
    -343
      indium-script.el
  18. +0
    -394
      indium-sourcemap.el
  19. +131
    -175
      indium-structs.el
  20. +0
    -702
      indium-v8.el
  21. +0
    -286
      indium-workspace.el
  22. +4
    -3
      indium.el
  23. +0
    -9
      test/fixtures/.indium.json
  24. +0
    -1
      test/fixtures/test-with-output.js
  25. +0
    -3
      test/fixtures/test.js
  26. +0
    -81
      test/integration/indium-nodejs-integration-test.el
  27. +0
    -64
      test/integration/indium-repl-integration-test.el
  28. +0
    -39
      test/unit/indium-backend-test.el
  29. +12
    -43
      test/unit/indium-breakpoint-test.el
  30. +43
    -86
      test/unit/indium-chrome-test.el
  31. +57
    -76
      test/unit/indium-debugger-test.el
  32. +7
    -10
      test/unit/indium-inspector-test.el
  33. +26
    -92
      test/unit/indium-interaction-test.el
  34. +0
    -46
      test/unit/indium-list-scripts-test.el
  35. +18
    -35
      test/unit/indium-nodejs-test.el
  36. +8
    -7
      test/unit/indium-repl-test.el
  37. +0
    -214
      test/unit/indium-script-test.el
  38. +0
    -134
      test/unit/indium-sourcemap-test.el
  39. +110
    -36
      test/unit/indium-structs-test.el
  40. +0
    -195
      test/unit/indium-v8-test.el
  41. +0
    -190
      test/unit/indium-workspace-test.el
  42. +0
    -158
      test/unit/wsc-test.el
  43. +0
    -482
      wsc.el

+ 0
- 156
indium-backend.el View File

@@ -1,156 +0,0 @@
;;; indium-backend.el --- Backend for indium.el -*- lexical-binding: t; -*-

;; Copyright (C) 2016-2018 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 `indium-register-backend'.
;; Once a connection to a JavaScript runtime is established by the backend, it
;; should set `indium-current-connection'.

;;; Code:

(require 'map)
(require 'seq)
(require 'indium-debugger-litable)
(eval-and-compile (require 'indium-structs))

(declare 'indium-debugger-unset-current-buffer)

(defgroup indium-backend nil
"Indium backend."
:prefix "indium-backend-"
:group 'indium)

(defcustom indium-connection-open-hook nil
"Hook called after a connection is open."
:group 'indium-backend
:type 'hook)

(defcustom indium-connection-closed-hook nil
"Hook called after a connection is closed."
:group 'indium-backend
:type 'hook)

(defvar indium-backends nil "List of registered backends.")

(defvar indium-script-parsed-hook nil "Hook run when a new script is parsed.")

(defun indium-register-backend (backend)
"Register a new BACKEND.
BACKEND should be a symbol."
(add-to-list 'indium-backends backend))

(declare-function indium-repl-get-buffer "indium-repl.el")
(declare-function indium-debugger-unset-current-buffer "indium-debugger.el")

(defun indium-backend-cleanup-buffers ()
"Cleanup all Indium buffers."
(seq-map (lambda (buf)
(with-current-buffer buf
(when buffer-file-name
(indium-debugger-unset-current-buffer))))
(buffer-list))
(when-let ((buf (indium-repl-get-buffer)))
(kill-buffer buf)))

(cl-defgeneric indium-backend-active-connection-p (_backend)
"Return non-nil if the current connection is active."
t)

(cl-defgeneric indium-backend-close-connection (_backend)
"Close the current connection.

Concrete implementations should run `indium-connection-closed-hook'.")

(cl-defgeneric indium-backend-reconnect (_backend)
"Try to re-establish a connection.
The new connection is created based on the current
`indium-current-connection'.")

(cl-defgeneric indium-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 indium-backend-get-completions (backend expression prefix callback)
"Get the completion for EXPRESSION that match PREFIX.
Evaluate CALLBACK on the filtered candidates.

EXPRESSION should be a valid JavaScript expression string.")

(cl-defgeneric indium-backend-register-breakpoint (backend breakpoint &optional callback)
"Request the addition of BREAKPOINT.")

(cl-defgeneric indium-backend-unregister-breakpoint (backend id &optional callback)
"Request the removal of the breakpoint with id ID.")

(cl-defgeneric indium-backend-deactivate-breakpoints (backend)
"Deactivate all breakpoints.
The runtime will not pause on any breakpoint."
)

(cl-defgeneric indium-backend-activate-breakpoints (backend)
"Deactivate all breakpoints.
The runtime will not pause on any breakpoint."
)

(cl-defgeneric indium-backend-set-script-source (backend url source &optional callback)
"Update the contents of the script at URL to SOURCE.")

(cl-defgeneric indium-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 indium-backend-get-script-source (backend frame callback)
"Get the source of the script for FRAME.
Evaluate CALLBACK with the result.")

(cl-defgeneric indium-backend-resume (backend &optional callback)
"Resume the debugger and evaluate CALLBACK if non-nil.")

(cl-defgeneric indium-backend-step-into (backend &optional callback)
"Step into the current stack frame and evaluate CALLBACK if non-nil.")

(cl-defgeneric indium-backend-step-out (backend &optional callback)
"Step out the current stack frame and evaluate CALLBACK if non-nil.")

(cl-defgeneric indium-backend-step-over (backend &optional callback)
"Step over the current stack frame and evaluate CALLBACK if non-nil.")

(cl-defgeneric indium-backend-continue-to-location (backend location &optional callback)
"Continue to LOCATION and evaluate CALLBACK if non-nil.")

(defun indium-backend-object-reference-p (value)
"Return non-nil if VALUE is a reference to a remote object."
(map-elt value 'objectid))

(provide 'indium-backend)
;;; indium-backend.el ends here

+ 46
- 68
indium-breakpoint.el View File

@@ -27,11 +27,9 @@

;;; Code:

(require 'indium-backend)
(require 'indium-client)
(require 'indium-faces)
(require 'indium-structs)
(eval-and-compile
(require 'indium-script))

(defvar indium-breakpoint--local-breakpoints (make-hash-table :weakness t)
"Table of all local breakpoints and their buffers.")
@@ -41,15 +39,10 @@

When CONDITION is non-nil, the breakpoint will be hit when
CONDITION is true."
(if-let ((location (indium-location-at-point)))
(let* ((brk (indium-breakpoint-create :original-location location
:condition (or condition ""))))
(map-put indium-breakpoint--local-breakpoints brk (current-buffer))
(indium-breakpoint--add-overlay brk)
(when-indium-connected
(indium-backend-register-breakpoint (indium-current-connection-backend)
brk)))
(user-error "Cannot place a breakpoint here")))
(let* ((brk (indium-breakpoint-create :condition (or condition ""))))
(map-put indium-breakpoint--local-breakpoints brk (current-buffer))
(indium-breakpoint--add-overlay brk)
(indium-client-add-breakpoint brk)))

(defun indium-breakpoint-edit-condition ()
"Edit condition of breakpoint at point."
@@ -61,12 +54,10 @@ CONDITION is true."
(indium-breakpoint-add new-condition))))

(defun indium-breakpoint-remove ()
"Remove the breakpoint from the current line."
(when-let ((brk (indium-breakpoint-at-point)))
(when-indium-connected
(when (indium-breakpoint-resolved brk)
(indium-backend-unregister-breakpoint (indium-current-connection-backend)
(indium-breakpoint-id brk))))
"Remove all breakpoints from the current line."
(seq-doseq (brk (indium-breakpoint-breakpoints-at-point))
(when (indium-breakpoint-resolved brk)
(indium-client-remove-breakpoint brk))
(map-delete indium-breakpoint--local-breakpoints brk)
(indium-breakpoint--remove-overlay)))

@@ -78,16 +69,17 @@ CONDITION is true."
(goto-char (overlay-start ov))
(indium-breakpoint-remove)))))

(defun indium-breakpoint-resolve (id script location)
"Update the breakpoint with ID for SCRIPT at LOCATION.
(defun indium-breakpoint-resolve (id line)
"Update the breakpoint with ID for SCRIPT at LINE.

This function should be called upon breakpoint resolution by the
backend, or when a breakpoint location gets updated from the
backend."
(let ((original-location (indium-script-original-location script location))
(brk (indium-breakpoint-breakpoint-with-id id)))
server, or when a breakpoint location gets updated from the
server."
(let* ((brk (indium-breakpoint-breakpoint-with-id id))
(location (indium-breakpoint-location brk)))
(setf (indium-breakpoint-resolved brk) t)
(indium-breakpoint--update-overlay brk original-location)))
(setf (indium-location-line location) line)
(indium-breakpoint--update-overlay brk location)))

(defun indium-breakpoint-breakpoint-with-id (id)
"Return the breakpoint with ID or nil."
@@ -95,11 +87,19 @@ backend."
(equal id (indium-breakpoint-id brk)))
(map-keys indium-breakpoint--local-breakpoints)))

(defun indium-breakpoint-breakpoints-at-point ()
"Return all breakpoints on the current line.
If there is no breakpoint set on the line, return nil."
(seq-filter (lambda (brk)
(let ((location (indium-breakpoint-location brk)))
(and (equal (indium-location-file location) buffer-file-name)
(equal (indium-location-line location) (line-number-at-pos)))))
(map-keys indium-breakpoint--local-breakpoints)))

(defun indium-breakpoint-at-point ()
"Return the breakpoint on the current line.
"Return the first breakpoint on the current line.
If there is no breakpoint set on the line, return nil."
(when-let ((ov (indium-breakpoint--overlay-on-current-line)))
(overlay-get ov 'indium-breakpoint)))
(car (indium-breakpoint-breakpoints-at-point)))

(defun indium-breakpoint-on-current-line-p ()
"Return non-nil if there is a breakpoint on the current line."
@@ -129,7 +129,7 @@ An icon is added to the left fringe."

(defun indium-breakpoint--remove-overlay ()
"Remove the breakpoint overlay from the current line."
(let ((ov (indium-breakpoint--overlay-on-current-line)))
(when-let ((ov (indium-breakpoint--overlay-on-current-line)))
(setf (indium-breakpoint-overlay (overlay-get ov 'indium-breakpoint)) nil)
(remove-overlays (overlay-start ov)
(overlay-end ov)
@@ -148,45 +148,29 @@ An icon is added to the left fringe."
(with-current-buffer (find-file-noselect file)
(save-excursion
(goto-char (point-min))
(forward-line line)
(forward-line (1- line))
(indium-breakpoint--add-overlay breakpoint)))))

(defun indium-breakpoint--update-breakpoints-in-current-buffer ()
"Update the breakpoints for the current buffer in the backend."
(indium-breakpoint--breakpoints-in-buffer-do
(lambda (brk overlay)
(indium-backend-unregister-breakpoint
(indium-current-connection-backend)
(indium-breakpoint-id brk)
(lambda ()
(save-excursion
(goto-char (overlay-start overlay))
(indium-breakpoint-add (indium-breakpoint-condition brk))))))))

(defun indium-breakpoint--resolve-all-breakpoints ()
"Resolve breakpoints from all buffers."
(let ((buffers (seq-uniq (map-values indium-breakpoint--local-breakpoints))))
(seq-doseq (buf buffers)
(with-current-buffer buf
(indium-breakpoint--resolve-breakpoints-in-current-buffer)))))
(defun indium-breakpoint-buffer (breakpoint)
"Return the buffer in which BREAKPOINT is set, or nil."
(when-let ((ov (indium-breakpoint-overlay breakpoint)))
(overlay-buffer ov)))

(defun indium-breakpoint--register-all-breakpoints ()
"Register all local breakpoints."
(map-apply (lambda (brk _)
(indium-client-add-breakpoint brk))
indium-breakpoint--local-breakpoints))

(defun indium-breakpoint--unregister-all-breakpoints ()
"Remove the registration information from all breakpoints."
(map-apply (lambda (brk _)
(indium-breakpoint-unregister brk)
(setf (indium-breakpoint-resolved brk) nil)
(indium-breakpoint--update-overlay
brk
(indium-breakpoint-original-location brk)))
(indium-breakpoint-location brk)))
indium-breakpoint--local-breakpoints))

(defun indium-breakpoint--resolve-breakpoints-in-current-buffer ()
"Resolve unresolved breakpoints from the current buffer."
(indium-breakpoint--breakpoints-in-buffer-do
(lambda (brk _)
(when (indium-breakpoint-can-be-resolved-p brk)
(indium-backend-register-breakpoint (indium-current-connection-backend)
brk)))))

(defun indium-breakpoint--fringe-icon (breakpoint)
"Return the fringe icon used for BREAKPOINT."
(propertize "b" 'display
@@ -210,18 +194,12 @@ If there is no overlay, make one."
(overlay-put ov 'indium-breakpoint-ov t)
ov)))

(defun indium-breakpoint--update-after-script-source-set (&rest _)
"Update the breakpoints in the current buffer each time its source is set."
(indium-breakpoint--update-breakpoints-in-current-buffer))

(defun indium-breakpoint--update-after-script-parsed (_)
"Attempt to resolve unresolved breakpoints."
(indium-breakpoint--resolve-all-breakpoints))
;; Handle breakpoint resolution
(add-hook 'indium-client-breakpoint-resolved-hook #'indium-breakpoint-resolve)

;; Update/Restore breakpoints
(add-hook 'indium-update-script-source-hook #'indium-breakpoint--update-after-script-source-set)
(add-hook 'indium-script-parsed-hook #'indium-breakpoint--update-after-script-parsed)
(add-hook 'indium-connection-closed-hook #'indium-breakpoint--unregister-all-breakpoints)
(add-hook 'indium-client-closed-hook #'indium-breakpoint--unregister-all-breakpoints)
(add-hook 'indium-client-connected-hook #'indium-breakpoint--register-all-breakpoints)

;; Helpers


+ 12
- 97
indium-chrome.el View File

@@ -30,10 +30,7 @@
(require 'map)
(require 'seq)

(require 'indium-v8)
(require 'indium-workspace)

(eval-and-compile (require 'indium-structs))
(declare-function indium-client-connect "indium-client.el")

(defgroup indium-chrome nil
"Chrome interaction."
@@ -58,48 +55,17 @@
"Default Chrome remote debugger port."
:type '(integer))

(defcustom indium-chrome-default-host
"localhost"
"Default Chrome remote debugger host."
:type '(string))

(defvar indium-chrome-url-history nil
"Chrome urls history.")

(defun indium-connect-to-chrome ()
"Open a connection to a Chrome tab."
(let* ((host (indium-chrome--host))
(port (indium-chrome--port)))
(indium-chrome--get-tabs-data host port #'indium-chrome--connect-to-tab)))

(defun indium-launch-chrome ()
"Start chrome/chromium with remote debugging enabled."
(make-process :name "indium-chrome-process"
:command (list (indium-chrome--find-executable)
(format "--remote-debugging-port=%s"
(indium-chrome--port))
(indium-chrome--url)))
(message "Connecting to Chrome instance...")
(indium-chrome--try-connect 10))

(defun indium-chrome--port ()
"Return the debugging port for the Chrome process.
The port is either read from the workpace configuration file or
`indium-chrome-default-port'."
(map-elt indium-workspace-configuration 'port indium-chrome-default-port))

(defun indium-chrome--host ()
"Return the debugging host for the Chrome process.
The host is either read from the workpace configuration file or
`indium-chrome-default-host'."
(map-elt indium-workspace-configuration 'host indium-chrome-default-host))

(defun indium-chrome--url ()
"Return the url to open for the Chrome process."
(let ((url (map-elt indium-workspace-configuration 'url)))
(unless url
(user-error "No Chrome url specified in the .indium.json file"))
url))
(defun indium-launch-chrome (conf)
"Start chrome/chromium with remote debugging enabled based on CONF settings."
(let-alist conf
(unless .url
(error "No url specified in configuration"))
(make-process :name "indium-chrome-process"
:command (list (indium-chrome--find-executable)
(format "--remote-debugging-port=%s"
(or .port indium-chrome-default-port))
.url))
(indium-client-connect (file-name-directory .projectFile) .name)))

(defun indium-chrome--find-executable ()
"Find chrome executable using `indium-chrome-executable'."
@@ -108,56 +74,5 @@ The host is either read from the workpace configuration file or
(user-error "Cannot find chrome/chromium binary (%s) in PATH" indium-chrome-executable))
executable))

(defun indium-chrome--try-connect (num-tries)
"Try to connect to chrome.
Try a maximum of NUM-TRIES."
(message "Trying to connect to the Chrome instance...")
(sleep-for 1)
(indium-chrome--get-tabs-data (indium-chrome--host)
(indium-chrome--port)
(lambda (tabs)
(if tabs
(indium-chrome--connect-to-tab tabs)
(when (> num-tries 0)
(indium-chrome--try-connect (1- num-tries)))))))

(defun indium-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)
(funcall callback (if (eq :error (car status))
nil
(indium-chrome--read-tab-data))))))

(defun indium-chrome--connect-to-tab (tabs)
"Connects to a tab in the list TABS.
If there are more then one tab available ask the user which tab to connect."
(unless tabs
(error "No Chrome tab found. Is Chrome running with the `--remote-debugging-port' flag set?"))
(if (= (seq-length tabs) 1)
(indium-chrome--connect-to-tab-with-url (map-elt (seq-elt tabs 0) 'url) tabs)
(let* ((urls (seq-map (lambda (tab)
(map-elt tab 'url))
tabs))
(url (completing-read "Tab: " urls nil t)))
(indium-chrome--connect-to-tab-with-url url tabs))))

(defun indium-chrome--connect-to-tab-with-url (url tabs)
"Connect to a tab with URL from list TABS."
(let* ((tab (seq-find (lambda (tab)
(string= (map-elt tab 'url) url))
tabs))
(websocket-url (map-elt tab 'webSocketDebuggerUrl)))
(indium-v8--open-ws-connection url websocket-url nil nil)))

(defun indium-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)))

(provide 'indium-chrome)
;;; indium-chrome.el ends here

+ 425
- 0
indium-client.el View File

@@ -0,0 +1,425 @@
;;; indium-client.el --- Indium process client -*- lexical-binding: t; -*-

;; Copyright (C) 2018 Nicolas Petton

;; Author: Nicolas Petton <nicolas@petton.fr>

;; 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 <https://www.gnu.org/licenses/>.

;;; Commentary:

;; The Indium process client starts and communicates with an "indium" process.
;;
;; Make sure to install the indium process with:
;; npm install -g indium

;;; Code:

(require 'json)
(require 'map)
(require 'subr-x)

(require 'indium-structs)

(defcustom indium-client-closed-hook nil
"Hook called after a client is closed."
:group 'indium-client
:type 'hook)

(defcustom indium-client-connected-hook nil
"Hook called after a client is connected."
:group 'indium-client
:type 'hook)

(defcustom indium-client-log-hook nil
"Hook called when a client receives a log event."
:group 'indium-client
:type 'hook)

(defcustom indium-client-breakpoint-resolved-hook nil
"Hook called upon breakpoint resolution."
:group 'indium-client
:type 'hook)

(defcustom indium-client-debugger-resumed-hook nil
"Hook called when the debugger is resumed."
:group 'indium-client
:type 'hook)

(defcustom indium-client-debugger-paused-hook nil
"Hook called when the debugger is paused."
:group 'indium-client
:type 'hook)

(defvar indium-client-debug nil
"When non-nil, log server output to *indium-client-log*.")

(defun indium-client-default-executable ()
"Return the default process executable."
(executable-find "indium"))

(defvar indium-client-executable (indium-client-default-executable)
"Process executable.")

(defvar indium-client--connection nil
"The client connection to the server process.")

(defvar indium-client--process nil
"The Indium server process.")

(defvar indium-client--process-port 13840
"The port on which the server should listen.")

(defvar indium-client--callbacks nil
"Alist of functions to be evaluated as callbacks on process response.")

(defun indium-client-start (callback)
"Start an Indium process and store it as the client process.
Evaluate CALLBACK once the server is started."
(when (indium-client-process-live-p)
(user-error "An indium process is already running"))
(let ((executable (executable-find indium-client-executable)))
(unless executable
(user-error "Cannot find the indium executable. Please run \"npm install -g indium\""))
(when indium-client-debug
(with-current-buffer (get-buffer-create "*indium-debug-log*")
(erase-buffer)))
(indium-client--start-server callback)))

(defun indium-client-stop ()
"Stop the indium process."
(when (process-live-p indium-client--connection)
(kill-buffer (process-buffer indium-client--process))
(kill-buffer (process-buffer indium-client--connection)))
(setq indium-client--connection nil)
(setq indium-client--process nil)
(setq indium-client--callbacks nil)
(run-hooks 'indium-client-closed-hook))

(defun indium-client-send (message &optional callback)
"Send MESSAGE to the Indium process.
When CALLBACK is non-nil, evaluate it with the process response."
(indium-client--ensure-process)
(let* ((id (indium-client--next-id))
(json (json-encode (cons `(id . ,id) message))))
(map-put indium-client--callbacks id callback)
(when indium-client-debug
(with-current-buffer (get-buffer-create "*indium-debug-log*")
(goto-char (point-max))
(insert (format "Sent: %s\n\n" (cons `(id . ,id) message)))))
(process-send-string indium-client--connection (format "%s\n" json))))

(defun indium-client-list-configurations (directory &optional callback)
"Request the list of configurations found in DIRECTORY.

Evaluate CALLBACK with the result."
(indium-client-send `((type . "configurations")
(payload . ((action . "list")
(directory . ,directory))))
callback))

(defun indium-client-connect (directory name)
"Connect to a runtime.
DIRECTORY is the path of the directory where the project file can be found.
NAME is the name of the configuration to use for connecting.

Once the client is connected, run the hook `indium-client-connected-hook'."
(indium-client-send `((type . "connection")
(payload . ((action . "connect")
(directory . ,directory)
(name . ,name))))
(lambda (&rest _)
(run-hooks 'indium-client-connected-hook))))

(defun indium-client-evaluate (expression &optional callback)
"Evaluate EXPRESSION.

When non-nil, evaluate CALLBACK with the result."
(indium-client-send
`((type . "runtime")
(payload . ((action . "evaluate")
(expression . ,expression))))
(lambda (obj)
(when callback
(funcall callback (indium-remote-object-from-alist obj))))))

(defun indium-client-get-completion (expression &optional callback)
"Request the list of completion for EXPRESSION.
When non-nil, evaluate CALLBACK with the result."
(indium-client-send `((type . "runtime")
(payload . ((action . "getCompletion")
(expression . ,expression))))
callback))

(defun indium-client-get-properties (id &optional callback)
"Request the list of properties for the remote object with ID.
When non-nil, evaluate CALLBACK with the result."
(indium-client-send
`((type . "runtime")
(payload . ((action . "getProperties")
(id . ,id))))
(lambda (properties)
(when callback
(funcall callback (seq-map #'indium-property-from-alist
properties))))))

(defun indium-client-activate-breakpoints ()
"Activate all breakpoints."
(indium-client-send `((type . "runtime")
(payload . ((action . "activateBreakpoints"))))))

(defun indium-client-deactivate-breakpoints ()
"Deactivate all breakpoints."
(indium-client-send `((type . "runtime")
(payload . ((action . "deactivateBreakpoints"))))))

(defun indium-client-add-breakpoint (breakpoint)
"Request the addition of BREAKPOINT."
(let* ((id (indium-breakpoint-id breakpoint))
(location (indium-breakpoint-location breakpoint))
(file (indium-location-file location))
(line (indium-location-line location)))
(indium-client-send `((type . "runtime")
(payload . ((action . "addBreakpoint")
(id . ,id)
(file . ,file)
(line . ,line)))))))

(defun indium-client-remove-breakpoint (breakpoint)
"Request the removal of BREAKPOINT."
(let ((id (indium-breakpoint-id breakpoint)))
(indium-client-send `((type . "runtime")
(payload . ((action . "removeBreakpoint")
(id . ,id)))))))

(defun indium-client-resume ()
"Resume the runtime execution."
(indium-client-send `((type . "runtime")
(payload . ((action . "resume"))))))

(defun indium-client-step-into ()
"Request a step into."
(indium-client-send `((type . "runtime")
(payload . ((action . "stepInto"))))))

(defun indium-client-step-out ()
"Request a step out."
(indium-client-send `((type . "runtime")
(payload . ((action . "stepOut"))))))

(defun indium-client-step-over ()
"Request a step over."
(indium-client-send `((type . "runtime")
(payload . ((action . "stepOver"))))))

(defun indium-client-continue-to-location (location)
"Request the runtime to resume until LOCATION is reached."
(indium-client-send
`((type . "runtime")
(payload . ((action . "continueToLocation")
(location . ((file . ,(indium-location-file location))
(line . ,(indium-location-line location))
(column . ,(indium-location-column location)))))))))

(defun indium-client-get-frame-source (frame &optional callback)
"Request the source of FRAME.

When CALLBACK is non-nil, evaluate it with the source"
(indium-client-send
`((type . "runtime")
(payload . ((action . "getSource")
(id . ,(indium-frame-script-id frame)))))
callback))

(defun indium-client-get-sourcemap-sources (&optional callback)
"Request the all the sourcemap source paths.

When CALLBACK is non-nil, evaluate it with the list of sources."
(indium-client-send
`((type . "runtime")
(payload . ((action . "getSourcemapSources"))))
callback))

(defun indium-client-get-script-sources (&optional callback)
"Request the all the script source paths.

When CALLBACK is non-nil, evaluate it with the list of sources."
(indium-client-send
`((type . "runtime")
(payload . ((action . "getScriptSources"))))
callback))

(defun indium-client--ensure-process ()
"Signal an error if the Indium is not started."
(unless (indium-client-process-live-p)
(user-error "Indium server not started")))

(defun indium-client-process-live-p ()
"Return non-nil if the indium process is running."
(process-live-p indium-client--connection))

(defun indium-client--start-server (callback)
"Start the Indium server process.

Evaluate CALLBACK once the server is started and the TCP
connection established."
(setq indium-client--process
(start-process "indium server"
(generate-new-buffer "*indium-process*")
"indium"
(format "%s" indium-client--process-port)))
(set-process-query-on-exit-flag indium-client--process nil)
(set-process-filter indium-client--process
(indium-client--process-filter-function callback)))

(defun indium-client--process-filter-function (callback)
"Return a process filter function for an Indium server process.

Evaluate CALLBACK when the server starts listening to TCP connections."
(let ((connected nil))
(lambda (process output)
(unless connected ;; do not try to open TCP connections multiple times
(with-current-buffer (process-buffer process)
(goto-char (point-max))
(insert output))
(if (string-match-p "server listening" output)
(indium-client--open-network-stream callback)
(progn
(indium-client-stop)
(error "Indium server process error: %s" output)))))))

(defun indium-client--open-network-stream (callback)
"Open a network connection to the indium server TCP process.
Evaluate CALLBACK once the connection is established."
(let ((process (open-network-stream "indium"
(generate-new-buffer " indium-client-conn ")
"localhost"
indium-client--process-port)))
(set-process-filter process #'indium-client--connection-filter)
(set-process-coding-system process 'utf-8)
(set-process-query-on-exit-flag process nil)
;; TODO: Set a process sentinel
;; (set-process-sentinel process #'indium-client--connection-sentinel)
(setq indium-client--connection process)
(funcall callback)))

(defun indium-client--connection-sentinel (callback)
"Evaluate CALLBACK when the network process is open."
(lambda (proc _event)
(when (eq (process-status proc) 'open)
(funcall callback))))

(defun indium-client--connection-filter (process output)
"Filter function for handling the indium PROCESS OUTPUT."
(let ((buf (process-buffer process)))
(with-current-buffer buf
(save-excursion
(goto-char (point-max))
(insert output)))
(indium-client--handle-data buf)))

(defun indium-client--handle-data (buffer)
"Handle process data in BUFFER.

Read the complete messages sequentially and handle them. Each
read message is deleted from BUFFER."
(with-current-buffer buffer
(when (indium-client--complete-message-p)
(save-excursion
(goto-char (point-min))
(when-let ((data (ignore-errors (json-read))))
(delete-region (point-min) (point))
(indium-client--handle-message data)
(indium-client--handle-data buffer))))))

(defun indium-client--complete-message-p ()
"Return non-nil if the current buffer has a complete message.
Messages end with a line feed."
(save-excursion
(goto-char (point-max))
(search-backward "\n" nil t)))

(defun indium-client--handle-message (data)
"Handle a server message with DATA."
(when indium-client-debug
(with-current-buffer (get-buffer-create "*indium-debug-log*")
(goto-char (point-max))
(insert (format "Received: %s\n\n" data))))
(let-alist data
(pcase .type
("error" (indium-client--handle-error .payload))
("success" (indium-client--handle-response .id .payload))
("notification" (indium-client--handle-notification .payload))
("log" (indium-client--handle-log .payload)))))

(defun indium-client--handle-error (payload)
"Handle an error from the server.
PAYLOAD is an alist containing the details of the error."
(let-alist payload
(message "Indium server error: %s" .error)))

(defun indium-client--handle-response (id payload)
"Handle a response to a client request.
ID is the id of the request for which the server has answered.
PAYLOAD contains the data of the response.

If a callback function has been registered for ID, evaluate it
with the PAYLOAD."
(let ((callback (map-elt indium-client--callbacks id)))
(when callback
(unwind-protect
(funcall callback payload)
(map-delete indium-client--callbacks id)))))

(defun indium-client--handle-log (payload)
"Handle a log event from the server.

PAYLOAD is an alist with the details of the log event.
If has the following keys:
type type of message
url url of the message origin
line line number in the resource that generated this message
result object to be logged."
(map-put payload 'result (indium-remote-object-from-alist
(map-elt payload 'result)))
(run-hook-with-args 'indium-client-log-hook
payload))

(defun indium-client--handle-notification (payload)
"Handle a notification event sent from the server.
PAYLOAD is an alist with the details of the notification."
(let-alist payload
(pcase .type
("breakpointResolved"
(progn
(run-hook-with-args 'indium-client-breakpoint-resolved-hook .id .line)))
("paused"
(run-hook-with-args 'indium-client-debugger-paused-hook
(seq-map #'indium-frame-from-alist .frames)
.reason
.description))
("resumed"
(run-hooks 'indium-client-debugger-resumed-hook))
(_ (message "Indium notification %s" payload)))))

(defvar indium-client--id 0)
(defun indium-client--next-id ()
"Return the next unique identifier to be used."
(cl-incf indium-client--id))

(provide 'indium-client)
;;; indium-client.el ends here

+ 0
- 136
indium-debugger-frames.el View File

@@ -1,136 +0,0 @@
;;; indium-debugger-frames.el --- List the stack frame -*- lexical-binding: t; -*-

;; Copyright (C) 2017-2018 Nicolas Petton

;; Author: Nicolas Petton <nicolas@petton.fr>

;; 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:

;;

;;; Code:

(require 'indium-render)
(require 'indium-script)
(require 'indium-structs)

(declare-function indium-debugger-select-frame "indium-debugger.el")

(defun indium-debugger-stack-frames ()
"List the stack frames in a separate buffer and switch to it."
(interactive)
(let ((buf (indium-debugger-frames-get-buffer-create))
(inhibit-read-only t))
(with-current-buffer buf
(indium-debugger-frames-list (indium-current-connection-frames)
(indium-current-connection-current-frame)))
(pop-to-buffer buf)))

(defun indium-debugger-frames-maybe-refresh ()
"When a buffer listing the stack frames is open, refresh it."
(interactive)
(let ((buf (indium-debugger-frames-get-buffer))
(inhibit-read-only t))
(when buf
(with-current-buffer buf
(indium-debugger-frames-list (indium-current-connection-frames)
(indium-current-connection-current-frame))))))

(defun indium-debugger-frames-list (frames &optional current-frame)
"Render the list of stack frames FRAMES.
CURRENT-FRAME is the current stack frame in the debugger."
(save-excursion
(erase-buffer)
(indium-render-header "Debugger stack")
(newline 2)
(seq-doseq (frame frames)
(indium-render-frame
frame
(indium-location-file (indium-script-original-location
(indium-frame-script frame)
(indium-frame-location frame)))
(eq current-frame frame))
(newline))))

(defun indium-debugger-frames-select-frame (frame)
"Select FRAME and switch to the corresponding debugger buffer."
(interactive)
(indium-debugger-select-frame frame))

(defun indium-debugger-frames-next-frame ()
"Go to the next frame in the stack."
(interactive)
(indium-debugger-frames-goto-next 'next))

(defun indium-debugger-frames-previous-frame ()
"Go to the previos frame in the stack."
(interactive)
(indium-debugger-frames-goto-next 'previous))

(defun indium-debugger-frames-goto-next (direction)
"Go to the next frame in DIRECTION."
(let ((next (eq direction 'next)))
(forward-line (if next 1 -1))
(back-to-indentation)
(while (and (not (if next
(eobp)
(bobp)))
(not (get-text-property (point) 'indium-action)))
(forward-char (if next 1 -1)))))

(defun indium-debugger-frames-get-buffer ()
"Return the buffer listing frames for the current connection.
If no buffer is found, return nil."
(get-buffer (indium-debugger-frames-buffer-name)))

(defun indium-debugger-frames-buffer-name ()
"Return the name of the frames buffer for the current connection."
"*JS Frames*")

(defun indium-debugger-frames-get-buffer-create ()
"Create a buffer for listing frames unless one exists, and return it."
(let ((buf (indium-debugger-frames-get-buffer)))
(unless buf
(setq buf (generate-new-buffer (indium-debugger-frames-buffer-name)))
(indium-debugger-frames-setup-buffer buf))
buf))

(defun indium-debugger-frames-setup-buffer (buffer)
"Setup the frames BUFFER."
(with-current-buffer buffer
(indium-debugger-frames-mode)
(setq-local truncate-lines nil)))

(defvar indium-debugger-frames-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [return] #'indium-follow-link)
(define-key map (kbd "C-m") #'indium-follow-link)
(define-key map (kbd "n") #'indium-debugger-frames-next-frame)
(define-key map (kbd "p") #'indium-debugger-frames-previous-frame)
(define-key map [tab] #'indium-debugger-frames-next-frame)
(define-key map [backtab] #'indium-debugger-frames-previous-frame)
map))

(define-derived-mode indium-debugger-frames-mode special-mode "Frames"
"Major mode visualizind and navigating the JS stack.

\\{indium-debugger-frames--mode-map}"
(setq buffer-read-only t)
(font-lock-ensure)
(read-only-mode))

(provide 'indium-debugger-frames)
;;; indium-debugger-frames.el ends here

+ 28
- 15
indium-debugger-litable.el View File

@@ -24,25 +24,34 @@
;;; Code:

(require 'js2-mode)
(require 'js2-refactor)
(require 'subr-x)
(require 'seq)

(require 'indium-render)

(declare-function indium-debugger-get-current-scopes "indium-debugger" ())
(declare-function indium-debugger-get-scope-properties "indium-debugger" (scope callback))
(declare-function indium-debugger-get-current-scopes "indium-debugger.el" ())
(declare-function indium-debugger-get-scopes-properties "indium-debugger.el" (scope callback))
(declare-function indium-debugger-get-buffer-create "indium-debugger.el" ())

(defun indium-debugger-litable-setup-buffer ()
"Render locals in the current buffer."
(let ((scope (car (indium-debugger-get-current-scopes))))
(indium-debugger-get-scope-properties
scope
(lambda (properties _)
;; This is just cosmetic, don't break the session
(ignore-errors
(js2-mode-wait-for-parse
(lambda ()
(js2-visit-ast js2-mode-ast
(indium-debugger-litable-make-visitor properties)))))))))
(indium-debugger-get-scopes-properties
(indium-debugger-get-current-scopes)
(lambda (properties _)
;; This is just cosmetic, don't break the session
(ignore-errors
(with-current-buffer (indium-debugger-get-buffer-create)
(js2-mode-wait-for-parse
(lambda ()
(with-current-buffer (indium-debugger-get-buffer-create)
(js2-visit-ast (indium-debugger-litable--scope-node)
(indium-debugger-litable-make-visitor properties))))))))))

(defun indium-debugger-litable--scope-node ()
"Return the scope node from point."
(or (js2r--closest #'js2-function-node-p)
js2-mode-ast))

(defun indium-debugger-litable-unset-buffer ()
"Remove locals from the current buffer."
@@ -75,6 +84,7 @@
(let ((parent (js2-node-parent node)))
(and parent (js2-name-node-p node)
(or (js2-var-init-node-p parent)
(js2-object-prop-node-p parent)
(js2-assign-node-p parent)))))

(defun indium-debugger-litable-visit-var-init-node (node properties)
@@ -89,7 +99,7 @@
(js2-node-abs-end node)))
(property (seq-find (lambda (property)
(string= name
(map-elt property 'name)))
(indium-property-name property)))
properties)))
(indium-debugger-litable-add-value-overlay node property)))

@@ -115,7 +125,7 @@ Ignore if the object name of NODE is not in the current scope."
(let ((inhibit-read-only t)
(ov (indium-debugger-litable--get-overlay-at-pos))
(contents (string-trim (indium-render-property-to-string property)))
(name (map-elt property 'name)))
(name (indium-property-name property)))
(unless (seq-contains (overlay-get ov 'indium-properties) name)
;; The overlay is already used to display exception details, so do not
;; append anything to it.
@@ -142,7 +152,10 @@ If the display string overflows, trim it to avoid truncating the line."
(save-excursion
(goto-char (point-at-eol))
(if (>= (+ (seq-length string) (current-column)) (window-width))
(let ((width (- (window-width) (current-column) 1)))
(let* ((line-number-width (if (fboundp 'line-number-display-width)
(line-number-display-width 'columns)
0))
(width (- (window-width) (current-column) line-number-width 1)))
(truncate-string-to-width string width 0 nil "..."))
string)))



+ 5
- 5
indium-debugger-locals.el View File

@@ -24,10 +24,10 @@
;;; Code:

(require 'indium-render)
(require 'indium-inspector)

(declare 'indium-backend-get-properties)
(declare 'indium-debugger-get-scopes-properties)
(declare 'indium-debugger-get-current-scopes)
(declare-function indium-debugger-get-scopes-properties "indium-debugger.el")
(declare-function indium-debugger-get-current-scopes "indium-debugger.el")

(defun indium-debugger-locals (&optional no-pop)
"Inspect the local variables in the current stack frame's scope.
@@ -55,8 +55,8 @@ Unless NO-POP is non-nil, pop the locals buffer."
Unless NO-POP in non-nil, pop the locals buffer."
(let* ((buf (indium-debugger-locals-get-buffer-create))
(inhibit-read-only t)
(name (map-elt scope 'name))
(type (map-elt scope 'type))
(name (indium-scope-name scope))
(type (indium-scope-type scope))
(description (if (or (null name)
(string= name "undefined"))
type


+ 164
- 59
indium-debugger.el View File

@@ -34,10 +34,7 @@
(require 'indium-structs)
(require 'indium-inspector)
(require 'indium-repl)
(require 'indium-interaction)
(require 'indium-render)
(require 'indium-workspace)
(require 'indium-debugger-frames)
(require 'indium-debugger-locals)
(require 'indium-debugger-litable)

@@ -62,6 +59,12 @@ from the debugger."
"When non-nil, use inspect as a default eval when debugging."
:type 'boolean)

(defvar indium-debugger-current-frame nil
"Currently selected frame in the debugger.")

(defvar indium-debugger-frames nil
"Call frames of the current debugger session.")

(defvar indium-debugger-buffer nil "Buffer used for debugging JavaScript sources.")

(defvar indium-debugger-message nil "Message to be displayed in the echo area.")
@@ -110,10 +113,7 @@ from the debugger."
:lighter " JS-debug"
:keymap indium-debugger-mode-map
(if indium-debugger-mode
(progn
(unless indium-interaction-mode
(indium-interaction-mode))
(add-hook 'pre-command-hook #'indium-debugger-refresh-echo-area nil t))
(add-hook 'pre-command-hook #'indium-debugger-refresh-echo-area nil t)
(remove-hook 'pre-command-hook #'indium-debugger-refresh-echo-area t)))

(defun indium-debugger-paused (frames reason &optional description)
@@ -139,7 +139,8 @@ Unset the debugging context and turn off indium-debugger-mode."
indium-debugger-mode))
(buffer-list)))
(with-current-buffer buf
(set-marker overlay-arrow-position nil (current-buffer))
(when overlay-arrow-position
(set-marker overlay-arrow-position nil (current-buffer)))
(indium-debugger-unset-current-buffer)
(indium-debugger-litable-unset-buffer)))
(let ((locals-buffer (indium-debugger-locals-get-buffer))
@@ -157,20 +158,30 @@ Unset the debugging context and turn off indium-debugger-mode."
(interactive)
(indium-debugger--jump-to-frame 'backward))

(defun indium-debugger-stack-frames ()
"List the stack frames in a separate buffer and switch to it."
(interactive)
(let ((buf (indium-debugger-frames-get-buffer-create))
(inhibit-read-only t))
(with-current-buffer buf
(indium-debugger-frames-list indium-debugger-frames
indium-debugger-current-frame))
(pop-to-buffer buf)))

(defun indium-debugger--jump-to-frame (direction)
"Jump to the next frame in DIRECTION.
DIRECTION is `forward' or `backward' (in the frame list)."
(let* ((current-position (seq-position (indium-current-connection-frames)
(indium-current-connection-current-frame)))
(let* ((current-position (seq-position indium-debugger-frames
indium-debugger-current-frame))
(step (pcase direction
(`forward -1)
(`backward 1)))
(position (+ current-position step)))
(when (>= position (seq-length (indium-current-connection-frames)))
(when (>= position (seq-length indium-debugger-frames))
(user-error "End of frames"))
(when (< position 0)
(user-error "Beginning of frames"))
(indium-debugger-select-frame (seq-elt (indium-current-connection-frames) position))))
(indium-debugger-select-frame (seq-elt indium-debugger-frames position))))

(defun indium-debugger-select-frame (frame)
"Make FRAME the current debugged stack frame.
@@ -181,26 +192,28 @@ to that buffer.
Try to find the file for the stack frame locally first using
Indium worskspaces. If not local file can be found, get the
remote source for that frame."
(indium-debugger-set-current-frame frame)
(setq indium-debugger-current-frame frame)
(switch-to-buffer (indium-debugger-get-buffer-create))
(indium-debugger-litable-setup-buffer)
(if buffer-file-name
(indium-debugger-setup-buffer-with-file)
(progn
(message "Downloading script source for debugging...")
(indium-backend-get-script-source
(indium-current-connection-backend)
frame
(lambda (source)
(indium-debugger-setup-buffer-with-source
(map-nested-elt source '(result scriptSource)))
(message "Downloading script source for debugging...done!"))))))
(if (yes-or-no-p "No file found for debugging (sourcemap issue?), download script source (might be slow)?")
(progn
(message "Downloading script source for debugging...")
(indium-client-get-frame-source
frame
(lambda (source)
(with-current-buffer (indium-debugger-get-buffer-create)
(indium-debugger-setup-buffer-with-source source))
(message "Downloading script source for debugging...done!"))))
(indium-client-resume)))))

(defun indium-debugger-setup-buffer-with-file ()
"Setup the current buffer for debugging."
(when (buffer-modified-p)
(revert-buffer nil nil t))
(indium-debugger--goto-current-frame))
(indium-debugger--goto-current-frame)
(indium-debugger-litable-setup-buffer))

(defun indium-debugger-setup-buffer-with-source (source)
"Setup the current buffer with the frame SOURCE."
@@ -209,14 +222,14 @@ remote source for that frame."
(let ((inhibit-read-only t))
(erase-buffer)
(insert source)))
(indium-debugger--goto-current-frame))
(indium-debugger--goto-current-frame)
(indium-debugger-litable-setup-buffer))

(defun indium-debugger--goto-current-frame ()
"Move the point to the current stack frame position in the current buffer."
(let* ((frame (indium-current-connection-current-frame))
(location (indium-script-get-frame-original-location frame)))
(let* ((location (indium-frame-location indium-debugger-current-frame)))
(goto-char (point-min))
(forward-line (indium-location-line location))
(forward-line (1- (indium-location-line location)))
(forward-char (indium-location-column location)))
(indium-debugger-setup-overlay-arrow)
(indium-debugger-highlight-node)
@@ -286,34 +299,40 @@ remote source for that frame."

(defun indium-debugger-top-frame ()
"Return the top frame of the current debugging context."
(car (indium-current-connection-frames)))
(car indium-debugger-frames))

(defun indium-debugger-step-into ()
"Request a step into."
(interactive)
(indium-backend-step-into (indium-current-connection-backend)))
(indium-client-step-into))

(defun indium-debugger-step-over ()
"Request a step over."
(interactive)
(indium-backend-step-over (indium-current-connection-backend)))
(indium-client-step-over))

(defun indium-debugger-step-out ()
"Request a step out."
(interactive)
(indium-backend-step-out (indium-current-connection-backend)))
(indium-client-step-out))

(defun indium-debugger-resume ()
"Request the runtime to resume the execution."
(interactive)
(indium-backend-resume (indium-current-connection-backend)))
(indium-client-resume))

(defun indium-debugger-here ()
"Request the runtime to resume the execution until the point.
When the position of the point is reached, pause the execution."
(interactive)
(indium-backend-continue-to-location (indium-current-connection-backend)
(indium-script-generated-location-at-point)))
(indium-client-continue-to-location (indium-location-at-point)))

(defun indium-debugger-switch-to-debugger-buffer ()
"Switch to the debugger buffer.
If there is no debugging session, signal an error."
(unless indium-debugger-current-frame
(user-error "No debugger to switch to"))
(indium-debugger-select-frame indium-debugger-current-frame))

(defun indium-debugger-evaluate (expression)
"Prompt for EXPRESSION to be evaluated.
@@ -328,63 +347,54 @@ result of the evaluation if possible."
(thing-at-point 'symbol))))
(read-string (format "Evaluate on frame: (%s): " default)
nil nil default))))
(indium-backend-evaluate (indium-current-connection-backend)
expression
(lambda (value _error)
(indium-client-evaluate expression
(lambda (value)
(let ((inspect (and (or indium-debugger-inspect-when-eval
current-prefix-arg)
(map-elt value 'objectid))))
(if inspect
(indium-inspector-inspect value)
(message "%s" (indium-render-value-to-string value)))))))
(message "%s" (indium-render-remote-object-to-string value)))))))

;; Debugging context

(defun indium-debugger-set-frames (frames)
"Set the debugger FRAMES."
(setf (indium-current-connection-frames) frames)
(indium-debugger-set-current-frame (car frames)))

(defun indium-debugger-set-current-frame (frame)
"Set FRAME as the current frame."
(setf (indium-current-connection-current-frame) frame))
(setq indium-debugger-frames frames)
(setq indium-debugger-current-frame (car frames)))

(defun indium-debugger-unset-frames ()
"Remove debugging information from the current connection."
(setf (indium-current-connection-frames) nil)
(setf (indium-current-connection-current-frame) nil))
(setq indium-debugger-frames nil)
(setq indium-debugger-current-frame nil))

(defun indium-debugger-get-current-scopes ()
"Return the scope of the current stack frame."
(indium-frame-scope-chain (indium-current-connection-current-frame)))
(and indium-debugger-current-frame
(indium-frame-scope-chain indium-debugger-current-frame)))

;; TODO: move to backends?
(defun indium-debugger-get-scopes-properties (scopes callback)
"Request a list of all properties in SCOPES.
CALLBACK is evaluated with the result."
(seq-do (lambda (scope)
(indium-debugger-get-scope-properties scope callback))
;; ignore the objects attached to global/window
(seq-remove (lambda (scope)
(string= (map-elt scope 'type) "global"))
scopes)))
scopes))

(defun indium-debugger-get-scope-properties (scope callback)
"Request the properties of SCOPE and evaluate CALLBACK.
CALLBACK is evaluated with two arguments, the properties and SCOPE."
(indium-backend-get-properties
(indium-current-connection-backend)
(map-nested-elt scope '(object objectid))
(lambda (properties)
(funcall callback properties scope))))
(let-alist scope
(indium-client-get-properties (indium-scope-id scope)
(lambda (properties)
(funcall callback properties scope)))))

(defun indium-debugger-get-buffer-create ()
"Create a debugger buffer for the current connection and return it.

If a buffer already exists, just return it."
(let* ((location (indium-script-get-frame-original-location (indium-current-connection-current-frame)))
(let* ((location (indium-frame-location indium-debugger-current-frame))
(file (indium-location-file location))
(buf (if (and file (file-exists-p file))
(buf (if (and file (file-regular-p file))
(find-file file)
(get-buffer-create (indium-debugger--buffer-name-no-file)))))
(indium-debugger-setup-buffer buf)
@@ -416,5 +426,100 @@ frame."
(read-only-mode -1)
(indium-debugger-litable-unset-buffer))

;; Frame listing

(defun indium-debugger-frames-maybe-refresh ()
"When a buffer listing the stack frames is open, refresh it."
(interactive)
(let ((buf (indium-debugger-frames-get-buffer))
(inhibit-read-only t))
(when buf
(with-current-buffer buf
(indium-debugger-frames-list indium-debugger-frames
indium-debugger-current-frame)))))

(defun indium-debugger-frames-list (frames &optional current-frame)
"Render the list of stack frames FRAMES.
CURRENT-FRAME is the current stack frame in the debugger."
(save-excursion
(erase-buffer)
(indium-render-header "Debugger stack")
(newline 2)
(seq-doseq (frame frames)
(indium-render-frame
frame
(eq current-frame frame))
(newline))))

(defun indium-debugger-frames-select-frame (frame)
"Select FRAME and switch to the corresponding debugger buffer."
(interactive)
(indium-debugger-select-frame frame))

(defun indium-debugger-frames-next-frame ()
"Go to the next frame in the stack."
(interactive)
(indium-debugger-frames-goto-next 'next))

(defun indium-debugger-frames-previous-frame ()
"Go to the previos frame in the stack."
(interactive)
(indium-debugger-frames-goto-next 'previous))

(defun indium-debugger-frames-goto-next (direction)
"Go to the next frame in DIRECTION."
(let ((next (eq direction 'next)))
(forward-line (if next 1 -1))
(back-to-indentation)
(while (and (not (if next
(eobp)
(bobp)))
(not (get-text-property (point) 'indium-action)))
(forward-char (if next 1 -1)))))

(defun indium-debugger-frames-get-buffer ()
"Return the buffer listing frames for the current connection.
If no buffer is found, return nil."
(get-buffer (indium-debugger-frames-buffer-name)))

(defun indium-debugger-frames-buffer-name ()
"Return the name of the frames buffer for the current connection."
"*JS Frames*")

(defun indium-debugger-frames-get-buffer-create ()
"Create a buffer for listing frames unless one exists, and return it."
(let ((buf (indium-debugger-frames-get-buffer)))
(unless buf
(setq buf (generate-new-buffer (indium-debugger-frames-buffer-name)))
(indium-debugger-frames-setup-buffer buf))
buf))

(defun indium-debugger-frames-setup-buffer (buffer)
"Setup the frames BUFFER."
(with-current-buffer buffer
(indium-debugger-frames-mode)
(setq-local truncate-lines nil)))

(defvar indium-debugger-frames-mode-map
(let ((map (make-sparse-keymap)))
(define-key map [return] #'indium-follow-link)
(define-key map (kbd "C-m") #'indium-follow-link)
(define-key map (kbd "n") #'indium-debugger-frames-next-frame)
(define-key map (kbd "p") #'indium-debugger-frames-previous-frame)
(define-key map [tab] #'indium-debugger-frames-next-frame)
(define-key map [backtab] #'indium-debugger-frames-previous-frame)
map))

(define-derived-mode indium-debugger-frames-mode special-mode "Frames"
"Major mode visualizind and navigating the JS stack.

\\{indium-debugger-frames--mode-map}"
(setq buffer-read-only t)
(font-lock-ensure)
(read-only-mode))

(add-hook 'indium-client-debugger-paused-hook #'indium-debugger-paused)
(add-hook 'indium-client-debugger-resumed-hook #'indium-debugger-resumed)

(provide 'indium-debugger)
;;; indium-debugger.el ends here

+ 24
- 26
indium-inspector.el View File

@@ -24,36 +24,37 @@

;;; Code:

(require 'seq)
(require 'map)
(require 'subr-x)
(require 'indium-structs)
(require 'indium-render)
(require 'indium-faces)

(declare-function indium-client-get-properties "indium-client.el")

(defvar indium-inspector-history nil)
(make-variable-buffer-local 'indium-inspector-history)

(declare-function indium-backend-get-properties "indium-backend.el")
(declare-function indium-backend "indium-backend.el")

(defun indium-inspector-inspect (reference)
"Open an inspector on the remote object REFERENCE."
(let ((objectid (map-elt reference 'objectid)))
(if objectid
(indium-backend-get-properties (indium-current-connection-backend)
objectid
(lambda (properties)
(indium-inspector--inspect-properties properties reference)))
(message "Cannot inspect %S" (map-elt reference 'description)))))

(defun indium-inspector--inspect-properties (properties reference)
"Insert all PROPERTIES for the remote object REFERENCE."
(defun indium-inspector-inspect (obj)
"Open an inspector on the remote object OBJ."
(if (indium-remote-object-reference-p obj)
(indium-client-get-properties
(indium-remote-object-id obj)
(lambda (properties)
(indium-inspector--inspect-properties properties obj)))
(message "Cannot inspect %S" (indium-remote-object-description obj))))

(defun indium-inspector--inspect-properties (properties obj)
"Insert all PROPERTIES for the remote object OBJ."
(let ((buf (indium-inspector-get-buffer-create))
(inhibit-read-only t))
(with-current-buffer buf
(indium-inspector-push-to-history reference)
(indium-inspector-push-to-history obj)
(save-excursion
(erase-buffer)
(indium-render-keyword (indium-description-string reference t))
(indium-render-keyword (indium-remote-object-to-string obj t))
(insert "\n\n")
(indium-inspector--insert-sorted-properties properties)))
(pop-to-buffer buf)))
@@ -71,18 +72,13 @@
"Split PROPERTIES into list where the first element is native properties and the second is the rest."
(seq-reduce (lambda (result property)
(push property
(if (indium-inspector--native-property-p property)
(if (indium-property-native-p property)
(car result)
(cadr result)))
result)
properties
(list nil nil)))

(defun indium-inspector--native-property-p (property)
"Return non-nil value if PROPERTY is a native code."
(string-match-p "{ \\[native code\\] }$"
(map-nested-elt property '(value description))))

(defun indium-inspector-pop ()
"Go back in the history to the last object inspected."
(interactive)
@@ -132,9 +128,11 @@ DIRECTION can be either `next' or `previous'."

(defun indium-inspector-push-to-history (reference)
"Add REFERENCE to the inspected objects history."
(unless (string= (map-elt reference 'objectid)
(map-elt (car indium-inspector-history) 'objectid))
(push reference indium-inspector-history)))
(let-alist reference
(when (or (seq-empty-p indium-inspector-history)
(not (equal (indium-remote-object-id reference)
(indium-remote-object-id (car indium-inspector-history)))))
(push reference indium-inspector-history))))

(defun indium-inspector-get-buffer ()
"Return the inspector buffer, or nil if no inspector buffer exists."


+ 71
- 95
indium-interaction.el View File

@@ -32,86 +32,82 @@
(require 'xref)
(require 'easymenu)

(require 'indium-workspace)
(require 'indium-backend)
(require 'indium-client)
(require 'indium-inspector)
(require 'indium-breakpoint)
(require 'indium-repl)
(require 'indium-render)
(require 'indium-nodejs)
(require 'indium-chrome)
(require 'indium-debugger)

(declare-function indium-backend-activate-breakpoints "indium-backend.el")
(declare-function indium-backend-deactivate-breakpoints "indium-backend.el")
(declare-function indium-workspace-make-url "indium-workspace.el")

(declare-function indium-connect-to-chrome "indium-chrome.el")
(declare-function indium-connect-to-nodejs "indium-nodejs.el")
(declare-function indium-launch-chrome "indium-chrome.el")
(declare-function indium-launch-nodejs "indium-nodejs.el")
(declare-function indium-debugger-select-frame "indium-debugger.el")

(defvar indium-update-script-source-hook nil
"Hook run when script source is updated.")

;;;###autoload
(defun indium-connect ()
"Open a new connection to a runtime."
(interactive)
(indium-maybe-quit)
(unless-indium-connected
(indium-workspace-read-configuration)
(pcase (map-elt indium-workspace-configuration 'type)
("node" (indium-connect-to-nodejs))
("chrome" (indium-connect-to-chrome))
(_ (user-error "Invalid project type, check the .indium.json project file")))))
(unless (indium-client-process-live-p)
(let ((dir (expand-file-name default-directory)))
(indium-client-start
(lambda ()
(indium-client-list-configurations
dir
(lambda (configurations)
(when-let ((conf (indium-interaction--read-configuration configurations)))
(indium-client-connect dir (map-elt conf 'name))))))))))

;;;###autoload
(defun indium-launch ()
"Start a process (web browser or NodeJS) and attempt to connect to it."
"Start a new process and connect to it."
(interactive)
(indium-maybe-quit)
(unless-indium-connected
(indium-workspace-read-configuration)
(pcase (map-elt indium-workspace-configuration 'type)
("node" (indium-launch-nodejs))
("chrome" (indium-launch-chrome))
(_ (user-error "Invalid project type, check the .indium.json project file")))))
(unless (indium-client-process-live-p)
(let ((dir (expand-file-name default-directory)))
(indium-client-start
(lambda ()
(indium-client-list-configurations
dir
(lambda (configurations)
(when-let ((conf (indium-interaction--read-configuration configurations)))
(pcase (map-elt conf 'type)
("node" (indium-launch-nodejs conf))
("chrome" (indium-launch-chrome conf))
(_ (error "Unsupported configuration")))))))))))

(defun indium-interaction--read-configuration (configurations)
"Prompt the user for a configuration from CONFIGURATIONS."
(let ((configuration-names (seq-map (lambda (configuration)
(map-elt configuration 'name))
configurations)))
(unless configuration-names
(user-error "No configuration name provided in the project file"))
(if (= (seq-length configuration-names) 1)
(seq-elt configurations 0)
(when-let ((name (completing-read "Choose a configuration: "
configuration-names nil t)))
(seq-find (lambda (conf)
(equal (map-elt conf 'name) name))
configurations)))))

(defun indium-quit ()
"Close the current connection and kill its REPL buffer if any.
If a process is attached to the connection, kill it as well.
When called interactively, prompt for a confirmation first."
"Close the current connection and kill its REPL buffer if any."
(interactive)
(unless-indium-connected
(user-error "No active connection to close"))
(when (or (not (called-interactively-p 'interactive))
(y-or-n-p (format "Do you really want to close the connection to %s ? "
(indium-current-connection-url))))
(let ((process (indium-current-connection-process)))
(indium-backend-close-connection (indium-current-connection-backend))
(indium-backend-cleanup-buffers)
(when (and process
(memq (process-status process)
'(run stop open listen)))
(kill-process process))
(setq indium-current-connection nil)
(setq indium-workspace-configuration nil))))
(indium-client-stop)
(indium-interaction--cleanup-buffers))

(defun indium-maybe-quit ()
"Close the current connection.

Unlike `indium-quit', do not signal an error when there is no
active connection."
(when-indium-connected
(call-interactively #'indium-quit)))

(defun indium-reconnect ()
"Try to re-establish a connection.
The new connection is based on the current (usually closed) one."
(interactive)
(unless-indium-connected
(user-error "No Indium connection to reconnect to"))
(indium-backend-reconnect (indium-current-connection-backend)))
(when (and (indium-client-process-live-p)
(yes-or-no-p "Do you want to close the current Indium process?"))
(indium-quit)))

(defun indium-eval (string &optional callback)
@@ -121,12 +117,11 @@ When CALLBACK is non-nil, evaluate CALLBACK with the result.
When called interactively, prompt the user for the string to be
evaluated."
(interactive "sEvaluate JavaScript: ")
(indium-backend-evaluate (indium-current-connection-backend) string callback))
(indium-client-evaluate string callback))

(defun indium-eval-buffer ()
"Evaluate the accessible portion of current buffer."
(interactive)
(indium-interaction--ensure-connection)
(indium-eval (buffer-string)))

(defun indium-eval-region (start end)
@@ -156,9 +151,7 @@ The point is moved to the top stack frame.

If there is no debugging session, signal an error."
(interactive)
(unless (indium-current-connection-frames)
(user-error "No debugger to switch to"))
(indium-debugger-select-frame (seq-elt (indium-current-connection-frames) 0)))
(indium-debugger-switch-to-debugger-buffer))

(defvar indium-interaction-eval-node-hook nil
"Hooks to run after evaluating node before the point.")
@@ -167,12 +160,11 @@ If there is no debugging session, signal an error."
(defun indium-interaction--eval-node (node &optional print)
"Evaluate the AST node NODE.
If PRINT is non-nil, print the output into the current buffer."
(indium-interaction--ensure-connection)
(js2-mode-wait-for-parse
(lambda ()
(indium-eval (js2-node-string node)
(lambda (value _error)
(let ((description (indium-render-value-to-string value)))
(lambda (value)
(let ((description (indium-render-remote-object-to-string value)))
(if print
(save-excursion
(insert description))
@@ -181,13 +173,11 @@ If PRINT is non-nil, print the output into the current buffer."
(defun indium-reload ()
"Reload the page."
(interactive)
(indium-interaction--ensure-connection)
(indium-backend-evaluate (indium-current-connection-backend) "window.location.reload()"))
(indium-client-evaluate "window.location.reload()"))

(defun indium-inspect-last-node ()
"Evaluate and inspect the node before point."
(interactive)
(indium-interaction--ensure-connection)
(js2-mode-wait-for-parse
(lambda ()
(indium-inspect-expression
@@ -196,19 +186,19 @@ If PRINT is non-nil, print the output into the current buffer."
(defun indium-inspect-expression (expression)
"Prompt for EXPRESSION to be inspected."
(interactive "sInspect expression: ")
(indium-interaction--ensure-connection)
(indium-eval expression
(lambda (result _error)
(lambda (result)
(indium-inspector-inspect result))))

(defun indium-switch-to-repl-buffer ()
"Switch to the repl buffer if any."
(interactive)
(if-let ((buf (indium-repl-get-buffer)))
(progn
(setq indium-repl-switch-from-buffer (current-buffer))
(pop-to-buffer buf t))
(user-error "No REPL buffer open")))
(if (indium-client-process-live-p)
(let ((buf (indium-repl-get-buffer-create)))
(progn
(setq indium-repl-switch-from-buffer (current-buffer))
(pop-to-buffer buf t)))
(user-error "Not connected, cannot open REPL buffer")))

(defun indium-toggle-breakpoint ()
"Add or remove a breakpoint on current line."
@@ -270,13 +260,13 @@ If there is no breakpoint, signal an error."
Breakpoints are not removed, but the runtime won't pause when
hitting a breakpoint."
(interactive)
(indium-backend-deactivate-breakpoints (indium-current-connection-backend))
(indium-client-deactivate-breakpoints)
(message "Breakpoints deactivated"))

(defun indium-activate-breakpoints ()
"Activate all breakpoints in all buffers."
(interactive)
(indium-backend-activate-breakpoints (indium-current-connection-backend))
(indium-client-activate-breakpoints)
(message "Breakpoints activated"))

(defun indium-list-breakpoints ()
@@ -342,11 +332,6 @@ hitting a breakpoint."
(setq node parent))
node)))

(defun indium-interaction--ensure-connection ()
"Signal an error if there is no indium connection."
(unless-indium-connected
(user-error "No Indium connection")))

(defvar indium-interaction-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-x C-e") #'indium-eval-last-node)
@@ -354,7 +339,6 @@ hitting a breakpoint."
(define-key map (kbd "C-c M-i") #'indium-inspect-last-node)
(define-key map (kbd "C-c M-:") #'indium-inspect-expression)
(define-key map (kbd "C-c C-z") #'indium-switch-to-repl-buffer)
(define-key map (kbd "C-c C-k") #'indium-update-script-source)
(define-key map [left-fringe mouse-1] #'indium-mouse-toggle-breakpoint)
(define-key map [left-margin mouse-1] #'indium-mouse-toggle-breakpoint)
(define-key map (kbd "C-c b t") #'indium-toggle-breakpoint)
@@ -401,31 +385,24 @@ hitting a breakpoint."
"Function to be evaluated when `indium-interaction-mode' is turned off."
(indium-breakpoint-remove-overlays-from-current-buffer))

(defun indium-interaction-update ()
"Update breakpoints and script source of the current buffer."
(when (and indium-interaction-mode indium-current-connection)
(indium-update-script-source)))

(defun indium-interaction-kill-buffer ()
"Remove all breakpoints prior to killing the current buffer."
(when indium-interaction-mode
(indium-breakpoint-remove-breakpoints-from-current-buffer)))

(defun indium-update-script-source ()
"Update the script source of the backend from the current buffer.
update all breakpoints set in the current buffer as well."
(interactive)
(when-let ((url (indium-workspace-make-url buffer-file-name)))
(indium-backend-set-script-source
(indium-current-connection-backend)
url
(buffer-string)
(lambda ()
(run-hook-with-args 'indium-update-script-source-hook url)))))
(defun indium-interaction--cleanup-buffers ()
"Cleanup all Indium buffers after a connection is closed."
(seq-map (lambda (buf)
(with-current-buffer buf
(when buffer-file-name
(indium-debugger-unset-current-buffer))))
(buffer-list))
(when-let ((buf (indium-repl-get-buffer)))
(kill-buffer buf)))

(defun indium-interaction--guard-breakpoint-at-point ()
"Signal an error if there is no breakpoint on the current line."
(unless (indium-breakpoint-at-point)
(unless (indium-breakpoint-on-current-line-p)
(user-error "No breakpoint on the current line")))

(defun indium-interaction--guard-no-breakpoint-at-point ()
@@ -433,7 +410,6 @@ update all breakpoints set in the current buffer as well."
(when (indium-breakpoint-at-point)
(user-error "There is already a breakpoint on the current line")))

(add-hook 'after-save-hook #'indium-interaction-update)
(add-hook 'kill-buffer-hook #'indium-interaction-kill-buffer)

(provide 'indium-interaction)


+ 0
- 67
indium-list-scripts.el View File

@@ -1,67 +0,0 @@
;;; indium-list-scripts.el --- List parsed scripts -*- lexical-binding: t; -*-

;; Copyright (C) 2017-2018 Nicolas Petton

;; Author: Nicolas Petton <nicolas@petton.fr>

;; 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:

;;

;;; Code:
(require 'indium-script)
(require 'indium-structs)

(require 'map)
(require 'tabulated-list)

;;;###autoload
(defun indium-list-scripts ()
"Display a list of parsed scripts."
(interactive)
(unless-indium-connected
(user-error "Connect Indium to a runtime first"))
(let ((buf (get-buffer-create "*Indium scripts*")))
(with-current-buffer buf
(indium-list-scripts-mode)
(indium-list-scripts--refresh)
(tabulated-list-print))
(display-buffer buf)))

(define-derived-mode indium-list-scripts-mode tabulated-list-mode "Indium list scripts"
"Major mode for listing parsed JavaScript scripts."
(setq tabulated-list-format [("Script source" 0 t)])
(add-hook 'tabulated-list-revert-hook 'indium-list-scripts--refresh nil t)
(tabulated-list-init-header))

(defun indium-list-scripts--refresh ()
"Refresh the list of parsed scripts."
(setq tabulated-list-entries
(map-apply (lambda (_ script)
(indium-list-scripts--make-entry script))
(indium-current-connection-scripts))))

(defun indium-list-scripts--make-entry (script)
"Return a tabulated list entry for SCRIPT."
(list (indium-script-id script)
(make-vector 1 (if-let ((file (indium-script-get-file script)))
(cons (indium-script-url script)
(list 'action (lambda (&rest _)
(find-file file))))
(indium-script-url script)))))

(provide 'indium-list-scripts)
;;; indium-list-scripts.el ends here

+ 98
- 0
indium-list-sources.el View File

<
@@ -0,0 +1,98 @@
;;; indium-list-scripts.el --- List script and sourcemap mappings -*- lexical-binding: t; -*-

;; Copyright (C) 2017-2018 Nicolas Petton

;; Author: Nicolas Petton <nicolas@petton.fr>

;; 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