From 216635e134e450b83efc11ea067ae9c8708a6d75 Mon Sep 17 00:00:00 2001 From: Nicolas Petton Date: Wed, 1 Aug 2018 23:43:51 +0200 Subject: [PATCH] Rewrite Indium to work as a client for the new server --- indium-backend.el | 156 ---- indium-breakpoint.el | 114 ++- indium-chrome.el | 109 +-- indium-client.el | 425 +++++++++++ indium-debugger-frames.el | 136 ---- indium-debugger-litable.el | 43 +- indium-debugger-locals.el | 10 +- indium-debugger.el | 223 ++++-- indium-inspector.el | 50 +- indium-interaction.el | 166 ++--- indium-list-scripts.el | 67 -- indium-list-sources.el | 98 +++ indium-nodejs.el | 168 ++--- indium-render.el | 133 ++-- indium-repl.el | 169 ++--- indium-scratch.el | 6 +- indium-script.el | 343 --------- indium-sourcemap.el | 394 ---------- indium-structs.el | 306 ++++---- indium-v8.el | 702 ------------------ indium-workspace.el | 286 ------- indium.el | 7 +- test/fixtures/.indium.json | 9 - test/fixtures/test-with-output.js | 1 - test/fixtures/test.js | 3 - .../indium-nodejs-integration-test.el | 81 -- .../indium-repl-integration-test.el | 64 -- test/unit/indium-backend-test.el | 39 - test/unit/indium-breakpoint-test.el | 55 +- test/unit/indium-chrome-test.el | 129 ++-- test/unit/indium-debugger-test.el | 133 ++-- test/unit/indium-inspector-test.el | 17 +- test/unit/indium-interaction-test.el | 118 +-- test/unit/indium-list-scripts-test.el | 46 -- test/unit/indium-nodejs-test.el | 53 +- test/unit/indium-repl-test.el | 15 +- test/unit/indium-script-test.el | 214 ------ test/unit/indium-sourcemap-test.el | 134 ---- test/unit/indium-structs-test.el | 146 +++- test/unit/indium-v8-test.el | 195 ----- test/unit/indium-workspace-test.el | 190 ----- test/unit/wsc-test.el | 158 ---- wsc.el | 482 ------------ 43 files changed, 1459 insertions(+), 4934 deletions(-) delete mode 100644 indium-backend.el create mode 100644 indium-client.el delete mode 100644 indium-debugger-frames.el delete mode 100644 indium-list-scripts.el create mode 100644 indium-list-sources.el delete mode 100644 indium-script.el delete mode 100644 indium-sourcemap.el delete mode 100644 indium-v8.el delete mode 100644 indium-workspace.el delete mode 100644 test/fixtures/.indium.json delete mode 100644 test/fixtures/test-with-output.js delete mode 100644 test/fixtures/test.js delete mode 100644 test/integration/indium-nodejs-integration-test.el delete mode 100644 test/integration/indium-repl-integration-test.el delete mode 100644 test/unit/indium-backend-test.el delete mode 100644 test/unit/indium-list-scripts-test.el delete mode 100644 test/unit/indium-script-test.el delete mode 100644 test/unit/indium-sourcemap-test.el delete mode 100644 test/unit/indium-v8-test.el delete mode 100644 test/unit/indium-workspace-test.el delete mode 100644 test/unit/wsc-test.el delete mode 100644 wsc.el diff --git a/indium-backend.el b/indium-backend.el deleted file mode 100644 index b8bc78f..0000000 --- a/indium-backend.el +++ /dev/null @@ -1,156 +0,0 @@ -;;; indium-backend.el --- Backend for indium.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2016-2018 Nicolas Petton - -;; Author: Nicolas Petton -;; 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 . - -;;; 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 diff --git a/indium-breakpoint.el b/indium-breakpoint.el index 460c99d..8fa938e 100644 --- a/indium-breakpoint.el +++ b/indium-breakpoint.el @@ -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 diff --git a/indium-chrome.el b/indium-chrome.el index 0b1320a..1bd1b40 100644 --- a/indium-chrome.el +++ b/indium-chrome.el @@ -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 diff --git a/indium-client.el b/indium-client.el new file mode 100644 index 0000000..5b84fd7 --- /dev/null +++ b/indium-client.el @@ -0,0 +1,425 @@ +;;; indium-client.el --- Indium process client -*- lexical-binding: t; -*- + +;; Copyright (C) 2018 Nicolas Petton + +;; Author: Nicolas Petton + +;; 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 . + +;;; 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 diff --git a/indium-debugger-frames.el b/indium-debugger-frames.el deleted file mode 100644 index 89457c8..0000000 --- a/indium-debugger-frames.el +++ /dev/null @@ -1,136 +0,0 @@ -;;; indium-debugger-frames.el --- List the stack frame -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton - -;; 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 . - -;;; 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 diff --git a/indium-debugger-litable.el b/indium-debugger-litable.el index 6c97e52..a040930 100644 --- a/indium-debugger-litable.el +++ b/indium-debugger-litable.el @@ -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))) diff --git a/indium-debugger-locals.el b/indium-debugger-locals.el index f6daf70..c61063c 100644 --- a/indium-debugger-locals.el +++ b/indium-debugger-locals.el @@ -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 diff --git a/indium-debugger.el b/indium-debugger.el index 52cd54c..a6c71e0 100644 --- a/indium-debugger.el +++ b/indium-debugger.el @@ -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 diff --git a/indium-inspector.el b/indium-inspector.el index dc4bc04..62fd9fa 100644 --- a/indium-inspector.el +++ b/indium-inspector.el @@ -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." diff --git a/indium-interaction.el b/indium-interaction.el index 060452e..00a4336 100644 --- a/indium-interaction.el +++ b/indium-interaction.el @@ -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) diff --git a/indium-list-scripts.el b/indium-list-scripts.el deleted file mode 100644 index edb357f..0000000 --- a/indium-list-scripts.el +++ /dev/null @@ -1,67 +0,0 @@ -;;; indium-list-scripts.el --- List parsed scripts -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton - -;; 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 . - -;;; 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 diff --git a/indium-list-sources.el b/indium-list-sources.el new file mode 100644 index 0000000..d7a52fc --- /dev/null +++ b/indium-list-sources.el @@ -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 + +;; 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 . + +;;; Commentary: +;; +;; This file provides commands useful for debugging project configuration +;; issues when breakpoints or sourcemaps do not work. +;; +;; - `indium-list-sourcemap-sources': List all sourcemap sources, as resolved to +;; disk files. This commands helps understanding how Indium maps sourcemaps to +;; file paths using the `.indium.json' project file. +;; +;; - `indium-list-script-sources': List all script parsed by the backend. Their +;; source file is resolved to a file on disk when possible. + +;;; Code: + + +(require 'indium-client) + +(require 'map) +(require 'tabulated-list) + +(defvar indium-list-sources-function nil + "Function used to fetch a list of sources.") + +(make-local-variable 'indium-list-sources-function) + +;;;###autoload +(defun indium-list-sourcemap-sources () + "Display a list of all resolved sourcemap sources." + (interactive) + (let ((buf (get-buffer-create (indium-list-sources-buffer-name)))) + (with-current-buffer buf + (setq indium-list-sources-function #'indium-client-get-sourcemap-sources) + (indium-list-sources-mode) + (indium-list-sources--refresh)) + (display-buffer buf))) + +;;;###autoload +(defun indium-list-script-sources () + "Display a list of all resolved script sources." + (interactive) + (let ((buf (get-buffer-create (indium-list-sources-buffer-name)))) + (with-current-buffer buf + (setq indium-list-sources-function #'indium-client-get-script-sources) + (indium-list-sources-mode) + (indium-list-sources--refresh)) + (display-buffer buf))) + +(defun indium-list-sources-buffer-name () + "Return the name of the buffer used to list sources." + "*Indium sources*") + +(define-derived-mode indium-list-sources-mode tabulated-list-mode "Indium list sources" + "Major mode for listing sources." + (setq tabulated-list-format [("sources" 0 t)]) + (add-hook 'tabulated-list-revert-hook 'indium-list-sources--refresh nil t) + (tabulated-list-init-header)) + +(defun indium-list-sources--refresh () + "Refresh the list of parsed scripts." + (funcall indium-list-sources-function + (lambda (sources) + (with-current-buffer (get-buffer (indium-list-sources-buffer-name)) + (setq tabulated-list-entries + (seq-map (lambda (source) + (indium-list-sources--make-entry source)) + (seq-filter #'identity sources))) + (tabulated-list-print))))) + +(defun indium-list-sources--make-entry (source) + "Return a tabulated list entry for SOURCE." + (list nil + (make-vector 1 (if (file-exists-p source) + (cons source + (list 'action (lambda (&rest _) + (find-file source)))) + (propertize source 'font-lock-face 'error))))) + +(provide 'indium-list-sources) +;;; indium-list-sources.el ends here diff --git a/indium-nodejs.el b/indium-nodejs.el index b12759f..1e2c54b 100644 --- a/indium-nodejs.el +++ b/indium-nodejs.el @@ -34,144 +34,60 @@ (require 'seq) (require 'subr-x) -(require 'indium-v8) -(require 'indium-workspace) - -(defgroup indium-nodejs nil - "Indium NodeJS." - :prefix "indium-nodejs-" - :group 'indium) - -(defcustom indium-nodejs-default-inspect-brk t - "When non-nil, break the execution at the first statement." - :type 'boolean) - -(defcustom indium-nodejs-default-host - "localhost" - "Default NodeJS remote debugger host." - :type 'string) - -(defcustom indium-nodejs-default-port - 9229 - "Default NodeJS remote debugger port." - :type 'integer) - -(defun indium-nodejs--command () - "Return the command to be run to start a node process. -The command is read from the workspace configuration file." - (let ((command (map-elt indium-workspace-configuration 'command))) - (unless command - (user-error "No NodeJS command specified in the .indium.json file")) - command)) +(declare-function indium-client-connect "indium-client.el") -(defun indium-launch-nodejs () +(defun indium-launch-nodejs (conf) "Start a NodeJS process. -Execute the command based on `indium-nodejs--command', adding the -`--inspect' flag. When the process is ready, open an Indium -connection on it. +Execute the command specified in CONF, adding the `--inspect' +flag. When the process is ready, open an Indium connection on +it. -If `indium-nodejs--inspect-brk' is set to non-nil, break the +If the configuration setting `inspect-brk' is non-nil, break the execution at the first statement." - (let* ((default-directory (indium-workspace-root)) - (process (make-process :name "indium-nodejs-process" - :buffer "*node process*" - :filter #'indium-nodejs--process-filter - :command (list shell-file-name - shell-command-switch - (indium-nodejs--command-with-flags))))) - (switch-to-buffer (process-buffer process)))) - -(defun indium-connect-to-nodejs () - "Open a connection to an existing NodeJS process." - (let* ((host (indium-nodejs--host)) - (port (indium-nodejs--port)) - (default-directory (indium-workspace-root))) - (indium-nodejs--get-process-id host - port - (lambda (id) - (indium-nodejs--connect host port id))))) - + (let-alist conf + (unless .command + (user-error "No NodeJS command specified in the .indium.json file")) + (let* ((default-directory .resolvedRoot) + (filter (indium-nodejs--process-filter-function conf)) + (process (make-process :name "indium-nodejs-process" + :buffer "*node process*" + :filter filter + :command (list shell-file-name + shell-command-switch + (indium-nodejs--command-with-flags + .command + .inspect-brk))))) + (switch-to-buffer (process-buffer process))))) -(defun indium-nodejs--host () - "Return the debugging host for a NodeJS process. -The host is either read from the workpace configuration file or -`indium-nodejs-default-host'." - (map-elt indium-workspace-configuration 'host indium-nodejs-default-host)) - -(defun indium-nodejs--port () - "Return the debugging port for a NodeJS process. -The port is either read from the workpace configuration file or -`indium-nodejs-default-port'." - (map-elt indium-workspace-configuration 'port indium-nodejs-default-port)) - -(defun indium-nodejs--inspect-brk () - "Return non nil if the option `--inspect-brk' should be used. -The setting is either read from the workpace configuration file or -`indium-nodejs-default-inspect-brk'." - (map-elt indium-workspace-configuration 'inspect-brk)) +(defun indium-nodejs--command-with-flags (command inspect-brk) + "Return COMMAND with flags to start the V8 inspector. -(defun indium-nodejs--get-process-id (host port callback) - "Get the id of the websocket path at HOST:PORT and evaluate CALLBACK with it." - (url-retrieve (format "http://%s:%s/json/list" host port) - (lambda (status) - (funcall callback (if (eq :error (car status)) - nil - (indium-nodejs--read-process-id)))))) - -(defun indium-nodejs--read-process-id () - "Return the process id read from the JSON data in the current buffer." - (when (save-match-data - (looking-at "^HTTP/.* 200 OK$")) - (goto-char (point-min)) - (search-forward "\n\n") - (delete-region (point-min) (point)) - (map-elt (seq-elt (json-read) 0) 'id))) - - -(defun indium-nodejs--connect (host port path &optional process) - "Ask the user for a websocket url HOST:PORT/PATH and connects to it. -When PROCESS is non-nil, attach it to the connection." - (indium-maybe-quit) - (unless indium-current-connection - (let ((websocket-url (format "ws://%s:%s/%s" host port path)) - (url (format "file://%s" default-directory))) - (indium-v8--open-ws-connection url - websocket-url - (when process - (lambda () - (setf (indium-current-connection-process) process))) - t)))) - -(defun indium-nodejs--command-with-flags () - "Return the command to be run with the `--inspect' or `--inspect-brk' flag." - (let ((command (indium-nodejs--command)) - (inspect-flag (if (indium-nodejs--inspect-brk) - "--inspect-brk" - "--inspect"))) +If INSPECT-BRK is nil, use the `--inspect', use the +`--inspect-brk' flag otherwise." + (let ((inspect-flag (if (eq inspect-brk t) "--inspect-brk" "--inspect"))) (if (string-match "\\" command) (replace-match (concat "node " inspect-flag) nil nil command) (user-error "Invalid command specified")))) -(defun indium-nodejs--process-filter (process output) - "Filter function for PROCESS. -Append OUTPUT to the PROCESS buffer, and parse it to detect the -socket URL to connect to." - ;; Append output to the process buffer - (with-current-buffer (process-buffer process) - (goto-char (point-max)) - (insert output)) - (when (string-match-p "Debugger listening on" output) - (ignore-errors - (indium-nodejs--connect-to-process process output)))) - -(defun indium-nodejs--connect-to-process (process output) - "If PROCESS OUTPUT contain the WS url, connect to it." - (save-match-data - (string-match "://.*/\\(.*\\)$" output) - (when-let ((path (match-string 1 output))) - (indium-nodejs--connect "127.0.0.1" "9229" path process)))) +(defun indium-nodejs--process-filter-function (conf) + "Return a process filter function for CONF. +The function detects the socket URL to connect to from the +process output." + (let ((connected)) + (lambda (process output) + ;; Append output to the process buffer + (with-current-buffer (process-buffer process) + (goto-char (point-max)) + (insert output)) + (when (and (not connected) + (string-match-p "Debugger listening on" output)) + ;; Node will keep outputing the "Debugger listening on" message after + ;; each deconnection, so only try to connect one. + (setq connected t) + (let-alist conf + (indium-client-connect (file-name-directory .projectFile) .name)))))) (provide 'indium-nodejs) ;;; indium-nodejs.el ends here diff --git a/indium-render.el b/indium-render.el index 108e9fa..a2b7666 100644 --- a/indium-render.el +++ b/indium-render.el @@ -24,46 +24,39 @@ ;;; Code: -(require 'indium-faces) (require 'seq) (require 'indium-seq-fix) -(require 'map) -(declare-function indium-backend-object-reference-p "indium-backend.el") +(require 'indium-faces) +(require 'indium-structs) + (declare-function indium-debugger-frames-select-frame "indium-debugger.el") (declare-function indium-inspector-inspect "indium-inspector.el") -(defun indium-render-values (values &optional separator) - "Render VALUES separated by SEPARATOR. -If no SEPARATOR is provided, separate VALUES by a space." - (unless separator (setq separator " ")) - (let ((length (seq-length values))) - (seq-map-indexed (lambda (value index) - (indium-render-value value) - (unless (<= (1- length) index) - (insert separator))) - values))) - -(defun indium-render-value (value) - "Render VALUE, based on its object type. -If VALUE represents a reference to a remote object, render it -with a link to an inspector on that object." - (if (indium-backend-object-reference-p value) - (indium-render-object-link value) - (indium-render-description value))) - -(defun indium-render-value-to-string (value) - "Return a string representation of VALUE." +(defun indium-render-remote-object (obj) + "Render OBJ, based on its object type. +If OBJ represents a reference to an object, render it with a link +to an inspector on that object." + (cond + ((indium-remote-object-error-p obj) + (indium-render-description obj 'indium-repl-error-face)) + ((indium-remote-object-reference-p obj) + (indium-render-object-link obj)) + (t + (indium-render-description obj 'indium-repl-stdout-face)))) + +(defun indium-render-remote-object-to-string (obj) + "Return a string representation of OBJ." (with-temp-buffer - (indium-render-value value) + (indium-render-remote-object obj) (buffer-string))) -(defun indium-render-description (value) - "Insert VALUE fontified as a description." - (let ((description (indium-description-string value))) +(defun indium-render-description (obj face) + "Insert OBJ fontified with FACE as a description." + (let ((description (indium-remote-object-to-string obj))) (insert (propertize description - 'font-lock-face 'indium-repl-stdout-face + 'font-lock-face face 'rear-nonsticky '(font-lock-face))))) (defun indium-render-keyword (string) @@ -91,38 +84,9 @@ ACTION should be a function that takes no argument." 'font-lock-face 'indium-header-face 'rear-nonsticky '(font-lock-face)))) -(defun indium-render-frame (frame url current) - "Render the stack frame FRAME with the URL of its script. -If CURRENT is non-nil, FRAME rendered as the current frame. When -clicked, jump in the debugger to the frame." - (insert (if current "* " " ")) - (insert (propertize (indium-render--frame-label frame) - 'font-lock-face (if current - 'indium-highlight-face - 'indium-link-face) - 'rear-nonsticky '(font-lock-face indium-action) - 'indium-action (lambda (&rest _) - (indium-debugger-frames-select-frame frame)))) - (when url - (insert (propertize (format " <%s>" url) - 'font-lock-face 'indium-frame-url-face)))) - -(defun indium-description-string (value &optional full) - "Return a short string describing VALUE. - -When FULL is non-nil, do not strip long descriptions and function -definitions." - (let ((description (map-elt value 'description)) - (type (map-elt value 'type))) - ;; Showing the source code of the function is too verbose - (if (and (not full) (eq type 'function)) - "function" - description))) - -(defun indium-render-object-link (value) - "Render VALUE as a link, with an optional preview." - (let* ((description (indium-description-string value)) - (preview (map-elt value 'preview)) +(defun indium-render-object-link (obj) + "Render OBJ as a link, with an optional preview." + (let* ((description (indium-remote-object-to-string obj)) (beg (point)) (end (progn (insert (indium-render--truncate-string-to-newline description)) @@ -131,24 +95,24 @@ definitions." (set-text-properties beg end `(font-lock-face ,face mouse-face highlight - indium-reference ,value)) - (when preview - (insert (format " %s" preview))))) + indium-reference ,obj)) + (when (indium-remote-object-has-preview-p obj) + (insert (format " %s" (indium-remote-object-preview obj)))))) (defun indium-render-properties (properties) "Insert all items in PROPERTIES sorted by name." (seq-map #'indium-render-property (seq-sort (lambda (p1 p2) - (string< (map-elt p1 'name) - (map-elt p2 'name))) + (string< (indium-property-name p1) + (indium-property-name p2))) properties))) (defun indium-render-property (property &optional separator) - "Insert the remote reference PROPERTY as a value. + "Insert the PROPERTY rendered as a remote object. When SEPARATOR is non-nil, insert it after the property. Otherwise, insert a newline." - (insert " " (map-elt property 'name) ": ") - (indium-render-value (map-elt property 'value)) + (insert " " (indium-property-name property) ": ") + (indium-render-remote-object (indium-property-remote-object property)) (insert (or separator "\n"))) (defun indium-render-property-to-string (property) @@ -157,6 +121,30 @@ Otherwise, insert a newline." (indium-render-property property "") (buffer-string))) +(defun indium-render-frame (frame current) + "Render the stack frame FRAME. +If CURRENT is non-nil, FRAME rendered as the current frame. When +clicked, jump in the debugger to the frame." + (let ((file (indium-location-file (indium-frame-location frame)))) + (insert (if current "* " " ")) + (insert (propertize (indium-render--frame-label frame) + 'font-lock-face (if current + 'indium-highlight-face + 'indium-link-face) + 'rear-nonsticky '(font-lock-face indium-action) + 'indium-action (lambda (&rest _) + (indium-debugger-frames-select-frame frame)))) + (when (not (string-empty-p file)) + (insert (propertize (format " <%s>" file) + 'font-lock-face 'indium-frame-url-face))))) + +(defun indium-render--frame-label (frame) + "Return the label for FRAME to be used in the debugger stack frame list." + (let ((label (indium-frame-function-name frame))) + (if (seq-empty-p label) + "Closure" + label))) + (declare #'indium-inspector-inspect) (defun indium-follow-link () @@ -193,12 +181,5 @@ If STRING is truncated, append ellipsis." (setq result (concat result "…"))) result)) -(defun indium-render--frame-label (frame) - "Return the label for FRAME to be used in the debugger stack frame list." - (let ((label (indium-frame-function-name frame))) - (if (seq-empty-p label) - (or (indium-frame-type frame) "Closure") - label))) - (provide 'indium-render) ;;; indium-render.el ends here diff --git a/indium-repl.el b/indium-repl.el index 5ede8e2..e7a066f 100644 --- a/indium-repl.el +++ b/indium-repl.el @@ -26,7 +26,7 @@ (require 'indium-render) (require 'indium-faces) -(require 'indium-backend) +(require 'indium-client) (require 'company) (require 'easymenu) @@ -36,8 +36,8 @@ (require 'subr-x) (require 'ansi-color) -(declare-function indium-workspace-lookup-file-safe "indium-workspace.el") (declare-function indium-inspector-inspect "indium-inspector.el") +(declare-function indium-maybe-quit "indium-interaction.el") (defgroup indium-repl nil "Interaction with the REPL." @@ -67,6 +67,10 @@ (prog1 (progn . ,body) (set-marker ,marker ,pos))))) +(defun indium-repl-setup () + "Create and switch to the REPL buffer." + (switch-to-buffer (indium-repl-get-buffer-create))) + (defun indium-repl-get-buffer-create () "Return a new REPL buffer." (let* ((buf (get-buffer-create (indium-repl-buffer-name)))) @@ -84,19 +88,20 @@ (defun indium-repl-setup-buffer (buffer) "Setup the REPL BUFFER." (with-current-buffer buffer - (indium-repl-mode) - (indium-repl-setup-markers) - (indium-repl-mark-output-start) - (indium-repl-insert-prompt) - (indium-repl-mark-input-start) - (indium-repl-emit-console-message `((text . ,(indium-repl--welcome-message)))))) + (unless (eq major-mode 'indium-repl-mode) + (indium-repl-mode) + (indium-repl-setup-markers) + (indium-repl-mark-output-start) + (indium-repl-insert-prompt) + (indium-repl-mark-input-start) + (indium-repl-emit-console-message + `((result . ,(indium-remote-object-create + :description (indium-repl--welcome-message)))))))) (defun indium-repl--welcome-message () "Return the welcome message displayed in new REPL buffers." - (format - (substitute-command-keys - "/* Welcome to Indium! -Connected to %s @ %s + (substitute-command-keys + "/* Welcome to Indium! Getting started: @@ -110,10 +115,7 @@ To disconnect from the JavaScript process, press <\\[indium-quit]>. Doing this will also close all inspectors and debugger buffers connected to the process. -*/") - (indium-current-connection-backend) - (indium-current-connection-url))) - +*/")) (defun indium-repl-setup-markers () "Setup the initial markers for the current REPL buffer." @@ -163,9 +165,8 @@ connected to the process. (defun indium-repl-inspect () "Inspect the result of the evaluation of the input at point." (interactive) - (indium-backend-evaluate (indium-current-connection-backend) - (indium-repl--input-content) - (lambda (result _error) + (indium-client-evaluate (indium-repl--input-content) + (lambda (result) (indium-inspector-inspect result)))) (defun indium-repl--input-content () @@ -176,28 +177,24 @@ connected to the process. "Return t if in input area." (<= indium-repl-input-start-marker (point))) -(declare-function #'indium-backend-evaluate "indium") - (defun indium-repl-evaluate (string) "Evaluate STRING in the browser tab and emit the output." (push string indium-repl-history) - (indium-backend-evaluate (indium-current-connection-backend) string #'indium-repl-emit-value) + (indium-client-evaluate string #'indium-repl-emit-value) ;; move the output markers so that output is put after the current prompt (save-excursion (goto-char (point-max)) (set-marker indium-repl-output-start-marker (point)) (set-marker indium-repl-output-end-marker (point)))) -(defun indium-repl-emit-value (value error) - "Emit a string representation of VALUE. -When ERROR is non-nil, display VALUE as an error." +(defun indium-repl-emit-value (value) + "Emit a string representation of the remote object VALUE." (with-current-buffer (indium-repl-get-buffer) (save-excursion (goto-char (point-max)) (insert-before-markers "\n") (set-marker indium-repl-output-start-marker (point)) - (when error (indium-repl--emit-logging-error)) - (indium-render-value value) + (indium-render-remote-object value) (insert "\n") (indium-repl-mark-input-start) (set-marker indium-repl-output-end-marker (point))) @@ -209,41 +206,33 @@ When ERROR is non-nil, display VALUE as an error." When ERROR is non-nil, display MESSAGE as an error. MESSAGE is a map (alist/hash-table) with the following keys: - text message text to be displayed - description optional additional description - level severity level (can be log, warning, error, debug) type type of message url url of the message origin line line number in the resource that generated this message - values message values to be logged + result object to be logged -MESSAGE must contain `text' or `values.'. Other fields are +MESSAGE must contain `result'. Other fields are optional." (with-current-buffer (indium-repl-get-buffer) - (when (string= (map-elt message 'level) 'error) - (setq error t)) + (let-alist message + (when (string= .type 'error) + (setq error t))) (save-excursion (goto-char indium-repl-output-end-marker) (set-marker indium-repl-output-start-marker (point)) (insert "\n") (when error (indium-repl--emit-logging-error)) - (indium-repl--emit-message-values message) + (indium-repl--emit-message message) (set-marker indium-repl-output-end-marker (point)) (unless (eolp) (insert "\n"))))) -(defun indium-repl--emit-message-values (message) - "Emit all values of console MESSAGE." - (let ((text (map-elt message 'text)) - (values (map-elt message 'values)) - (url (map-elt message 'url)) - (line (map-elt message 'line))) - (when (seq-empty-p values) - (setq values `(((type . "string") - (description . ,text))))) - (indium-render-values values "\n") - (indium-repl--emit-message-url-line url line))) +(defun indium-repl--emit-message (message) + "Emit the value of console MESSAGE." + (let-alist message + (indium-render-remote-object .result) + (indium-repl--emit-message-url-line .url .line))) (defun indium-repl--emit-logging-error () "Emit a red \"Error\" label." @@ -257,17 +246,16 @@ optional." (defun indium-repl--emit-message-url-line (url line) "Emit the URL and LINE for a message." (unless (seq-empty-p url) - (let ((path (indium-workspace-lookup-file-safe url))) - (insert "\nFrom " - (propertize (if line - (format "%s:%s" path line) - path) - 'font-lock-face 'indium-link-face - 'indium-action (lambda () - (if (file-regular-p path) - (find-file path) - (browse-url path))) - 'rear-nonsticky '(font-lock-face indium-action)))))) + (insert "\nFrom " + (propertize (if line + (format "%s:%s" url line) + url) + 'font-lock-face 'indium-link-face + 'indium-action (lambda () + (if (file-regular-p url) + (find-file url) + (browse-url url))) + 'rear-nonsticky '(font-lock-face indium-action))))) (defun indium-repl-next-input () "Insert the content of the next input in the history." @@ -317,31 +305,6 @@ DIRECTION is `forward' or `backard' (in the history list)." (when indium-repl-switch-from-buffer (pop-to-buffer indium-repl-switch-from-buffer t))) -(defun indium-repl--handle-connection-closed () - "Display a message when the connection is closed." - (when-let ((buf (indium-repl-get-buffer))) - (with-current-buffer buf - (save-excursion - (goto-char (point-max)) - (insert-before-markers "\n") - (set-marker indium-repl-output-start-marker (point)) - (insert "Connection closed. ") - (indium-repl--insert-connection-buttons) - (insert "\n") - (set-marker indium-repl-input-start-marker (point)) - (set-marker indium-repl-output-end-marker (point))) - (indium-repl-insert-prompt)))) - -(defun indium-repl--insert-connection-buttons () - "Insert buttons when the connection is lost. - -The user can either close all related buffers or try to reopen -the connection." - (indium-render-button "Reconnect" #'indium-reconnect) - (insert " or ") - (indium-render-button "close all buffers" #'indium-quit) - (insert ".")) - (defun company-indium-repl (command &optional arg &rest _args) "Indium REPL backend for company-mode. See `company-backends' for more info about COMMAND and ARG." @@ -355,18 +318,27 @@ See `company-backends' for more info about COMMAND and ARG." (lambda (callback) (indium-repl-get-completions arg callback)))))) -(defun indium-repl-get-completions (arg callback) - "Get the completion list matching the prefix ARG. +(defun indium-repl-get-completions (prefix callback) + "Get the completion list matching PREFIX. Evaluate CALLBACK with the completion candidates." - (let ((expression (buffer-substring-no-properties - (let ((bol (line-beginning-position)) - (prev-delimiter (1+ (save-excursion - (re-search-backward "[([:space:]]" nil t))))) - (if prev-delimiter - (max bol prev-delimiter) - bol)) - (point)))) - (indium-backend-get-completions (indium-current-connection-backend) expression arg callback))) + (let* ((input (buffer-substring-no-properties + (let ((bol (point-at-bol)) + (prev-delimiter (1+ (save-excursion + (re-search-backward "[([:space:]]" nil t))))) + (if prev-delimiter + (max bol prev-delimiter) + bol)) + (point))) + (expression (if (string-match-p "\\." input) + (replace-regexp-in-string "\\.[^\\.]*$" "" input) + "this"))) + (indium-client-get-completion + expression + (lambda (candidates) + (funcall callback + (seq-filter (lambda (candidate) + (string-prefix-p prefix candidate)) + candidates)))))) (defun indium-repl--complete-or-indent () "Complete or indent at point." @@ -385,8 +357,6 @@ Evaluate CALLBACK with the completion candidates." (defvar indium-repl-mode-hook nil "Hook executed when entering `indium-repl-mode'.") -(declare 'indium-quit) - (defvar indium-repl-mode-map (let ((map (make-sparse-keymap))) (define-key map [return] #'indium-repl-return) @@ -397,7 +367,7 @@ Evaluate CALLBACK with the completion candidates." (define-key map (kbd "C-c M-i") #'indium-repl-inspect) (define-key map (kbd "C-c C-o") #'indium-repl-clear-output) (define-key map (kbd "C-c C-z") #'indium-repl-pop-buffer) - (define-key map (kbd "C-c C-q") #'indium-quit) + (define-key map (kbd "C-c C-q") #'indium-maybe-quit) (define-key map (kbd "M-p") #'indium-repl-previous-input) (define-key map (kbd "M-n") #'indium-repl-next-input) (define-key map (kbd "C-") #'indium-repl-previous-input) @@ -410,7 +380,7 @@ Evaluate CALLBACK with the completion candidates." "--" ["Switch to source buffer" indium-repl-pop-buffer] "--" - ["Quit" indium-quit])) + ["Quit" indium-maybe-quit])) map)) (define-derived-mode indium-repl-mode fundamental-mode "JS-REPL" @@ -428,7 +398,7 @@ Evaluate CALLBACK with the completion candidates." (repl-buffer (current-buffer)) (string (buffer-substring-no-properties start end))) (with-current-buffer - (get-buffer-create "*indium-fontification*") + (get-buffer-create " indium-fontification ") (let ((inhibit-modification-hooks nil)) (js-mode) (erase-buffer) @@ -445,7 +415,8 @@ Evaluate CALLBACK with the completion candidates." repl-buffer))) (setq pos next))))))) -(add-hook 'indium-connection-closed-hook #'indium-repl--handle-connection-closed) +(add-hook 'indium-client-connected-hook #'indium-repl-setup) +(add-hook 'indium-client-log-hook #'indium-repl-emit-console-message) (provide 'indium-repl) ;;; indium-repl.el ends here diff --git a/indium-scratch.el b/indium-scratch.el index d19e301..ee329c8 100644 --- a/indium-scratch.el +++ b/indium-scratch.el @@ -38,11 +38,7 @@ one first." (defun indium-scratch-get-buffer-create () "Return a scratch buffer for the current connection. -If no buffer exists, create one. - -If there is no current connection, throw an error." - (unless-indium-connected - (user-error "No current connection")) +If no buffer exists, create one." (let* ((bufname (indium-scratch-buffer-name)) (buf (get-buffer bufname))) (unless buf diff --git a/indium-script.el b/indium-script.el deleted file mode 100644 index 40d5474..0000000 --- a/indium-script.el +++ /dev/null @@ -1,343 +0,0 @@ -;;; indium-script.el --- Handle scripts for a connection -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton - -;; 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 . - -;;; Commentary: - -;; Handle script source registration, script locations (with sourcemap support) -;; for the current indium connection. - -;;; Code: - -(require 'seq) -(require 'map) -(require 'rx) -(require 'indium-backend) -(require 'indium-structs) -(require 'indium-workspace) -(require 'indium-backend) -(require 'indium-sourcemap) -(require 'url) -(require 'url-http) -(require 'url-handlers) -(require 'subr-x) - -(defgroup indium-script nil - "Indium script and location handling" - :prefix "indium-script-" - :group 'indium) - -(defcustom indium-script-enable-sourcemaps t - "When non-nil, use sourcemaps when debugging." - :type 'boolean) - -(defvar indium-script-default-sourcemap-path-overrides - '(("webpack:///./~/" . "${root}/node_modules/") - ("webpack:///src/" . "${root}/") - ("webpack:///./" . "${root}/") - ("webpack:///" . "/")) - "Sourcemap mappings that are included by default in Indium. -Any override in the workspace configuration will override this -defaults.") - -(defun indium-location-url (location) - "Lookup the url associated with LOCATION's file." - (indium-workspace-make-url (indium-location-file location))) - -(defun indium-script-add-script-parsed (id url &optional sourcemap-url) - "Add a parsed script from the runtime with ID at URL. -If SOURCEMAP-URL is non-nil, add it to the parsed script. - -Return the new parsed script. - -If an existing script has the same URL, remove that script first, -so that the new script overrides it, as we cannot have multiple -parsed scripts with the same URL." - (when-let ((script (indium-script-find-from-url url))) - (map-delete (indium-current-connection-scripts) - (intern (indium-script-id script)))) - (let ((script (indium-script-create :id id - :url url - :sourcemap-url sourcemap-url))) - ;; TODO Should use `indum-current-connection-scripts' but I get a - ;; compilation warning. - (map-put (indium-connection-scripts indium-current-connection) - (intern id) - script) - script)) - -(defun indium-script-find-by-id (id) - "Return the parsed script with id ID in the current connection. -If not such script was parsed, return nil." - (map-elt (indium-current-connection-scripts) (intern id))) - -(defun indium-script-get-file (script &optional ignore-existence) - "Lookup the local file associated with SCRIPT. -If no local file can be found and IGNORE-EXISTENCE is nil, return nil." - (indium-workspace-lookup-file (indium-script-url script) ignore-existence)) - -(defun indium-script-find-from-location (location) - "Return the script associated to LOCATION. - -LOCATION can either be a buffer location or a -generated (sourcemap) script location." - (let ((file (indium-location-file location))) - (or (indium-script-find-from-file file) - (indium-script-find-from-url file)))) - -(defun indium-script-find-from-url (url) - "Lookup a script for URL. -Return nil if no script can be found." - (seq-find #'identity - (map-apply (lambda (_id script) - (when (string= url (indium-script-url script)) - script)) - (indium-current-connection-scripts)))) - -(defun indium-script-find-from-file (file) - "Lookup a script from a local FILE. -Return nil if no script can be found." - (indium-script-find-from-url (indium-workspace-make-url file))) - -(defun indium-script-has-sourcemap-p (script) - "Return non-nil if SCRIPT has an associated sourcemap." - (when-let ((sourcemap-url (indium-script-sourcemap-url script))) - (not (seq-empty-p sourcemap-url)))) - -(defun indium-script-all-scripts-with-sourcemap () - "Return all parsed scripts that contain a sourcemap. -The scripts are sorted by parsed time, to ensure the newest -script is picked up first when using sourcemaps." - (seq-sort (lambda (a b) - (not (time-less-p (indium-script-parsed-time a) - (indium-script-parsed-time b)))) - (seq-filter #'indium-script-has-sourcemap-p - (map-values (indium-current-connection-scripts))))) - -(defun indium-script-get-frame-original-location (frame) - "Return the location stack FRAME, possibly using sourcemaps." - (let* ((script (indium-frame-script frame)) - (location (indium-frame-location frame))) - (if (indium-script-has-sourcemap-p script) - (indium-script-original-location script location) - location))) - -(defun indium-script-original-location (script location) - "Use the sourcemap of SCRIPT to lookup its original LOCATION. -If SCRIPT has no sourcemap, return LOCATION." - (if indium-script-enable-sourcemaps - (if-let ((sourcemap (indium-script-sourcemap script)) - (original-location (indium-sourcemap-original-position-for - sourcemap - (1+ (indium-location-line location)) - (1+ (indium-location-column location)))) - (file (plist-get original-location :source))) - (indium-location-create :file file - :line (max 0 (1- (plist-get original-location :line))) - :column (plist-get original-location :column)) - location) - location)) - -(defun indium-script-generated-location (location) - "Return a generated location from the original LOCATION. - -If there is a parsed script for LOCATION's file, return LOCATION. -Otherwise, if a sourcemap exists, generate a location using that -sourcemap." - (let ((file (indium-location-file location))) - (if (indium-script-find-from-file file) - location - (if indium-script-enable-sourcemaps - (or (seq-some (lambda (script) - (if-let ((sourcemap (indium-script-sourcemap script)) - (generated-location (indium-sourcemap-generated-position-for - sourcemap - (indium-location-file location) - (1+ (indium-location-line location)) - 0))) - (indium-location-create :file (indium-script-url script) - :line (max 0 (1- (plist-get generated-location :line))) - :column (plist-get generated-location :column)))) - (indium-script-all-scripts-with-sourcemap)) - location) - location)))) - -(defun indium-script-generated-location-at-point () - "Return a location for the position of point. -If no location can be found, return nil." - (indium-script-generated-location - (indium-location-at-point))) - -(defun indium-script-sourcemap (script) - "Return the sourcemap object associated with SCRIPT. -The sourcemap object is cached in SCRIPT. - -If no local sourcemap file can be found, try to download it. -If the sourcemap file cannot be downloaded either, return nil." - (when (indium-script-has-sourcemap-p script) - (unless (indium-script-sourcemap-cache script) - (setf (indium-script-sourcemap-cache script) - (or (indium-script--sourcemap-from-data-url script) - (if-let ((file (indium-script--sourcemap-file script))) - (indium-sourcemap-from-file file) - (when-let ((str (indium-script--download - (indium-script--absolute-sourcemap-url script)))) - (indium-sourcemap-from-string str))))) - (when-let (sourcemap (indium-script-sourcemap-cache script)) - (indium-script--transform-sourcemap-sources sourcemap script))) - (indium-script-sourcemap-cache script))) - -(defun indium-script--sourcemap-from-data-url (script) - "Return the sourcemap for SCRIPT if it's specified by a data url. -If the sourcemap url is not a data url, return nil." - (let ((url (indium-script-sourcemap-url script)) buf) - (when (and url (string-prefix-p "data:" url)) - (setq buf (url-data (url-generic-parse-url url))) - (with-current-buffer buf - ;; `url-insert' does not handle Content-Transfer-Encoding; - ;; this is adapted from - ;; `url-handle-content-transfer-encoding', which handles gzip - (when-let (cte (mail-fetch-field "content-transfer-encoding")) - (cond - ((string= cte "base64") - (save-restriction - (widen) - (goto-char (point-min)) - (when (search-forward "\n\n") - (base64-decode-region (point) (point-max))))) - ((string= cte "8bit")) - (t (error "Unknown Content-Transfer-Encoding %s" cte))))) - (with-temp-buffer - (url-insert buf) - (goto-char (point-min)) - (indium-sourcemap--decode (json-read)))))) - -(defun indium-script--sourcemap-path-overrides () - "Return the sourcemap path overrides from the workspace settings. -If no overrides are defined, return the default ones. - -Override paths are expanded." - (let ((overrides (map-elt indium-workspace-configuration 'sourceMapPathOverrides - indium-script-default-sourcemap-path-overrides))) - (map-apply (lambda (regexp transformation) - (cons (if (symbolp regexp) - (symbol-name regexp) - regexp) - (indium-script--expand-path-override transformation))) - overrides))) - -(defun indium-script--expand-path-override (path) - "Return PATH expanded. - -Occurrences of ${root} (alias ${webRoot}) are replaced with the -absolute path of the root directory of the project as returned -by `indium-workspace-root'." - (save-match-data - (if (string-match (rx (or "${root}" "${webRoot}")) path) - (expand-file-name (replace-match (indium-workspace-root) nil t path)) - path))) - -(defun indium-script--transform-sourcemap-sources (sourcemap script) - "Transform source mappings in SOURCEMAP to locations on disk. - -Some source mappings might not be usable as is an need -transformation to map to source paths on disk. - -The transformation map is read from -`indium-script--sourcemap-path-overrides'. - -Paths relative to SCRIPT are also converted to absolute paths -based on the directory path of SCRIPT." - (let ((dir (file-name-directory (indium-script-get-file script t))) - (overrides (indium-script--sourcemap-path-overrides))) - (seq-do (lambda (mapping) - (when (indium-source-mapping-source mapping) - (indium-script--apply-sourcemap-path-overrides mapping overrides) - (indium-script--apply-absolute-sourcemap-path mapping dir))) - (indium-sourcemap-generated-mappings sourcemap)))) - -(defun indium-script--apply-sourcemap-path-overrides (mapping overrides) - "Mutate MAPPING by applying sourcemap path overrides on its source. -OVERRIDES is an alist of '(REGEXP . OVERRIDE) transformation rules." - (when (indium-source-mapping-source mapping) - (map-apply (lambda (regexp transformation) - (let ((source (indium-source-mapping-source mapping))) - (unless (file-name-absolute-p source) - (save-match-data - (when (string-match regexp source) - (setf (indium-source-mapping-source mapping) - (replace-match - transformation - nil t source))))))) - overrides))) - -(defun indium-script--apply-absolute-sourcemap-path (mapping dir) - "Mutate MAPPING by setting its source to an absolute path based on DIR. -Do nothing if MAPPING's source is already an absolute path. - -Mapping paths can be either absolute, or relative to a SCRIPT's -directory. To make things simpler with sourcemaps manipulation, -make all source paths absolute." - (when-let ((source (indium-source-mapping-source mapping))) - (unless (file-name-absolute-p source) - (setf (indium-source-mapping-source mapping) - (expand-file-name source dir))))) - -(defun indium-script--sourcemap-file (script) - "Return the local sourcemap file associated with SCRIPT. -If no sourcemap file can be found, return nil." - (when-let ((script-file (indium-script-get-file script))) - (indium-workspace-lookup-file-safe - (expand-file-name (indium-script-sourcemap-url script) - (file-name-directory script-file))))) - -(defun indium-script--download (url &optional fix-address) - "Download and return the content of URL. -If the request fails or has no data, return nil. - -Because of debbugs#17976 in Emacs <= 25.3, when the first call -fails, the function is called again with FIX-ADDRESS, in which -case 'localhost' is replaced with '127.0.0.1' in URL." - (message "Downloading sourcemap file %s..." url) - (when-let ((buf (condition-case nil - (url-retrieve-synchronously url t) - (error nil)))) - (with-current-buffer buf - (message "Downloading sourcemap file...done") - (goto-char (point-min)) - (if (re-search-forward "^HTTP/.+ 200 OK$" nil (line-end-position)) - (when (search-forward "\n\n" nil t) - (buffer-substring (point) (point-max))) - ;; Fix for bug#17976 - (unless fix-address - (let ((url (replace-regexp-in-string "localhost" "127.0.0.1" url))) - (indium-script--download url t))))))) - -(defun indium-script--absolute-sourcemap-url (script) - "Return the absolute URL for the sourcemap associated with SCRIPT. - -For instance, for a script located at -\"http://localhost/foo/bar.js\" with a sourcmap located at -\"bar.js.map\", return \"http://localhost/foo/bar.js.ap\"." - (let* ((url (indium-script-url script)) - (sourcemap-url (indium-script-sourcemap-url script))) - (unless (string-empty-p url) - (url-expand-file-name sourcemap-url url)))) - -(provide 'indium-script) -;;; indium-script.el ends here diff --git a/indium-sourcemap.el b/indium-sourcemap.el deleted file mode 100644 index 7d592fe..0000000 --- a/indium-sourcemap.el +++ /dev/null @@ -1,394 +0,0 @@ -;;; indium-sourcemap.el --- Indium helpers for source map decoding - -;; Copyright (C) 2012 Julian Scheid -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Julian Scheid , Nicolas Petton -;; Keywords: tools -;; Package: indium - -;; This file is not part of GNU Emacs. - -;; Indium 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. - -;; Indium 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 Indium. If not, see . - -;;; Commentary: - -;; This file was initially written for kite by -;; Julian Scheid. -;; -;; This package provides helper functions for decoding source maps and looking -;; up mappings in them. -;; -;; It is mostly a transliteration of Mozilla's code found at -;; https://github.com/mozilla/source-map/ -;; -;; See also: -;; * http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/ -;; * https://github.com/mozilla/source-map/ -;; * https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1# - - -;;; Code: - -(require 'map) -(require 'seq) -(require 'json) -(require 'cl-lib) -(require 'subr-x) - -(defconst indium-sourcemap--vlq-base-shift 5) - -(defconst indium-sourcemap--vlq-base (lsh 1 indium-sourcemap--vlq-base-shift)) - -(defconst indium-sourcemap--vlq-base-mask (- indium-sourcemap--vlq-base 1)) - -(defconst indium-sourcemap--vlq-continuation-bit indium-sourcemap--vlq-base) - -(defconst indium--supported-sourcemap-version 3) - -(defconst indium--base64-char-to-int-map - (let* ((index 0) - (base64-chars "\ -ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") - (map (make-hash-table :size 64))) - (dolist (char - (string-to-list base64-chars)) - (puthash char index map) - (cl-incf index)) - map)) - -(cl-defstruct (indium-source-mapping) - "Holds the parsed mapping coordinates from the source map's - `mappings' attribute." - generated-line - generated-column - source - original-line - original-column - name) - -(cl-defstruct (indium-sourcemap) - "Representation of a parsed source map suitable for fast -lookup." - names - sources - generated-mappings) - -(defun indium-sourcemap-from-file (file) - "Return a sourcemap from FILE." - (let ((contents (with-temp-buffer - (insert-file-contents file) - (buffer-string)))) - (message "Parsing sourcemap file %s..." file) - (prog1 - (indium-sourcemap--cached-decode contents) - (message "Parsing sourcemap file %s...done!" file)))) - -(defun indium-sourcemap-from-string (string) - "Return a sourcemap from STRING." - (message "Parsing sourcemap...") - (prog1 - (indium-sourcemap--cached-decode string) - (message "Parsing sourcemap ...done!"))) - -(defun indium-sourcemap-original-position-for (sourcemap line column) - "Given SOURCEMAP, find the original position for LINE and COLUMN. -SOURCEMAP should be an `indium-sourcemap' struct. - -Return a plist with `:source', `:line', `:column', and `:name', -or nil if not found." - (when-let ((match (indium-sourcemap--binary-search sourcemap line column))) - (list :source (indium-source-mapping-source match) - :line (indium-source-mapping-original-line match) - :column (indium-source-mapping-original-column match) - :name (indium-source-mapping-name match)))) - -(defun indium-sourcemap-generated-position-for (sourcemap source line column) - "Given SOURCEMAP, find the generated position for SOURCE at LINE and COLUMN. -SOURCEMAP should be an `indium-sourcemap' struct. - -Return a plist with `:source', `:line', `:column', and `:name', -or nil if not found." - (let ((same-source-map (indium-sourcemap--filter-same-source sourcemap source))) - (when-let ((match (indium-sourcemap--binary-search same-source-map line column t))) - (list :source (indium-source-mapping-source match) - :line (indium-source-mapping-generated-line match) - :column (indium-source-mapping-generated-column match) - :name (indium-source-mapping-name match))))) - - -(defun indium--base64-decode (char) - "Decode a single base64 CHAR into its corresponding integer value. - -Raise an error if the character is invalid." - (or (gethash char indium--base64-char-to-int-map) - (error "Invalid base 64 characters: %c" char))) - -(defun indium--from-vlq-signed (value) -"Convert to a two-complement value from VALUE. - -The sign bit is is placed in the least significant bit. For -example, as decimals: 2 (10 binary) becomes 1, 3 (11 binary) -becomes -1, 4 (100 binary) becomes 2, 5 (101 binary) becomes -2." - (let ((shifted (lsh value -1))) - (if (eq 1 (logand value 1)) - (- shifted) - shifted))) - -(defun indium--base64-vlq-decode (string-as-list) - "Decode the next base 64 VLQ value from the given STRING-AS-LIST. - -Return the value and the rest of the string as values, that is a -list (VALUE STRING-REST)." - (let ((result 0) - (continuation t) - (shift 0)) - (while continuation - (when (null string-as-list) - (error "Expected more digits in base 64 VLQ value")) - (let ((digit (indium--base64-decode (car string-as-list)))) - (setq continuation - (not (eq 0 (logand digit indium-sourcemap--vlq-continuation-bit)))) - (setq digit (logand digit indium-sourcemap--vlq-base-mask)) - (cl-incf result (lsh digit shift))) - (cl-incf shift indium-sourcemap--vlq-base-shift) - (setq string-as-list (cdr string-as-list))) - (list :value (indium--from-vlq-signed result) - :rest string-as-list))) - -(defvar indium-sourcemap--cache nil - "Cache hash table of decoded sourcemaps.") - -(defun indium-sourcemap--reset-cache () - "Reset the cache of sourcemap data." - (setq indium-sourcemap--cache (make-hash-table :test 'equal))) - -(indium-sourcemap--reset-cache) - -(defun indium-sourcemap--cached-decode (string) - "Decode STRING json object. - -Return a cached version if STRING was already decoded." - (let ((md5 (md5 string))) - (if (map-contains-key indium-sourcemap--cache md5) - (map-elt indium-sourcemap--cache md5) - (let ((decoded (indium-sourcemap--decode (json-read-from-string string)))) - (unless (indium-sourcemap-p decoded) - (message "Not a sourcemap!!!!!!!!!!!!!! :%s" (type-of decoded))) - (map-put indium-sourcemap--cache md5 decoded) - decoded)))) - -(defun indium-sourcemap--decode (json) - "Decode JSON object. -Return a `indium-sourcemap' struct." - - (when (not (eq (map-elt json 'version) - indium--supported-sourcemap-version)) - (error "Unsupported source map version %s" - (map-elt json 'version))) - - (let* ((source-root (map-elt json 'sourceRoot)) - (names (map-elt json 'names)) - (sources (map-elt json 'sources)) - (string (string-to-list (map-elt json 'mappings))) - (result (make-indium-sourcemap - :names names - :sources sources)) - (generated-mappings-list) - (generated-line 1) - (previous-generated-column 0) - (previous-original-line 0) - (previous-original-column 0) - (previous-source 0) - (previous-name 0)) - (cl-flet - ((starts-with-mapping-separator (string) - (or (null string) - (eq (car string) ?,) - (eq (car string) ?\;)))) - (while string - (cond - ((eq (car string) ?\;) - (cl-incf generated-line) - (setq string (cdr string)) - (setq previous-generated-column 0)) - ((eq (car string) ?,) - (setq string (cdr string))) - (t - (let ((mapping (make-indium-source-mapping - :generated-line generated-line))) - - ;; Generated column. - (let ((temp (indium--base64-vlq-decode string))) - (setf (indium-source-mapping-generated-column mapping) - (+ previous-generated-column - (plist-get temp :value))) - (setq previous-generated-column - (indium-source-mapping-generated-column mapping)) - (setq string (plist-get temp :rest))) - - (when (not (starts-with-mapping-separator string)) - - ;; Original source. - (let ((temp (indium--base64-vlq-decode string))) - (setf (indium-source-mapping-source mapping) - (concat source-root - (elt sources - (+ previous-source - (plist-get temp :value))))) - (cl-incf previous-source (plist-get temp :value)) - (setq string (plist-get temp :rest))) - - (when (starts-with-mapping-separator string) - (error "Found a source, but no line and column")) - - ;; Original line. - (let ((temp (indium--base64-vlq-decode string))) - (setf (indium-source-mapping-original-line mapping) - (+ previous-original-line - (plist-get temp :value))) - (setq previous-original-line - (indium-source-mapping-original-line mapping)) - - ;; Lines are stored 0-based - (cl-incf (indium-source-mapping-original-line mapping)) - - (setq string (plist-get temp :rest))) - - (when (starts-with-mapping-separator string) - (error "Found a source and line, but no column")) - - ;; Original column - (let ((temp (indium--base64-vlq-decode string))) - (setf (indium-source-mapping-original-column mapping) - (+ previous-original-column - (plist-get temp :value))) - (setq previous-original-column - (indium-source-mapping-original-column mapping)) - - (setq string (plist-get temp :rest))) - - (when (not (starts-with-mapping-separator string)) - - ;; Original name - (let ((temp (indium--base64-vlq-decode string))) - (setf (indium-source-mapping-name mapping) - (elt names (+ previous-name - (plist-get temp :value)))) - (cl-incf previous-name (plist-get temp :value)) - - (setq string (plist-get temp :rest))))) - - (push mapping generated-mappings-list))))) - - (setf (indium-sourcemap-generated-mappings result) - (vconcat (nreverse generated-mappings-list)))) - result)) - -(defun indium-sourcemap--filter-same-source (sourcemap source) - "Return a copy of SOURCEMAP with entries filtered for SOURCE only." - (let ((map (copy-indium-sourcemap sourcemap))) - (setf (indium-sourcemap-generated-mappings map) - (seq-filter (lambda (mapping) - (string= (indium-source-mapping-source mapping) - source)) - (indium-sourcemap-generated-mappings map))) - map)) - -(defun indium-sourcemap--binary-search (sourcemap line column &optional generated) - "Given SOURCEMAP, find the position for LINE and COLUMN. -If GENERATED is nil, find an original position, otherwise find a -generated position. - -Return a plist with `:source', `:line', `:column', and `:name', -or nil if not found. - -This is an implementation of binary search which will always try -and return the next lowest value checked if there is no exact -hit. This is because mappings between original and generated -line/col pairs are single points, and there is an implicit region -between each of them, so a miss just means that you aren't on the -very start of a region." - - (when (<= line 0) - (error "Line must be greater than or equal to 1")) - (when (< column 0) - (error "Column must be greater than or equal to 0")) - - (let* ((haystack (indium-sourcemap-generated-mappings sourcemap)) - (low -1) - (high (length haystack)) - terminate - found - line-fn - column-fn) - (if generated - (progn - (setq line-fn #'indium-source-mapping-original-line) - (setq column-fn #'indium-source-mapping-original-column)) - (progn - (setq line-fn #'indium-source-mapping-generated-line) - (setq column-fn #'indium-source-mapping-generated-column))) - - (when (> (length haystack) 0) - - ;; This terminates when one of the following is true: - ;; - ;; 1. We find the exact element we are looking for. - ;; - ;; 2. We did not find the exact element, but we can return the - ;; next closest element that is less than that element. - ;; - ;; 3. We did not find the exact element, and there is no - ;; next-closest element which is less than the one we are - ;; searching for, so we return null. - (while (not terminate) - (let* ((mid (floor (+ (/ (- high low) 2) low))) - (cur (elt haystack mid))) - (cond - ((and (eq (funcall line-fn cur) line) - (eq (funcall column-fn cur) column)) - ;; Found the element we are looking for. - (setq found cur) - (setq terminate t)) - - ((or (< (funcall line-fn cur) line) - (and (eq (funcall line-fn cur) line) - (< (funcall column-fn cur) - column))) - ;; haystack[mid] is greater than our needle. - (if (> (- high mid) 1) - ;; The element is in the upper half. - (setq low mid) - ;; We did not find an exact match, return the next - ;; closest one (termination case 2). - (setq found cur) - (setq terminate t))) - - (t - ;; haystack[mid] is less than our needle. - (if (> (- mid low) 1) - ;; The element is in the lower half. - (setq high mid) - ;; The exact needle element was not found in this - ;; haystack. Determine if we are in termination case (2) - ;; or (3) and return the appropriate thing. - (unless (< low 0) - (setq found (elt haystack low))) - (setq terminate t)))))) - found))) - -(provide 'indium-sourcemap) - -;;; indium-sourcemap.el ends here diff --git a/indium-structs.el b/indium-structs.el index 670c386..f6bf96e 100644 --- a/indium-structs.el +++ b/indium-structs.el @@ -25,13 +25,10 @@ ;; Backends should make instances of the structs defined in this file from data ;; they receive. ;; -;; `indium-script' represents a JavaScript file parsed by the runtime. Scripts -;; are structs indexed by `id' in the current Indium connection. A script contain -;; an `url' slot, and an optional `sourcemap-url' slot. -;; ;; `indium-location' represents a location (most often to a file). A location ;; is a struct with a `line' and `column' slot. If a location points to a local -;; file, it also contains a `file' slot. Columns and lines start at 0. +;; file, it also contains a `file' slot. Columns are 0-based and lines are +;; 1-based. ;; ;; `indium-frame' represents a call frame in the context of debugging. ;; @@ -43,197 +40,156 @@ (require 'map) (require 'subr-x) -(declare-function indium-script-get-file "indium-script.el") -(declare-function indium-script-find-by-id "indium-script.el") -(declare-function indium-script-generated-location "indium-script.el") - -(defmacro when-indium-connected (&rest body) - "Evaluate BODY if there is a current Indium connection." - (declare (indent 0) (debug t)) - `(when indium-current-connection - ,@body)) - -(defmacro unless-indium-connected (&rest body) - "Evalute BODY unless there is a current Indium connection." - (declare (indent 0)) - `(unless indium-current-connection - ,@body)) - -(defvar indium-current-connection nil - "Current connection to the browser tab.") - -(cl-defstruct (indium-connection (:constructor indium-connection-create) - (:copier nil)) - (backend nil :type symbol :read-only t) - (url nil :type string :read-only t) - ;; Optional process attached to the connection (used by NodeJS) - (process nil :type process) - (callbacks (make-hash-table) :type hash-table) - (scripts (make-hash-table) :type hash-table) - (frames nil :type list) - (current-frame nil :type indium-frame) - (project-root nil :type string) - ;; extra properties that can be added by the backend - (props (make-hash-table) :type hash-table)) - -(defun indium-current-connection-backend () - "Return the backend of the current connection if any." - (when-indium-connected - (indium-connection-backend indium-current-connection))) - -(defun indium-current-connection-url () - "Return the url of the current connection if any." - (when-indium-connected - (indium-connection-url indium-current-connection))) - -(defun indium-current-connection-callbacks () - "Return the callbacks of the current connection if any." - (when-indium-connected - (indium-connection-callbacks indium-current-connection))) - -(defun indium-current-connection-process () - "Return the process attached to the current connection if any." - (when-indium-connected - (indium-connection-process indium-current-connection))) - -(defun indium-current-connection-project-root () - "Return the root directory of the current connection's project." - (when-indium-connected - (indium-connection-project-root indium-current-connection))) - -(cl-defmethod (setf indium-current-connection-process) (process) - (when-indium-connected - (setf (indium-connection-process indium-current-connection) process))) - -(cl-defmethod (setf indium-current-connection-callbacks) (callbacks) - (when-indium-connected - (setf (indium-connection-callbacks indium-current-connection) callbacks))) - -(defun indium-current-connection-scripts () - "Return the scripts of the current connection if any." - (when-indium-connected - (indium-connection-scripts indium-current-connection))) - -(defun indium-current-connection-props () - "Return the props of the current connection if any." - (when-indium-connected - (indium-connection-props indium-current-connection))) - -(defun indium-current-connection-frames () - "Return the frames of the current connection if any." - (when-indium-connected - (indium-connection-frames indium-current-connection))) - -(cl-defmethod (setf indium-current-connection-frames) (frames) - (when-indium-connected - (setf (indium-connection-frames indium-current-connection) frames))) - -(defun indium-current-connection-current-frame () - "Return the current frame of the current connection if any." - (when-indium-connected - (indium-connection-current-frame indium-current-connection))) - -(cl-defmethod (setf indium-current-connection-current-frame) (frame) - (when-indium-connected - (setf (indium-connection-current-frame indium-current-connection) frame))) - -(cl-defstruct (indium-script (:constructor indium-script-create) - (:copier nil)) - (id nil :type string :read-only t) - (url nil :type string :read-only t) - (sourcemap-url nil :type string :read-only t) - (parsed-time (current-time) :read-only t) - ;; Keep a cache of the parsed sourcemap for speed. See - ;; `indium-script-sourcemap'. - (sourcemap-cache nil)) +(declare-function indium-client--next-id "indium-client.el") (cl-defstruct (indium-location (:constructor indium-location-create) - (:constructor indium-location-from-script-id - (&key (script-id "") - line - column - &aux (file (indium-script-get-file - (indium-script-find-by-id script-id))))) + (:constructor indium-location-at-point + (&aux (file buffer-file-name) + (line (line-number-at-pos)) + (column (current-column)))) + (:constructor indium-location-from-alist + (alist &aux + (file (map-elt alist 'file)) + (line (map-elt alist 'line)) + (column (map-elt alist 'column)))) (:copier nil)) - (line 0 :type number :read-only t) - (column 0 :type number :read-only t) - (file nil :type string :read-only t)) - -(defun indium-location-at-point () - "Return an `indium-location' for the position at point." - (indium-location-create :file buffer-file-name - :line (1- (line-number-at-pos)) - :column (current-column))) - -(cl-defstruct (indium-frame (:constructor indium-frame-create) - (:copier nil)) - (id nil :type string :read-only t) - ;; TODO: make a scope a struct as well. - (scope-chain nil :type list :read-only t) - (location nil :type indium-location :read-only t) - (script nil :type indium-script :read-only t) - (type nil :type string :read-only t) - (function-name nil :type string)) + (line 1 :type number) + (column 0 :type number) + (file nil :type string)) (cl-defstruct (indium-breakpoint (:constructor indium-breakpoint-create - (&key original-location - condition + (&key condition overlay - id))) - (id nil :type string) + id)) + (:copier nil)) + (id (indium-client--next-id) :type string) (overlay nil) (resolved nil) - (original-location nil :type indium-location :read-only t) (condition "" :type string)) +(defun indium-breakpoint-location (brk) + "Return the location of BRK." + (when-let ((ov (indium-breakpoint-overlay brk)) + (pos (overlay-start ov)) + (buf (overlay-buffer ov))) + (with-current-buffer buf + (save-excursion + (goto-char (point-min)) + (forward-char pos) + (indium-location-at-point))))) + (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-generated-location (breakpoint) - "Return the generated location for BREAKPOINT." - (indium-script-generated-location - (indium-breakpoint-original-location breakpoint))) - -(defun indium-breakpoint-unregistered-p (breakpoint) - "Return non-nil if BREAKPOINT is not registered in the backend." - (null (indium-breakpoint-id breakpoint))) - -(defun indium-breakpoint-registered-p (breakpoint) - "Return non-nil if BREAKPOINT is registered in the backend." - (not (indium-breakpoint-unregistered-p breakpoint))) - (defun indium-breakpoint-unresolved-p (breakpoint) "Return non-nil if BREAKPOINT is not yet resolved in the runtime." (not (indium-breakpoint-resolved breakpoint))) -(defun indium-breakpoint-can-be-resolved-p (breakpoint) - "Return non-nil if BREAKPOINT can be resolved. - -A breakpoint can be resolved if re-registering it in the backend -could lead to its resolution, eg: - -- The breakpoint is not yet registered at all - -- The breakpoint is registered but its generated location is - different from its original location, meaning that a new script - was parsed, where the breakpoint should be set." - (and (indium-breakpoint-unresolved-p breakpoint) - (or (indium-breakpoint-unregistered-p breakpoint) - (not (equal (indium-breakpoint-original-location breakpoint) - (indium-breakpoint-generated-location breakpoint)))))) - -(defun indium-breakpoint-register (breakpoint id) - "Register BREAKPOINT by giving it a backend ID." - (setf (indium-breakpoint-id breakpoint) id)) - -(defun indium-breakpoint-unregister (breakpoint) - "Remove the registration & resolution information from BREAKPOINT." - (setf (indium-breakpoint-id breakpoint) nil) - (setf (indium-breakpoint-resolved breakpoint) nil)) +(cl-defstruct (indium-frame + (:constructor indium-frame-create + (&key script-id + function-name + location + scope-chain)) + (:constructor indium-frame-from-alist + (alist &aux + (script-id (map-elt alist 'scriptId)) + (function-name (map-elt alist 'functionName)) + (location (indium-location-from-alist + (map-elt alist 'location))) + (scope-chain (seq-map #'indium-scope-from-alist + (map-elt alist 'scopeChain))))) + (:copier nil)) + (function-name "" :type string) + (script-id "" :type string) + (location nil :type indium-location) + (scope-chain nil)) + +(cl-defstruct (indium-scope + (:constructor indium-scope-create + (&key type + name + id)) + (:constructor indium-scope-from-alist + (alist &aux + (type (map-elt alist 'type)) + (name (map-elt alist 'name)) + (id (map-elt alist 'id)))) + (:copier nil)) + (id "" :type string) + (name "" :type string) + (type "" :type string)) + +(cl-defstruct (indium-remote-object + (:constructor indium-remote-object-create + (&key id + type + description + preview)) + (:constructor indium-remote-object-from-alist + (alist &aux + (id (map-elt alist 'id)) + (type (map-elt alist 'type)) + (description (map-elt alist 'description)) + (preview (map-elt alist 'preview)))) + (:copier nil)) + (id nil :type string) + (type "" :type string) + (description "" :type string) + (preview "" :type string)) + +(defun indium-remote-object-error-p (obj) + "Retun non-nil if OBJ represents an error." + (equal (indium-remote-object-type obj) "error")) + +(defun indium-remote-object-reference-p (obj) + "Return non-nil if OBJ is a reference to a remote object." + (let ((id (indium-remote-object-id obj))) + (and (not (null id)) + (not (string-empty-p id))))) + +(defun indium-remote-object-function-p (obj) + "Return non-nil if OBJ represents a function." + (equal (indium-remote-object-type obj) "function")) + +(defun indium-remote-object-has-preview-p (obj) + "Return non-nil if OBJ has a preview string." + (let ((preview (indium-remote-object-preview obj))) + (and preview (not (string-empty-p preview))))) + +(defun indium-remote-object-to-string (obj &optional full) + "Return a short string describing OBJ. + +When FULL is non-nil, do not strip long descriptions and function +definitions." + (if (and (not full) (indium-remote-object-function-p obj)) + "function" + (indium-remote-object-description obj))) + +(cl-defstruct (indium-property + (:constructor indium-property-create + (&key name + remote-object)) + (:constructor indium-property-from-alist + (alist &aux + (name (map-elt alist 'name)) + (remote-object (indium-remote-object-from-alist + (map-elt alist 'value))))) + (:copier nil)) + (name "" :type string) + (remote-object nil :type indium-remote-object)) + +(defun indium-property-native-p (property) + "Return non-nil value if PROPERTY is native code." + (string-match-p "{ \\[native code\\] }$" + (or (indium-remote-object-description + (indium-property-remote-object + property)) + ""))) (provide 'indium-structs) ;;; indium-structs.el ends here diff --git a/indium-v8.el b/indium-v8.el deleted file mode 100644 index 6ad8e78..0000000 --- a/indium-v8.el +++ /dev/null @@ -1,702 +0,0 @@ -;;; indium-v8.el --- V8/Blink backend for indium -*- lexical-binding: t; -*- - -;; Copyright (C) 2016-2018 Nicolas Petton - -;; Author: Nicolas Petton -;; Keywords: tools, javascript - -;; This program is free software; you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see . - -;;; Commentary: - -;; Indium backend implementation for V8 and Blink. Connection is handled in -;; indium-chrome.el and indium-nodejs.el. This backend currently supports the -;; REPL, code completion, object inspection and the stepping debugger. -;; -;; Parts of the backend use the TOT (tip of tree) of the protocol, and may break -;; in the future. When using the TOT, functions are flagged with "experimental -;; API". -;; -;; This backend supports both Chrome/Chromium 60 and Nodejs 8.x. -;; -;; The protocol is documented at -;; https://chromedevtools.github.io/debugger-protocol-viewer/1-2/. -;; https://chromedevtools.github.io/devtools-protocol/tot - -;;; Code: - -(require 'json) -(require 'map) -(require 'seq) - -(require 'wsc) -(require 'indium-backend) -(require 'indium-structs) -(require 'indium-repl) -(require 'indium-debugger) -(require 'indium-workspace) -(require 'indium-script) -(require 'indium-breakpoint) - -(defvar indium-v8-cache-disabled nil - "Network cache disabled state. If non-nil disable cache when Indium starts.") - -(defvar indium-v8-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 -;; Keywords: - -;; 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 . - -;;; Commentary: - -;; Indium workspace management. -;; -;; When connecting to a backend, Indium will lookup and read a project -;; configuration file `.indium.json' in the project root directory. - -;;; .indium.json configuration file example: -;; -;; { -;; "configurations": [ -;; { -;; "name": "Chrome", -;; "type": "chrome", -;; "url": "http://localhost:3333/" -;; }, -;; { -;; "name": "Node server", -;; "type": "node", -;; "command": "node ./src/server.js", -;; "inspect-brk": false -;; } -;; { -;; "name": "Gulp", -;; "type": "node", -;; "command": "node ./node_modules/gulp/bin/gulp.js default", -;; "inspect-brk": true -;; } -;; ] -;; } - -;;; Available settings: -;; -;; "type": Type of runtime (currently "node" or "chrome" are supported). -;; -;; "root": Relative path to the root directory from where files are served. -;; Alias: "webRoot". -;; -;; "sourceMapPathOverrides": Custom sourcemap mappings. Maps source paths to -;; locations on disk. -;; -;; Default value: -;; -;; { -;; "webpack:///./~/": "${root}/node_modules/", -;; "webpack:///./": "${root}/", -;; "webpack:///": "/", -;; "webpack:///src/": "${root}/" -;; } -;; -;; Chrome-specific settings: -;; -;; "host": Host on which Chrome is running (defaults to "localhost"). -;; -;; "port": Port on which Chrome is running (defaults to 9222). -;; -;; "url": Url to open when running `indium-launch-chrome'. -;; -;; Nodejs-specific settings: -;; -;; "command": Nodejs command to start a new process. The `--inspect' flag will -;; be added automatically. -;; -;; "inspect-brk": Whether Indium should break at the first statement (true by -;; default). -;; -;; "host": Host on which the Node inspector is listening (defaults to "localhost"). -;; -;; "port": Port on which the Node inspector is listening (defaults to 9229). - -;;; Code: - - -(require 'url) -(require 'seq) -(require 'map) -(require 'subr-x) -(require 'json) - -(require 'indium-structs) -(require 'indium-backend) - -(declare-function indium-connection-nodejs-p "indium-nodejs.el") - - -(defvar indium-workspace-filename ".indium.json" - "Name of the configuration file containing the Indium project settings.") - -(defvar indium-workspace-configuration nil - "Configuration in the settings file used for connecting. -Do not set this variable directly.") - -(defmacro with-indium-workspace-configuration (&rest body) - "Promt the users for a configuration and evaluate BODY. -During the evaluation of BODY, `indium-workspace-configuration' -is set to the choosen configuration." - (declare (indent 0) (debug t)) - `(progn - (unless indium-workspace-configuration - (indium-workspace-read-configuration)) - ,@body)) - -(defun indium-workspace-root () - "Lookup the root workspace directory from the current buffer. - -If a connection is already open, return the `project-root' stored -in that connection. - -If no connection is open yet, lookup the root directory as follows: - - - The root directory is specified by the \"webRoot\" (alias - \"root\") configuration option. - - - If no \"root\" option is set, it defaults to the directory - containing the \".indium.json\" project file. - -If the root directory does not exist, signal an error." - (let ((root (or (indium-current-connection-project-root) - (indium-workspace--root-from-configuration) - (indium-workspace--project-directory)))) - (unless (file-directory-p root) - (user-error "Project root directory does not exist")) - root)) - -(defun indium-workspace--root-from-configuration () - "Return the root directory read from the project configuration. -If no root is specified, return nil." - (when-let ((root (or (map-elt indium-workspace-configuration 'root) - (map-elt indium-workspace-configuration 'webRoot)))) - (expand-file-name root (indium-workspace--project-directory)))) - -(defun indium-workspace--project-directory () - "Return the directory containing the \".indium.json\" file." - (locate-dominating-file default-directory - indium-workspace-filename)) - -(defun indium-workspace-ensure-setup () - "Signal an error no workspace file can be found." - (unless (indium-workspace-root) - (error "No file .indium.json found in the current project"))) - -(defun indium-workspace-settings-file () - "Lookup the filename of the settings file for the current workspace. -Return nil if not found." - (when-let ((dir (indium-workspace--project-directory))) - (expand-file-name indium-workspace-filename - dir))) - -(defun indium-workspace-settings () - "Return the workspace settings read from the workspace file." - (indium-workspace-ensure-setup) - (with-temp-buffer - (insert-file-contents (indium-workspace-settings-file)) - (goto-char (point-min)) - (ignore-errors - (json-read)))) - -(defun indium-workspace-read-configuration () - "Prompt for the configuration used for connecting to a backend. -Set `indium-workspace-configuration' to the choosen configuration. -If the settings file contains only one configuration, set it." - (let* ((settings (indium-workspace-settings)) - (configurations (map-elt settings 'configurations)) - (configuration-names (seq-map (lambda (configuration) - (map-elt configuration 'name)) - configurations))) - (unless configurations - (user-error "No configuration provided in the project file")) - (setq indium-workspace-configuration - (if (= (seq-length configurations) 1) - (seq-elt configurations 0) - (let ((name (completing-read "Choose a configuration: " - configuration-names - nil - t))) - (seq-find (lambda (configuration) - (string-equal (map-elt configuration 'name) - name)) - configurations)))))) - - - -(defun indium-workspace-lookup-file (url &optional ignore-existence) - "Return a local file matching URL for the current connection. -If no file is found, and IGNORE-EXISTENCE is nil, return nil, -otherwise return the path of a file that does not exist." - (when url - (or (indium-workspace--lookup-using-file-protocol url) - (indium-workspace--lookup-using-workspace url ignore-existence)))) - -(defun indium-workspace-lookup-file-safe (url) - "Find a local file for URL, or return URL is no file can be found." - (or (indium-workspace-lookup-file url) url)) - -(defun indium-workspace--lookup-using-file-protocol (url) - "Return a local file matching URL if URL use the file:// protocol." - (when (indium-workspace--file-protocol-p) - (let* ((url (url-generic-parse-url url)) - (path (car (url-path-and-query url)))) - (when (file-regular-p path) - path)))) - -(defun indium-workspace--lookup-using-workspace (url &optional ignore-existence) - "Return a local file matching URL using the current Indium workspace. -When IGNORE-EXISTENCE is non-nil, also match file paths that are -not on disk." - (indium-workspace-ensure-setup) - (let* ((root (indium-workspace-root)) - (path (seq-drop (car (url-path-and-query - (url-generic-parse-url url))) - 1)) - (file (expand-file-name path root))) - (when (or ignore-existence (file-regular-p file)) - file))) - -(defun indium-workspace-make-url (file) - "Return the url associated with the local FILE." - (or (indium-workspace--make-url-using-file-path file) - (indium-workspace--make-url-using-file-protocol file) - (indium-workspace--make-url-using-workspace file))) - -(defun indium-workspace--make-url-using-file-path (file) - "When using nodejs, the path of FILE should be used directly." - (when (indium-connection-nodejs-p indium-current-connection) - (convert-standard-filename file))) - -(defun indium-workspace--make-url-using-file-protocol (file) - "Return a url using the \"file://\" protocol for FILE. -If the current connection doesn't use the file protocol, return nil." - (when (indium-workspace--file-protocol-p) - (format "file://%s" file))) - -(defun indium-workspace--make-url-using-workspace (file) - "Return the url associated with the local FILE. -The url is built using `indium-workspace-root'." - (indium-workspace-ensure-setup) - (let* ((root (indium-workspace-root)) - (url (indium-workspace--url-basepath (indium-current-connection-url))) - (path (file-relative-name file root))) - (setf (url-filename url) (indium-workspace--absolute-path path)) - (url-recreate-url url))) - -(defun indium-workspace--file-protocol-p () - "Return non-nil if the current connection use the file protocol." - (let ((url (url-generic-parse-url (indium-current-connection-url)))) - (string= (url-type url) "file"))) - -(defun indium-workspace--absolute-path (path) - "Return PATH as absolute. -Prepend a \"/\" to PATH unless it already starts with one." - (unless (string= (seq-take path 1) "/") - (concat "/" path))) - -(defun indium-workspace--url-basepath (url) - "Return an urlobj with the basepath of URL. -The path and query string of URL are stripped." - (let ((urlobj (url-generic-parse-url url))) - (url-parse-make-urlobj (url-type urlobj) - (url-user urlobj) - (url-password urlobj) - (url-host urlobj) - (url-port urlobj) - nil nil nil t))) - -(provide 'indium-workspace) -;;; indium-workspace.el ends here diff --git a/indium.el b/indium.el index 6c95ebd..8f514ef 100644 --- a/indium.el +++ b/indium.el @@ -31,10 +31,11 @@ ;;; Code: (require 'cl-lib) -(require 'indium-chrome) -(require 'indium-nodejs) +(require 'indium-client) (require 'indium-scratch) -(require 'indium-list-scripts) +(require 'indium-debugger) +(require 'indium-interaction) +(require 'indium-list-sources) (provide 'indium) ;;; indium.el ends here diff --git a/test/fixtures/.indium.json b/test/fixtures/.indium.json deleted file mode 100644 index 1c10cf7..0000000 --- a/test/fixtures/.indium.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "configurations": [ - { - "name": "node", - "type": "node", - "command": "node ./test.js" - } - ] -} diff --git a/test/fixtures/test-with-output.js b/test/fixtures/test-with-output.js deleted file mode 100644 index e5713eb..0000000 --- a/test/fixtures/test-with-output.js +++ /dev/null @@ -1 +0,0 @@ -setTimeout(() => console.log("hello world"), 1000); diff --git a/test/fixtures/test.js b/test/fixtures/test.js deleted file mode 100644 index f647cd0..0000000 --- a/test/fixtures/test.js +++ /dev/null @@ -1,3 +0,0 @@ -function foo(a, b) { - return a + b; -} diff --git a/test/integration/indium-nodejs-integration-test.el b/test/integration/indium-nodejs-integration-test.el deleted file mode 100644 index 01474e1..0000000 --- a/test/integration/indium-nodejs-integration-test.el +++ /dev/null @@ -1,81 +0,0 @@ -;;; indium-nodejs-integration-test.el --- Integration tests for indium-nodejs.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton -;; Keywords: test, integration - -;; 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 . - -;;; Commentary: - -;; Integration tests for the nodejs connection. This actually run nodejs, so it -;; has to be installed on the machine. -;; -;; Warning: Before running each test, the current indium-connection is closed! - -;;; Code: - -(require 'buttercup) -(require 'subr-x) -(require 'cl-lib) - -(require 'indium-nodejs) - -(describe "NodeJS connection" - (before-each - (when-indium-connected - (indium-quit))) - - (after-each - (when-indium-connected - (indium-quit))) - - (it "should be able to start a node process and connect to it" - (with-indium-test-fs - (spy-on 'indium-workspace-root :and-return-value default-directory) - (expect indium-current-connection :to-be nil) - (indium-launch) - (sleep-for 2) - (expect indium-current-connection :not :to-be nil))) - - (it "should not try to open a new connection on process output" - (with-indium-test-fs - (spy-on 'indium-workspace-root :and-return-value default-directory) - (spy-on 'indium-nodejs--connect-to-process :and-call-through) - (expect indium-current-connection :to-be nil) - (indium-launch) - (sleep-for 2) - (expect #'indium-nodejs--connect-to-process :to-have-been-called-times 1))) - - (it "should run hooks when opening a connection" - (with-indium-test-fs - (spy-on 'indium-workspace-root :and-return-value default-directory) - (spy-on 'ignore) - (add-hook 'indium-connection-open-hook #'ignore) - (indium-launch) - (sleep-for 2) - (expect #'ignore :to-have-been-called) - (remove-hook 'indium-connection-open-hook #'ignore))) - - ;; (it "should create a REPL buffer upon connection" - ;; (with-indium-test-fs - ;; (spy-on 'indium-workspace-root :and-return-value default-directory) - ;; (expect (get-buffer (indium-repl-buffer-name)) :to-be nil) - ;; (indium-launch) - ;; (sleep-for 2) - ;; (expect (get-buffer (indium-repl-buffer-name)) :not :to-be nil))) - - (provide 'indium-nodejs-integration-test)) -;;; indium-nodejs-integration-test.el ends here diff --git a/test/integration/indium-repl-integration-test.el b/test/integration/indium-repl-integration-test.el deleted file mode 100644 index 14b69bf..0000000 --- a/test/integration/indium-repl-integration-test.el +++ /dev/null @@ -1,64 +0,0 @@ -;;; indium-repl-integration-test.el --- Integration tests for indium-repl.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton -;; Keywords: test, integration - -;; 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 . - -;;; Commentary: - -;; Integration tests for the repl buffer. This actually runs nodejs, so it -;; has to be installed on the machine. -;; -;; Warning: Before running each test, the current indium-connection is closed! - -;;; Code: - -(require 'buttercup) -(require 'subr-x) -(require 'cl-lib) - -(require 'indium-repl) -(require 'indium-nodejs) - -(describe "Repl output" - (it "should display a prompt" - (with-repl-buffer - (expect (buffer-string) :to-match "js> $"))) - - (it "should display a welcome message" - (with-repl-buffer - (expect (buffer-string) :to-match "Welcome to Indium"))) - - (it "should be able to clear all output" - (with-repl-buffer - (press "C-c C-o") - (expect (buffer-string) :to-match "^js> $"))) - - (it "should be able to eval and print results" - (with-repl-buffer - (repl-eval "true") - (expect (buffer-string) :to-match "js> true\ntrue\njs> $"))) - - (it "should be able to inspect objects" - (with-repl-buffer - (insert "console") - (expect (indium-inspector-get-buffer) :to-be nil) - (press-and-sleep-for "C-c M-i" 2) - (expect (indium-inspector-get-buffer) :not :to-be nil)))) - -(provide 'indium-repl-integration-test) -;;; indium-repl-integration-test.el ends here diff --git a/test/unit/indium-backend-test.el b/test/unit/indium-backend-test.el deleted file mode 100644 index 33193f2..0000000 --- a/test/unit/indium-backend-test.el +++ /dev/null @@ -1,39 +0,0 @@ -;;; indium-backend-test.el --- Test for indium-backend.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2016-2018 Nicolas Petton - -;; Author: Nicolas Petton - -;; 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 . - -;;; Commentary: - -;;; Code: - -(require 'buttercup) - -(require 'indium-backend) -(require 'indium-v8) - -(describe "Open/Closed connections" - (it "should be an active connection if generic" - (with-indium-connection '((backend . fake)) - (expect (indium-backend-active-connection-p 'fake) :to-be-truthy))) - - (it "should not be active unless a websocket is open" - (with-indium-connection (indium-connection-create :backend 'v8) - (expect (indium-backend-active-connection-p 'v8) :to-be nil)))) - -(provide 'indium-backend-test) -;;; indium-backend-test.el ends here diff --git a/test/unit/indium-breakpoint-test.el b/test/unit/indium-breakpoint-test.el index b7de06f..d328d7f 100644 --- a/test/unit/indium-breakpoint-test.el +++ b/test/unit/indium-breakpoint-test.el @@ -26,6 +26,7 @@ (require 'seq) (require 'map) (require 'indium-breakpoint) +(require 'indium-client) (describe "Breakpoint position when editing buffers (GH issue #82)" (it "should keep the breakpoint on the original line when adding a line before" @@ -113,6 +114,7 @@ (it "can put a breakpoint on the current line" (with-js2-file + (spy-on #'indium-client-add-breakpoint) (goto-char (point-min)) (expect (indium-breakpoint-on-current-line-p) :to-be nil) (indium-breakpoint-add) @@ -122,6 +124,7 @@ (spy-on #'read-from-minibuffer :and-return-value "new condition") (spy-on #'indium-breakpoint-remove :and-call-through) (spy-on #'indium-breakpoint-add :and-call-through) + (spy-on #'indium-client-add-breakpoint) (with-js2-file (goto-char (point-min)) (indium-breakpoint-add "old condition") @@ -132,6 +135,7 @@ (describe "Breakpoint duplication handling" (it "can add a breakpoint multiple times on the same line without duplicating it" + (spy-on #'indium-client-add-breakpoint) (with-js2-file (indium-breakpoint-add) (let ((number-of-overlays (seq-length (overlays-in (point-min) (point-max))))) @@ -147,8 +151,8 @@ (it "removing overlays should remove them from breakpoints" (with-js2-buffer "let a = 2;" - (let* ((brk (indium-breakpoint-create)) - (ov (indium-breakpoint--add-overlay brk))) + (let* ((brk (indium-breakpoint-create))) + (indium-breakpoint--add-overlay brk) (indium-breakpoint--remove-overlay) (expect (indium-breakpoint-overlay brk) :to-be nil)))) @@ -162,6 +166,7 @@ (describe "Keeping track of local breakpoints in buffers" (it "should track breakpoints when added" + (spy-on #'indium-client-add-breakpoint) (with-js2-file (let ((indium-breakpoint--local-breakpoints (make-hash-table :weakness t))) (indium-breakpoint-add) @@ -169,6 +174,7 @@ (expect (car (map-values indium-breakpoint--local-breakpoints)) :to-be (current-buffer))))) (it "should untrack breakpoints when removed" + (spy-on #'indium-client-add-breakpoint) (with-js2-file (let ((indium-breakpoint--local-breakpoints (make-hash-table :weakness t))) (indium-breakpoint-add) @@ -176,6 +182,7 @@ (expect (seq-length (map-keys indium-breakpoint--local-breakpoints)) :to-be 0)))) (it "should untrack breakpoints when killing a buffer" + (spy-on #'indium-client-add-breakpoint) (with-js2-file (let ((indium-breakpoint--local-breakpoints (make-hash-table :weakness t))) (indium-breakpoint-add) @@ -191,55 +198,17 @@ 'bar) (expect (indium-breakpoint-breakpoint-with-id 'foo) :to-be brk))))) -(describe "Breakpoint registration" - (it "should be able to unregister breakpoints" - (with-js2-file - (let ((indium-breakpoint--local-breakpoints (make-hash-table))) - (indium-breakpoint-add) - (let ((brk (indium-breakpoint-at-point))) - ;; Fake the registration of the breakpoint - (setf (indium-breakpoint-id (indium-breakpoint-at-point)) 'foo) - (expect (indium-breakpoint-registered-p brk) :to-be-truthy) - (indium-breakpoint--unregister-all-breakpoints) - (expect (indium-breakpoint-registered-p brk) :to-be nil)))))) - (describe "Breakpoint resolution" - (it "breakpoints should not be resolvable when already resolved" - (let ((brk (indium-breakpoint-create))) - (setf (indium-breakpoint-resolved brk) t) - (expect (indium-breakpoint-can-be-resolved-p brk) - :to-be nil))) - - (it "breakpoints should be resolvable when they are not registered" - (expect (indium-breakpoint-can-be-resolved-p (indium-breakpoint-create)) - :to-be-truthy)) - - (it "breakpoints should be resolvable when registered and different generated location" - (let ((brk (indium-breakpoint-create))) - (spy-on 'indium-breakpoint-generated-location :and-return-value 'foo) - (indium-breakpoint-register brk 'id) - (expect (indium-breakpoint-can-be-resolved-p brk) - :to-be-truthy))) - - (it "breakpoints not should be resolvable when registered and same generated location" - (let ((brk (indium-breakpoint-create :original-location 'foo))) - (indium-breakpoint-register brk 'id) - (spy-on 'indium-breakpoint-generated-location :and-return-value 'foo) - (expect (indium-breakpoint-can-be-resolved-p brk) - :to-be nil))) - (it "should be able to unresolve breakpoints" + (spy-on #'indium-client-add-breakpoint) (with-js2-file (let ((indium-breakpoint--local-breakpoints (make-hash-table))) (indium-breakpoint-add) (let ((brk (indium-breakpoint-at-point))) ;; Fake the registration of the breakpoint - (setf (indium-breakpoint-id (indium-breakpoint-at-point)) 'foo) - ;; Fake the resolution of the breakpoint - (setf (indium-breakpoint-resolved brk) t) - (expect (indium-breakpoint-unresolved-p brk) :to-be nil) + (setf (indium-breakpoint-resolved (indium-breakpoint-at-point)) t) (indium-breakpoint--unregister-all-breakpoints) - (expect (indium-breakpoint-unresolved-p brk) :to-be-truthy)))))) + (expect (indium-breakpoint-resolved brk) :to-be nil)))))) (provide 'indium-breakpoint-test) ;;; indium-breakpoint-test.el ends here diff --git a/test/unit/indium-chrome-test.el b/test/unit/indium-chrome-test.el index 2a79bc2..fad0828 100644 --- a/test/unit/indium-chrome-test.el +++ b/test/unit/indium-chrome-test.el @@ -26,95 +26,52 @@ (require 'buttercup) (require 'indium-chrome) - -(describe "Reading tab data" - (it "should be able to parse tab data" - (let ((data "HTTP/1.1 200 OK - -{\"foo\": 1, \"bar\": 2}")) - (with-temp-buffer - (insert data) - (goto-char (point-min)) - (expect (indium-chrome--read-tab-data) - :to-equal '((foo . 1) - (bar . 2)))))) - - (it "should return nil when there is no data" - (with-temp-buffer - (goto-char (point-min)) - (expect (indium-chrome--read-tab-data) - :to-equal nil))) - - (it "should return nil when not getting a 200 response" - (let ((data "HTTP/1.1 404 NOT-FOUND - -{\"foo\": 1, \"bar\": 2}")) - (with-temp-buffer - (insert data) - (goto-char (point-min)) - (expect (indium-chrome--read-tab-data) - :to-equal nil))))) - -(describe "Connecting to a tab" - (it "should signal an error when the list of tabs is empty" - (expect (indium-chrome--connect-to-tab nil) - :to-throw)) - - (it "should automatically connect to the first tab if there is only one" - (spy-on 'indium-chrome--connect-to-tab-with-url) - (let ((tabs '(((url . foo))))) - (indium-chrome--connect-to-tab tabs) - (expect #'indium-chrome--connect-to-tab-with-url - :to-have-been-called-with 'foo tabs)))) +(require 'indium-client) + +(describe "Chrome executable" + (it "Should try to find the executable" + (spy-on 'executable-find :and-return-value 'foo) + (expect (indium-chrome--find-executable) :to-be 'foo) + (expect #'executable-find :to-have-been-called-with indium-chrome-executable)) + + (it "Should return the default executable based on the system" + (let ((system-type "gnu/linux")) + (expect (indium-chrome--default-executable) + :to-equal "chromium")) + (let ((system-type "darwin")) + (expect (indium-chrome--default-executable) + :to-equal "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")) + (let ((system-type "windows-nt")) + (expect (indium-chrome--default-executable) + :to-equal "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe")))) (describe "Running Chrome" (it "Should start the Chrome process" - (spy-on 'indium-chrome--find-executable :and-return-value "chrome") - (spy-on 'make-process) - (spy-on 'indium-chrome--try-connect) - (spy-on 'indium-chrome--url :and-return-value "foo.html") - (spy-on 'indium-chrome--port :and-return-value 9999) - (indium-launch-chrome) - (expect #'make-process - :to-have-been-called-with - :name "indium-chrome-process" - :command '("chrome" "--remote-debugging-port=9999" "foo.html"))) - - (it "Should try to open connections" - (spy-on 'indium-chrome--find-executable :and-return-value "chrome") - (spy-on 'make-process) - (spy-on 'indium-chrome--try-connect) - (spy-on 'indium-chrome--url :and-return-value "foo.html") - (spy-on 'indium-chrome--port :and-return-value 9999) - (indium-launch-chrome) - (expect #'indium-chrome--try-connect :to-have-been-called-with 10))) - -(describe "Connecting to a Chrome process" - (it "Should wait between connection retries" - (spy-on 'sleep-for) - (spy-on 'indium-chrome--get-tabs-data) - (indium-chrome--try-connect 1) - (expect #'sleep-for :to-have-been-called-with 1)) - - (it "Should retry if the connection attempt fails" - (spy-on 'sleep-for) - (spy-on 'indium-chrome--try-connect :and-call-through) - (spy-on 'indium-chrome--get-tabs-data - :and-call-fake - (lambda (host port callback) - (funcall callback nil))) - (indium-chrome--try-connect 1) - (expect #'indium-chrome--try-connect :to-have-been-called-times 2)) - - (it "Should connect to a tab when found" - (spy-on 'sleep-for) - (spy-on 'indium-chrome--connect-to-tab) - (spy-on 'indium-chrome--get-tabs-data - :and-call-fake - (lambda (host port callback) - (funcall callback 'tabs))) - (indium-chrome--try-connect 1) - (expect #'indium-chrome--connect-to-tab :to-have-been-called-with 'tabs))) + (let ((conf '((url . "http://localhost:9999") + (name . "Web project") + (type . "chrome") + (projectFile . "/foo/bar/.indium.json") + (port . "9223")))) + (spy-on 'indium-chrome--find-executable :and-return-value "chrome") + (spy-on 'make-process) + (spy-on 'indium-client-connect) + (indium-launch-chrome conf) + (expect #'make-process + :to-have-been-called-with + :name "indium-chrome-process" + :command '("chrome" "--remote-debugging-port=9223" "http://localhost:9999")))) + + (it "Should connect to the chrome process" + (let ((conf '((url . "http://localhost:9999") + (name . "Web project") + (type . "chrome") + (projectFile . "/foo/bar/.indium.json") + (port . "9999")))) + (spy-on 'indium-chrome--find-executable :and-return-value "chrome") + (spy-on 'make-process) + (spy-on 'indium-client-connect) + (indium-launch-chrome conf) + (expect #'indium-client-connect :to-have-been-called)))) (provide 'indium-chrome-test) ;;; indium-chrome-test.el ends here diff --git a/test/unit/indium-debugger-test.el b/test/unit/indium-debugger-test.el index 02b011f..c764a3e 100644 --- a/test/unit/indium-debugger-test.el +++ b/test/unit/indium-debugger-test.el @@ -26,59 +26,45 @@ (describe "Debugging frames are correctly set" (it "can set debugger frames and current frame" - (with-fake-indium-connection - (let ((frames '(first second)) - (current-frame 'first)) - (indium-debugger-set-frames frames) - (expect (indium-current-connection-frames) :to-be frames) - (expect (indium-current-connection-current-frame) :to-be current-frame)))) - - (it "can set the current frame" - ;; We're not interested in buffer setups - (spy-on 'indium-debugger-get-buffer-create) - - (with-fake-indium-connection - (indium-debugger-set-current-frame 'current) - (expect (indium-current-connection-current-frame) :to-be 'current))) + (let ((frames '(first second)) + (current-frame 'first)) + (indium-debugger-set-frames frames) + (expect indium-debugger-frames :to-be frames) + (expect indium-debugger-current-frame :to-be current-frame))) (it "can unset the debugging frames" - (with-fake-indium-connection - (indium-debugger-set-frames '(first second)) - (indium-debugger-unset-frames) - (expect (indium-current-connection-frames) :to-be nil) - (expect (indium-current-connection-current-frame) :to-be nil)))) + (indium-debugger-set-frames '(first second)) + (indium-debugger-unset-frames) + (expect indium-debugger-frames :to-be nil) + (expect indium-debugger-current-frame :to-be nil))) (describe "Jumping to the next/previous frame" (before-each (spy-on 'indium-debugger-select-frame)) (it "can select the next frame" - (with-fake-indium-connection - (let ((frames '(first second))) - (indium-debugger-set-frames frames) - (indium-debugger-set-current-frame 'second) - (indium-debugger-next-frame) - (expect 'indium-debugger-select-frame :to-have-been-called-with 'first)))) + (let ((frames '(first second))) + (indium-debugger-set-frames frames) + (setq indium-debugger-current-frame 'second) + (indium-debugger-next-frame) + (expect 'indium-debugger-select-frame :to-have-been-called-with 'first))) (it "can select the previous frame" - (with-fake-indium-connection - (let ((frames '(first second))) - (indium-debugger-set-frames frames) - (indium-debugger-previous-frame) - (expect 'indium-debugger-select-frame :to-have-been-called-with 'second)))) + (let ((frames '(first second))) + (indium-debugger-set-frames frames) + (indium-debugger-previous-frame) + (expect 'indium-debugger-select-frame :to-have-been-called-with 'second))) (it "should throw when selecting the next frame if it does not exist" - (with-fake-indium-connection - (let ((frames '(first second))) - (indium-debugger-set-frames frames) - (expect (indium-debugger-next-frame) :to-throw 'user-error)))) + (let ((frames '(first second))) + (indium-debugger-set-frames frames) + (expect (indium-debugger-next-frame) :to-throw 'user-error))) (it "should throw when selecting the previous frame if it does not exist" - (with-fake-indium-connection - (let ((frames '(first second))) - (indium-debugger-set-frames frames) - (indium-debugger-set-current-frame 'second) - (expect (indium-debugger-previous-frame) :to-throw 'user-error))))) + (let ((frames '(first second))) + (indium-debugger-set-frames frames) + (setq indium-debugger-current-frame 'second) + (expect (indium-debugger-previous-frame) :to-throw 'user-error)))) (describe "Regression test for GitHub issue 53" (before-each @@ -94,9 +80,9 @@ ;; flickering. Since stepping over or into cause the execution to be resumed ;; and paused, the debugger buffer should not be killed. (it "should not killing the debugger buffer when execution is resumed" - (spy-on 'indium-backend-resume) + (spy-on 'indium-client-resume) (expect (get-buffer (indium-debugger--buffer-name-no-file)) :not :to-be nil) - (indium-debugger-resume) + (indium-client-resume) (expect (get-buffer (indium-debugger--buffer-name-no-file)) :not :to-be nil))) (describe "Debugger stepping" @@ -104,47 +90,42 @@ ;; resumed, which happens between each step over/into/out. (it "should not unset the debugger buffer when stepping" (spy-on 'indium-debugger-unset-current-buffer) - (spy-on 'indium-backend-step-into) - (spy-on 'indium-backend-step-out) - (spy-on 'indium-backend-step-over) + (spy-on 'indium-client-step-into) + (spy-on 'indium-client-step-out) + (spy-on 'indium-client-step-over) (indium-debugger-step-into) (expect #'indium-debugger-unset-current-buffer :not :to-have-been-called) - (indium-debugger-step-over) + (indium-client-step-over) (expect #'indium-debugger-unset-current-buffer :not :to-have-been-called) - (indium-debugger-step-out) + (indium-client-step-out) (expect #'indium-debugger-unset-current-buffer :not :to-have-been-called)) - (it "should call the backend when stepping into" - (with-fake-indium-connection - (spy-on 'indium-backend-step-into) - (indium-debugger-step-into) - (expect #'indium-backend-step-into :to-have-been-called-with 'fake))) - - (it "should call the backend when stepping over" - (with-fake-indium-connection - (spy-on 'indium-backend-step-over) - (indium-debugger-step-over) - (expect #'indium-backend-step-over :to-have-been-called-with 'fake))) - - (it "should call the backend when stepping out" - (with-fake-indium-connection - (spy-on 'indium-backend-step-out) - (indium-debugger-step-out) - (expect #'indium-backend-step-out :to-have-been-called-with 'fake))) - - (it "should call the backend when resuming execution" - (with-fake-indium-connection - (spy-on 'indium-backend-resume) - (indium-debugger-resume) - (expect #'indium-backend-resume :to-have-been-called-with 'fake))) - - (it "should call the backend when jumping to a location" - (with-fake-indium-connection - (spy-on 'indium-backend-continue-to-location) - (spy-on 'indium-script-generated-location-at-point :and-return-value 'location) - (indium-debugger-here) - (expect #'indium-backend-continue-to-location :to-have-been-called-with 'fake 'location)))) + (it "should call the client when stepping into" + (spy-on 'indium-client-step-into) + (indium-debugger-step-into) + (expect #'indium-client-step-into :to-have-been-called)) + + (it "should call the client when stepping over" + (spy-on 'indium-client-step-over) + (indium-debugger-step-over) + (expect #'indium-client-step-over :to-have-been-called)) + + (it "should call the client when stepping out" + (spy-on 'indium-client-step-out) + (indium-debugger-step-out) + (expect #'indium-client-step-out :to-have-been-called)) + + (it "should call the client when resuming execution" + (spy-on 'indium-client-resume) + (indium-debugger-resume) + (expect #'indium-client-resume :to-have-been-called)) + + (it "should call the client when jumping to a location" + (spy-on 'indium-client-continue-to-location) + (indium-debugger-here) + (expect #'indium-client-continue-to-location + :to-have-been-called-with (indium-location-at-point)))) (provide 'indium-debugger-test) ;;; indium-debugger-test.el ends here diff --git a/test/unit/indium-inspector-test.el b/test/unit/indium-inspector-test.el index a559369..108561c 100644 --- a/test/unit/indium-inspector-test.el +++ b/test/unit/indium-inspector-test.el @@ -23,20 +23,17 @@ (require 'buttercup) (require 'indium-inspector) +(require 'indium-structs) (describe "Inspector should split properties to a better looking form" :var (native non-native) (before-all - (setq native '((value (description . "function f() { [native code] }") ))) - (setq non-native '((value (description . "42") )))) - - (it "can detect native code property" - (expect (indium-inspector--native-property-p native) - :to-be-truthy)) - - (it "can detect non-native code property" - (expect (indium-inspector--native-property-p non-native) - :to-be nil)) + (setq native (indium-property-from-alist + '((name . "foo") + (value . ((description . "function f() { [native code] }")))))) + (setq non-native (indium-property-from-alist + '((name . "foo") + (value . ((description . "42"))))))) (it "can split empty property list" (expect (indium-inspector--split-properties '()) diff --git a/test/unit/indium-interaction-test.el b/test/unit/indium-interaction-test.el index 7aae2d7..91c288f 100644 --- a/test/unit/indium-interaction-test.el +++ b/test/unit/indium-interaction-test.el @@ -25,99 +25,30 @@ (require 'assess) (require 'indium-interaction) - -(describe "Launching and connecting Indium" - (after-each - (setq indium-workspace-configuration nil)) - - (it "should fail to connect when there is no .indium.json file" - (assess-with-filesystem '() - (expect (indium-connect) :to-throw))) - - (it "should fail to connect with an invalid project type" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"type\": \"foo\"}]}")) - (expect (indium-connect) :to-throw))) - - (it "should call `indium-connect-to-nodejs' for node projects" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"type\": \"node\"}]}")) - (spy-on #'indium-connect-to-nodejs) - (indium-connect) - (expect #'indium-connect-to-nodejs :to-have-been-called-times 1))) - - (it "should call `indium-connect-to-chrome' for chrome projects" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"type\": \"chrome\"}]}")) - (spy-on #'indium-connect-to-chrome) - (indium-connect) - (expect #'indium-connect-to-chrome :to-have-been-called-times 1))) - - (it "should fail to launch when there is no .indium.json file" - (assess-with-filesystem '() - (expect (indium-launch) :to-throw))) - - (it "should fail to launch with an invalid project type" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"type\": \"foo\"}]}")) - (expect (indium-launch) :to-throw))) - - (it "should call `indium-launch-nodejs' for node projects" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"type\": \"node\"}]}")) - (spy-on #'indium-launch-nodejs) - (indium-launch) - (expect #'indium-launch-nodejs :to-have-been-called-times 1))) - - (it "should call `indium-launch-chrome' for chrome projects" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"type\": \"chrome\"}]}")) - (spy-on #'indium-launch-chrome) - (indium-launch) - (expect #'indium-launch-chrome :to-have-been-called-times 1))) - - (it "should fail to reconnect when there is no active connection" - (expect (indium-reconnect) :to-throw)) - - (it "should call `indium-backend-reconnect' when reconnecting" - (let ((indium-current-connection (indium-connection-create :backend 'foo))) - (spy-on #'indium-backend-reconnect) - (indium-reconnect) - 'foo))) +(require 'indium-client) (describe "Killing previous connections when connecting" - (after-each - (when-indium-connected (indium-quit))) - (it "should kill the previous connection process when there is one" - (let ((indium-current-connection (indium-connection-create - :process 'first-process))) - (spy-on #'indium-connect-to-nodejs) - (spy-on 'y-or-n-p :and-return-value t) - - (spy-on 'kill-process) - (spy-on 'process-buffer) - (spy-on 'process-status :and-return-value 'run) - (spy-on 'indium-backend-close-connection) - - (with-js2-file (indium-launch)) - - (expect #'kill-process :to-have-been-called-with 'first-process) - (expect #'indium-backend-close-connection :to-have-been-called)))) - -(describe "Setting indium-workspace-connection" - (after-each - (setq indium-workspace-configuration nil)) - - (it "should should set `indium-workspace-connection' to nil when disconnecting" - (setq indium-workspace-configuration '(type . "node")) - (let ((indium-current-connection (indium-connection-create - :process 'first-process))) - (spy-on #'indium-connect-to-nodejs) - (spy-on 'y-or-n-p :and-return-value t) - - (spy-on 'kill-process) - (spy-on 'process-buffer) - (spy-on 'process-status :and-return-value 'run) - (spy-on 'indium-backend-close-connection) - - (indium-quit) - - (expect indium-workspace-configuration :to-be nil)))) + (spy-on #'indium-client-process-live-p :and-return-value nil) + (spy-on #'indium-client-start) + (spy-on #'indium-maybe-quit) + + (indium-connect) + (expect #'indium-maybe-quit :to-have-been-called)) + + (it "should call `indium-quit' when the user confirms" + (spy-on #'yes-or-no-p :and-return-value t) + (spy-on #'indium-client-process-live-p :and-return-value t) + (spy-on #'indium-quit) + (indium-maybe-quit) + (expect #'indium-quit :to-have-been-called)) + + (it "should not call `indium-quit' when the user cancels" + (spy-on #'yes-or-no-p :and-return-value nil) + (spy-on #'indium-client-process-live-p :and-return-value t) + (spy-on #'indium-quit) + (indium-maybe-quit) + (expect #'indium-quit :not :to-have-been-called))) (describe "Finding the AST node to evaluate" (it "can find variable nodes" @@ -215,6 +146,7 @@ (describe "Adding/removing invalid breakpoints" (it "should not try to add duplicate breakpoints" + (spy-on #'indium-client-add-breakpoint) (with-js2-file (insert "let a = 2;") (indium-add-breakpoint) @@ -231,6 +163,7 @@ (expect (indium-edit-breakpoint-condition) :to-throw 'user-error))) (it "should not try to add conditional breakpoints twice" + (spy-on #'indium-client-add-breakpoint) (with-js2-file (insert "let a = 2;") (indium-add-breakpoint) @@ -238,7 +171,7 @@ (describe "Adding conditional breakpoints" (it "should call `indium-add-breakpoint' with a condition (GH issue #92)" - (spy-on 'indium-add-breakpoint) + (spy-on #'indium-add-breakpoint) (with-js2-file (insert "let a = 2;") (indium-add-conditional-breakpoint "foo") @@ -246,7 +179,8 @@ (describe "Interaction mode" (it "should remove breakpoints when killing a buffer" - (spy-on 'indium-breakpoint-remove-breakpoints-from-current-buffer) + (spy-on #'indium-breakpoint-remove-breakpoints-from-current-buffer) + (spy-on #'indium-client-add-breakpoint) (with-js2-file (indium-add-breakpoint) (kill-buffer) diff --git a/test/unit/indium-list-scripts-test.el b/test/unit/indium-list-scripts-test.el deleted file mode 100644 index ca0fc4c..0000000 --- a/test/unit/indium-list-scripts-test.el +++ /dev/null @@ -1,46 +0,0 @@ -;;; indium-list-scripts-test.el --- Tests for indium-list-scripts.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton - -;; 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 . - -;;; Commentary: - -;;; Code: - -(require 'buttercup) -(require 'indium-list-scripts) - -(describe "Listing entries" - (it "Should signal an error when there is no connection" - (let ((indium-current-connection nil)) - (expect (indium-list-scripts) :to-throw 'user-error)))) - -(describe "Making tabulated list entries" - (it "Should be able to make entries" - (spy-on 'indium-script-get-file :and-return-value nil) - (let* ((script (indium-script-create :id "1" :url "foo.html")) - (entry (indium-list-scripts--make-entry script))) - (expect entry :to-equal '("1" ["foo.html"])))) - - (it "Should make an entry with a button when there is a local file" - (spy-on 'indium-script-get-file :and-return-value "bar.html") - (let* ((script (indium-script-create :id "1" :url "foo.html")) - (entry (indium-list-scripts--make-entry script))) - (expect (cadr (elt (cadr entry) 0)) :to-equal 'action)))) - -(provide 'indium-list-scripts-test) -;;; indium-list-scripts-test.el ends here diff --git a/test/unit/indium-nodejs-test.el b/test/unit/indium-nodejs-test.el index afba9d3..cc0cbc3 100644 --- a/test/unit/indium-nodejs-test.el +++ b/test/unit/indium-nodejs-test.el @@ -18,6 +18,8 @@ ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . +;;; Commentary: + ;;; Code: (require 'buttercup) @@ -25,48 +27,29 @@ (describe "Executing NodeJS processes" (it "should set the correct flags when executing nodejs" - (spy-on 'make-process) - (spy-on 'switch-to-buffer) - (spy-on 'process-buffer) - - (spy-on 'indium-nodejs--command-with-flags) - - (with-js2-file (indium-launch-nodejs)) + (spy-on #'make-process) + (spy-on #'set-process-query-on-exit-flag) + (spy-on #'set-process-sentinel) + (spy-on #'set-process-filter) + (spy-on #'switch-to-buffer) + (spy-on #'process-buffer) + + (spy-on #'indium-nodejs--command-with-flags) + + (with-js2-file + (indium-launch-nodejs '((command . "node index.js") + (inspect-brk . t)))) (expect #'indium-nodejs--command-with-flags - :to-have-been-called-with)) + :to-have-been-called-with "node index.js" t)) (it "should append extra flags" - (spy-on #'indium-nodejs--command :and-return-value "node foo") - (expect (indium-nodejs--command-with-flags) + (expect (indium-nodejs--command-with-flags "node foo" nil) :to-equal "node --inspect foo") - (spy-on #'indium-nodejs--inspect-brk :and-return-value t) - (expect (indium-nodejs--command-with-flags) + (expect (indium-nodejs--command-with-flags "node foo" t) :to-equal "node --inspect-brk foo") ;; Regression for GitHub issue #150 - (spy-on #'indium-nodejs--command :and-return-value "ENV_VAR=\"VAL\" node foo") - (expect (indium-nodejs--command-with-flags) + (expect (indium-nodejs--command-with-flags "ENV_VAR=\"VAL\" node foo" t) :to-equal "ENV_VAR=\"VAL\" node --inspect-brk foo"))) -(describe "Connecting to a NodeJS process" - (it "should find the websocket URL from the process output" - (spy-on 'indium-nodejs--connect) - (let ((output "To start debugging, open the following URL in Chrome: - chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/43c07a90-1aed-4753-961d-1d449b21e84f")) - (indium-nodejs--connect-to-process 'process output) - (expect #'indium-nodejs--connect - :to-have-been-called-with "127.0.0.1" "9229" "43c07a90-1aed-4753-961d-1d449b21e84f" 'process)))) - -(describe "Connecting to a NodeJS process" - (it "should connect to process using a host, port and path." - (spy-on 'indium-v8--open-ws-connection) - (let ((path "43c07a90-1aed-4753-961d-1d449b21e84f")) - (indium-nodejs--connect "localhost" 9229 path) - (expect #'indium-v8--open-ws-connection - :to-have-been-called-with - (format "file://%s" default-directory) - "ws://localhost:9229/43c07a90-1aed-4753-961d-1d449b21e84f" - nil - t)))) - (provide 'indium-nodejs-test) ;;; indium-nodejs-test.el ends here diff --git a/test/unit/indium-repl-test.el b/test/unit/indium-repl-test.el index 728220d..ae4a9e3 100644 --- a/test/unit/indium-repl-test.el +++ b/test/unit/indium-repl-test.el @@ -30,19 +30,20 @@ (require 'buttercup) (describe "Switching from and to the REPL buffer" - (it "should throw an error if there's no REPL buffer" - (spy-on 'indium-repl-get-buffer :and-return-value nil) + (it "should throw an error when not connected" + (spy-on #'indium-client-process-live-p :and-return-value nil) (expect (indium-switch-to-repl-buffer) :to-throw 'user-error)) - (it "should be able to switch to the REPL buffer" - (spy-on 'indium-repl-get-buffer :and-return-value 'repl) - (spy-on 'pop-to-buffer) + (it "should be able to switch to the REPL buffer when connected" + (spy-on #'indium-client-process-live-p :and-return-value t) + (spy-on #'pop-to-buffer) (indium-switch-to-repl-buffer) - (expect #'pop-to-buffer :to-have-been-called-with 'repl t)) + (expect #'pop-to-buffer :to-have-been-called)) (it "should be able to switch back from the REPL buffer" + (spy-on #'indium-client-process-live-p :and-return-value t) + (spy-on 'pop-to-buffer) (let ((indium-repl-switch-from-buffer 'from)) - (spy-on 'pop-to-buffer) (indium-repl-pop-buffer) (expect #'pop-to-buffer :to-have-been-called-with 'from t)))) diff --git a/test/unit/indium-script-test.el b/test/unit/indium-script-test.el deleted file mode 100644 index e46f16d..0000000 --- a/test/unit/indium-script-test.el +++ /dev/null @@ -1,214 +0,0 @@ -;;; indium-script-test.el --- Unit tests for indium-script.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton -;; Keywords: test - -;; 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 . - -;;; Commentary: - -;;; Code: - -(require 'buttercup) -(require 'assess) -(require 'indium-script) -(require 'indium-workspace) - -(defvar indium-script--test-fs - '((".indium.json" "{\"configurations\": [{}]}") - ("js" ("foo.js" "foo.js.map" "bar.js"))) - "Fake filesystem used in script tests.") - -(describe "Looking up scripts" - (it "should be able to retrieve parsed scripts url" - (with-fake-indium-connection - (indium-script-add-script-parsed "1" "foo") - (expect (indium-script-url (indium-script-find-by-id "1")) :to-equal "foo"))) - - (it "should be able to retrieve parsed scripts sourcemap url" - (with-fake-indium-connection - (indium-script-add-script-parsed "1" "foo" "foo-map") - (expect (indium-script-sourcemap-url (indium-script-find-by-id "1")) :to-equal "foo-map"))) - - (it "should be able to retrieve parsed scripts ids" - (with-fake-indium-connection - (indium-script-add-script-parsed "1" "foo") - (expect (indium-script-id (indium-script-find-from-url "foo")) :to-equal "1"))) - - (it "should be able to return all scripts with a sourcemap" - (with-fake-indium-connection - (indium-script-add-script-parsed "1" "foo") - (indium-script-add-script-parsed "2" "bar" "bar.map") - (expect (seq-map #'indium-script-id - (indium-script-all-scripts-with-sourcemap)) - :to-equal '("2")))) - - (it "should be able to find scripts by location with file" - (spy-on 'indium-script-find-from-file :and-return-value 'script) - (spy-on 'indium-script-find-from-url) - (let* ((location (indium-location-create :file "foo")) - (script (indium-script-find-from-location location))) - (expect #'indium-script-find-from-file :to-have-been-called-with "foo") - (expect #'indium-script-find-from-url :not :to-have-been-called) - (expect script :to-be 'script))) - - (it "should be able to find scripts by location with url" - (spy-on 'indium-script-find-from-file) - (spy-on 'indium-script-find-from-url :and-return-value 'script) - (let* ((location (indium-location-create :file "foo")) - (script (indium-script-find-from-location location))) - (expect #'indium-script-find-from-url :to-have-been-called-with "foo") - (expect script :to-be 'script))) - - (it "should be able to find the sourcemap file for a script" - (assess-with-filesystem indium-script--test-fs - (with-fake-indium-connection - (indium-script-add-script-parsed "1" "http://localhost/js/foo.js" "foo.js.map") - (let ((script (indium-script-find-by-id "1"))) - (expect (indium-script--sourcemap-file script) - :to-equal (expand-file-name "js/foo.js.map")))))) - - (it "should be able to parse a sourcemap data url for a script (base64)" - (let* ((sourcemap-json '((version . 3) - (file . "js/foo.js") - (sources . ["foo-1.js" "foo-2.js"]) - (names . []) - (mappings . ";;;;;;kBAAe;AAAA,SAAM,QAAQ,GAAR,CAAY,aAAZ,CAAN;AAAA,C"))) - (sourcemap (indium-sourcemap--decode sourcemap-json))) - (with-fake-indium-connection - (indium-script-add-script-parsed - "1" "http://localhost/js/foo.js" - (concat "data:application/json;charset=utf-8;base64," - (base64-encode-string (json-encode sourcemap-json)))) - (let ((script (indium-script-find-by-id "1"))) - (expect (indium-script--sourcemap-from-data-url script) - :to-equal sourcemap))))) - - (it "should be able to parse a sourcemap data url for a script (url-encoded)" - (let* ((sourcemap-json '((version . 3) - (file . "js/foo.js") - (sources . ["foo-1.js" "foo-2.js"]) - (names . []) - (mappings . ";;;;;;kBAAe;AAAA,SAAM,QAAQ,GAAR,CAAY,aAAZ,CAAN;AAAA,C"))) - (sourcemap (indium-sourcemap--decode sourcemap-json))) - (with-fake-indium-connection - (indium-script-add-script-parsed - "1" "http://localhost/js/foo.js" - (concat "data:application/json;charset=utf-8," - (url-hexify-string (json-encode sourcemap-json)))) - (let ((script (indium-script-find-by-id "1"))) - (expect (indium-script--sourcemap-from-data-url script) - :to-equal sourcemap)))))) - -(describe "Adding scripts" - (it "should not multiple scripts with the same url" - (with-fake-indium-connection - (indium-script-add-script-parsed "1" 'url) - (indium-script-add-script-parsed "2" 'url) - (expect (indium-script-id (indium-script-find-from-url 'url)) - :to-equal "2")))) - -(describe "Handling sourcemap files" - (it "should convert all sourcemap entry paths to absolute paths" - (spy-on 'indium-workspace-lookup-file :and-return-value "/foo/bar/script.js") - (let* ((script (indium-script-create :url "/bar/script.js")) - (entry (make-indium-source-mapping :source "./baz.js")) - (map (make-indium-sourcemap :generated-mappings (make-vector 1 entry)))) - (assess-with-filesystem indium-script--test-fs - (indium-script--transform-sourcemap-sources map script)) - (expect (indium-source-mapping-source entry) - :to-equal "/foo/bar/baz.js"))) - - (it "should not convert sourcemap entries paths that are absolute" - (spy-on 'indium-workspace-lookup-file :and-return-value "/foo/bar/script.js") - (let* ((script (indium-script-create :url "/bar/script.js")) - (entry (make-indium-source-mapping :source "/baz.js")) - (map (make-indium-sourcemap :generated-mappings (make-vector 1 entry)))) - (assess-with-filesystem indium-script--test-fs - (indium-script--transform-sourcemap-sources map script)) - (expect (indium-source-mapping-source entry) - :to-equal "/baz.js")))) - -(describe "Handling sourcemap path overrides" - (it "should expand root token in path overrides" - (spy-on 'indium-workspace-root :and-return-value "/my/project") - (expect (indium-script--expand-path-override "foo/bar") - :to-equal "foo/bar") - (expect (indium-script--expand-path-override "${root}/foo/bar") - :to-equal "/my/project/foo/bar") - (expect (indium-script--expand-path-override "${webRoot}/foo/bar") - :to-equal "/my/project/foo/bar")) - - (it "should apply default sourcemap path overrides" - (assess-with-filesystem indium-script--test-fs - - (let* ((script (indium-script-create :url "/js/foo.js")) - (entry (make-indium-source-mapping :source "webpack:///./js/foo.js")) - (map (make-indium-sourcemap :generated-mappings (make-vector 1 entry)))) - (indium-script--transform-sourcemap-sources map script) - (expect (indium-source-mapping-source entry) - :to-equal (expand-file-name "./js/foo.js"))) - - (let* ((script (indium-script-create :url "/js/foo.js")) - (entry (make-indium-source-mapping :source "webpack:///src/js/foo.js")) - (map (make-indium-sourcemap :generated-mappings (make-vector 1 entry)))) - (indium-script--transform-sourcemap-sources map script) - (expect (indium-source-mapping-source entry) - :to-equal (expand-file-name "./js/foo.js"))) - - (let* ((script (indium-script-create :url "/node_modules/foo/index.js")) - (entry (make-indium-source-mapping :source "webpack:///./~/foo/index.js")) - (map (make-indium-sourcemap :generated-mappings (make-vector 1 entry)))) - (indium-script--transform-sourcemap-sources map script) - (expect (indium-source-mapping-source entry) - :to-equal (expand-file-name "./node_modules/foo/index.js"))) - - (let* ((script (indium-script-create :url "/foo.js")) - (entry (make-indium-source-mapping :source "webpack:///foo.js")) - (map (make-indium-sourcemap :generated-mappings (make-vector 1 entry)))) - (indium-script--transform-sourcemap-sources map script) - (expect (indium-source-mapping-source entry) - :to-equal "/foo.js")))) - - (it "should convert root directories whenapplying sourcemap path overrides" - (assess-with-filesystem indium-script--test-fs - - (let* ((indium-workspace-configuration '((root . "js"))) - (script (indium-script-create :url "/js/foo.js")) - (entry (make-indium-source-mapping :source "webpack:///./foo.js")) - (map (make-indium-sourcemap :generated-mappings (make-vector 1 entry)))) - (indium-script--transform-sourcemap-sources map script) - (expect (indium-source-mapping-source entry) - :to-equal (expand-file-name "./js/foo.js"))))) - - (it "should apply custom sourcemap path overrides" - (assess-with-filesystem indium-script--test-fs - (let* ((indium-workspace-configuration - '((webRoot . "js") - (sourceMapPathOverrides . (("foo://" . "${webRoot}/foo"))))) - (script (indium-script-create :url "/js/foo/bar.js")) - (entry (make-indium-source-mapping :source "foo:///bar.js")) - (map (make-indium-sourcemap :generated-mappings (make-vector 1 entry)))) - (indium-script--transform-sourcemap-sources map script) - (expect (indium-source-mapping-source entry) - :to-equal (expand-file-name "./js/foo/bar.js")))))) - -(describe "Downloading sourcemap files" - (it "should return nil when download is not possible" - (should-not (indium-script--download "foo")))) - -(provide 'indium-script-test) -;;; indium-script-test.el ends here diff --git a/test/unit/indium-sourcemap-test.el b/test/unit/indium-sourcemap-test.el deleted file mode 100644 index b665f72..0000000 --- a/test/unit/indium-sourcemap-test.el +++ /dev/null @@ -1,134 +0,0 @@ -;;; indium-sourcemap-test.el --- Test for indium-sourcemap.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton - -;; 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 . - -;;; Commentary: - -;;; Code: - -(require 'buttercup) -(require 'map) - -(require 'indium-sourcemap) - -(defconst indium-test-sourcemap-json - '((version . 3) - (file . "test.coffee.map") - (sources . ["test.coffee"]) - (names . ["foo" "bar"]) - (mappings . "\ -AAAC;;;EAAA,eAAK,IAAL,GAAa,SAAA,CAAA,QAAA,CAAA;;;MACV,2BAAmB,YAAnB,aAAA,\ -CAAA,KAAA,CAAA;QAAI,OAAe;QAAT;oBACR,QAAQ,KAAR,CAAc,IAAd,EAAoB,KAApB;;;;;\ -EAED,GAAA,GAAM,CAAA;AAAA,IAAC,CAAD;AAAA,IAAG,CAAH;AAAA,IAAK,CAAL;AAAA,\ -EAAA;EACV,MAAA,GAAS,GAAG,IAAH,CAAQ,SAAA,CAAA,IAAA,CAAA;WAAU,IAAA,CAAA,\ -CAAA,CAAO;GAAzB;EAET,OAAO,IAAP,CAAY,MAAZ"))) - -(describe "Decoding sourcemaps" - (before-each - (indium-sourcemap--reset-cache)) - - (after-each - (indium-sourcemap--reset-cache)) - - (it "should read files as JSON when decoding" - (spy-on 'indium-sourcemap--decode :and-call-through) - ;; (spy-on 'json-read-file :and-return-value indium-test-sourcemap-json) - (spy-on 'insert-file-contents :and-call-fake - (lambda (_) - (insert (json-encode indium-test-sourcemap-json)))) - (indium-sourcemap-from-file "foo.js") - (expect #'indium-sourcemap--decode - :to-have-been-called-with indium-test-sourcemap-json)) - - (it "should read strings as JSON when decoding" - (spy-on 'indium-sourcemap--decode :and-call-through) - (spy-on 'json-read-from-string :and-return-value indium-test-sourcemap-json) - (indium-sourcemap-from-string "test") - (expect #'indium-sourcemap--decode - :to-have-been-called-with indium-test-sourcemap-json)) - - (it "should fail if there is no sourcemap version" - (expect (indium-sourcemap--decode '((sources . ["foo.js"]) - (file . "bar.js") - (names . []) - (mappings . ""))) - :to-throw)) - - (it "Should decode Base64" - (expect (indium--base64-decode ?A) :to-equal 0) - (expect (indium--base64-decode ?Z) :to-equal 25) - (expect (indium--base64-decode ?a) :to-equal 26) - (expect (indium--base64-decode ?z) :to-equal 51) - (expect (indium--base64-decode ?0) :to-equal 52) - (expect (indium--base64-decode ?9) :to-equal 61) - (expect (indium--base64-decode ?+) :to-equal 62) - (expect (indium--base64-decode ?/) :to-equal 63) - (expect (indium--base64-decode ?%) :to-throw)) - - (it "Should convert VLQ sign" - (expect (indium--from-vlq-signed 2) :to-equal 1) - (expect (indium--from-vlq-signed 3) :to-equal -1) - (expect (indium--from-vlq-signed 4) :to-equal 2) - (expect (indium--from-vlq-signed 5) :to-equal -2)) - - (it "Should decode VLQ" - (expect (indium--base64-vlq-decode (string-to-list "A")) - :to-equal `(:value 0 :rest ,(string-to-list ""))) - - (expect (indium--base64-vlq-decode (string-to-list "zA")) - :to-equal `(:value -9 :rest ,(string-to-list ""))) - - (expect (indium--base64-vlq-decode (string-to-list "zza")) - :to-equal `(:value -13625 :rest ,(string-to-list ""))) - - (expect (indium--base64-vlq-decode (string-to-list "gzX")) - :to-equal `(:value 12080 :rest ,(string-to-list "")))) - - (it "Should decode sourcemaps" - (let ((sourcemap (indium-sourcemap--decode indium-test-sourcemap-json))) - (expect (indium-sourcemap-sources sourcemap) :to-equal '["test.coffee"]) - (expect (indium-sourcemap-names sourcemap) :to-equal '["foo" "bar"]) - (expect (length (indium-sourcemap-generated-mappings sourcemap)) :to-equal 62) - (let ((mapping (elt (indium-sourcemap-generated-mappings sourcemap) 20))) - (expect (indium-source-mapping-p mapping) :to-be-truthy) - (expect (indium-source-mapping-generated-column mapping) :to-equal 28) - (expect (indium-source-mapping-generated-line mapping) :to-equal 10) - (expect (indium-source-mapping-source mapping) :to-equal "test.coffee") - (expect (indium-source-mapping-original-column mapping) :to-equal 14) - (expect (indium-source-mapping-original-line mapping) :to-equal 3) - (expect (indium-source-mapping-name mapping) :to-be nil))))) - -(describe "Sourcemap lookups" - (it "Should lookup original positions" - (let ((sourcemap (indium-sourcemap--decode indium-test-sourcemap-json))) - (expect (indium-sourcemap-original-position-for sourcemap 23 10) - :to-equal (list :source "test.coffee" - :line 8 - :column 8 - :name nil)))) - - (it "Should lookup generated positions" - (let ((sourcemap (indium-sourcemap--decode indium-test-sourcemap-json))) - (expect (indium-sourcemap-generated-position-for sourcemap "test.coffee" 8 8) - :to-equal (list :source "test.coffee" - :line 23 - :column 9 - :name nil))))) - -(provide 'indium-sourcemap-test) -;;; indium-sourcemap-test.el ends here diff --git a/test/unit/indium-structs-test.el b/test/unit/indium-structs-test.el index bbc6853..424a6f4 100644 --- a/test/unit/indium-structs-test.el +++ b/test/unit/indium-structs-test.el @@ -25,48 +25,122 @@ ;;; Code: (require 'buttercup) -(require 'assess) (require 'indium-structs) -(require 'cl-lib) - -(describe "Setting current connection slots" - (it "should be able to set the frames" - (with-indium-connection (indium-connection-create) - (setf (indium-current-connection-frames) 'foo) - (expect (indium-current-connection-frames) - :to-be 'foo))) - - (it "should be able to set the current frame" - (with-indium-connection (indium-connection-create) - (setf (indium-current-connection-current-frame) 'foo) - (expect (indium-current-connection-current-frame) - :to-be 'foo)))) - -(describe "Struct creation" - (it "should be able to make locations from script ids" - (spy-on 'indium-script-get-file :and-return-value "foo.js") - (spy-on 'indium-script-find-by-id :and-return-value "id") - (let ((loc (indium-location-from-script-id - :script-id "id" - :line 2 - :column 3))) - (expect #'indium-script-find-by-id :to-have-been-called-with "id") - (expect (indium-location-file loc) :to-equal "foo.js"))) +(require 'assess) +(require 'seq) + +(describe "Locations" + (it "Should be able to make locations at point" + (assess-with-filesystem '(("index.js" "let foo = 1;\nlet bar = 2;")) + (with-current-buffer (find-file-noselect "index.js") + (goto-char (point-max)) + (let ((loc (indium-location-at-point))) + (expect (indium-location-file loc) :to-equal (expand-file-name "index.js")) + (expect (indium-location-line loc) :to-equal 2))))) + (it "Should be able to make locations from alists" + (let ((loc (indium-location-from-alist '((file . "index.js") + (line . 22) + (column . 0))))) + (expect (indium-location-file loc) :to-equal "index.js") + (expect (indium-location-line loc) :to-equal 22) + (expect (indium-location-column loc) :to-equal 0)))) + +(describe "Breakpoints" (it "Should be able to make breakpoints" (let ((brk (indium-breakpoint-create :id 'id - :original-location (indium-location-create - :line 5 - :column 2 - :file "foo.js")))) + :condition "foo === bar"))) (expect (indium-breakpoint-id brk) :to-be 'id) - (expect (indium-location-file (indium-breakpoint-original-location brk)) - :to-equal "foo.js") - (expect (indium-location-line (indium-breakpoint-original-location brk)) - :to-equal 5) - (expect (indium-location-column (indium-breakpoint-original-location brk)) - :to-equal 2)))) + (expect (indium-breakpoint-condition brk) + :to-equal "foo === bar") + (expect (indium-breakpoint-location brk) + :to-be nil))) + + (it "Should be have the location of its overlay" + (with-temp-buffer + (insert "let foo = 1;\nlet bar = 2;") + (goto-char (point-min)) + (let* ((ov (make-overlay (point) (point))) + (brk (indium-breakpoint-create :overlay ov))) + (expect (indium-location-line (indium-breakpoint-location brk)) + :to-equal 1) + (expect (indium-location-file (indium-breakpoint-location brk)) + :to-equal (buffer-file-name (overlay-buffer ov)))))) + + (it "Should follow the location of the overlay when it changes" + (with-temp-buffer + (insert "let foo = 1;\nlet bar = 2;") + (goto-char (point-min)) + (let* ((ov (make-overlay (point) (point))) + (brk (indium-breakpoint-create :overlay ov))) + (move-overlay ov (point-at-eol) (point-at-eol)) + (save-excursion + (goto-char (point-max)) + (expect (indium-location-line (indium-breakpoint-location brk)) + :to-equal 2)))))) + +(describe "Scopes" + (it "Should be able to make scopes from alists" + (let ((s (indium-scope-from-alist '((type . "closure") + (name . "this.foo") + (id . "25"))))) + (expect (indium-scope-id s) :to-equal "25") + (expect (indium-scope-type s) :to-equal "closure") + (expect (indium-scope-name s) :to-equal "this.foo")))) + +(describe "Frames" + (it "Should be able to make frames from alists" + (let ((f (indium-frame-from-alist '((scriptId . "22") + (functionName . "foo") + (location . ((file . "index.js") + (line . 22) + (column . 0))) + (scopeChain . [((type . "closure") + (name . "this.foo") + (id . "25")) + ((type . "local") + (name . "bar") + (id . "26"))]))))) + (expect (indium-frame-script-id f) :to-equal "22") + (expect (indium-frame-function-name f) :to-equal "foo") + (expect (length (indium-frame-scope-chain f)) :to-equal 2) + (expect (indium-scope-type (seq-elt (indium-frame-scope-chain f) 0)) + :to-equal "closure") + (expect (indium-scope-name (seq-elt (indium-frame-scope-chain f) 0)) + :to-equal "this.foo") + (expect (indium-scope-id (seq-elt (indium-frame-scope-chain f) 0)) + :to-equal "25") + (expect (indium-scope-type (seq-elt (indium-frame-scope-chain f) 1)) + :to-equal "local") + (expect (indium-scope-name (seq-elt (indium-frame-scope-chain f) 1)) + :to-equal "bar") + (expect (indium-scope-id (seq-elt (indium-frame-scope-chain f) 1)) + :to-equal "26") + (expect (indium-location-file (indium-frame-location f)) + :to-equal "index.js") + (expect (indium-location-line (indium-frame-location f)) + :to-equal 22) + (expect (indium-location-column (indium-frame-location f)) + :to-equal 0)))) + +(describe "Native properties" + :var (native non-native) + (before-all + (setq native (indium-property-from-alist + '((name . "foo") + (value . ((description . "function f() { [native code] }")))))) + (setq non-native (indium-property-from-alist + '((name . "foo") + (value . ((description . "42"))))))) + + (it "can detect native code property" + (expect (indium-property-native-p native) + :to-be-truthy)) + + (it "can detect non-native code property" + (expect (indium-property-native-p non-native) + :to-be nil))) (provide 'indium-structs-test) ;;; indium-structs-test.el ends here diff --git a/test/unit/indium-v8-test.el b/test/unit/indium-v8-test.el deleted file mode 100644 index 56e0838..0000000 --- a/test/unit/indium-v8-test.el +++ /dev/null @@ -1,195 +0,0 @@ -;;; indium-v8-test.el --- Tests for indium-v8.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton -;; Keywords: - -;; 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 . - -;;; Commentary: - -;; Tests for indium-v8.el - -;;; Code: - -(require 'buttercup) -(require 'indium-v8) - -(describe "Generating request ids" - (it "should increment request ids" - (let ((indium-v8--request-id 0)) - (expect (indium-v8--next-request-id) :to-be 1) - (expect (indium-v8--next-request-id) :to-be 2) - (expect (indium-v8--next-request-id) :to-be 3) - (expect (indium-v8--next-request-id) :to-be 4)))) - -(describe "V8 connection websocket" - (it "should be able to set a websocket" - (let ((conn (indium-connection-create))) - (setf (indium-connection-ws conn) 'foo) - (expect (indium-connection-ws conn) - :to-be 'foo)))) - -(describe "V8 connection handling" - (it "should be active if the websocket is open" - (spy-on 'wsc-connection-open-p :and-return-value t) - (spy-on 'indium-connection-ws :and-return-value 'ws) - (with-fake-indium-connection - (expect (indium-backend-active-connection-p 'v8) :to-be-truthy))) - - (it "should be inactive if the websocket is closed" - (let ((indium-current-connection (indium-connection-create :backend 'v8))) - (expect (indium-backend-active-connection-p 'v8) :to-be nil))) - - (it "should close the socket when closing the connection" - (spy-on 'wsc-close) - (with-indium-connection (indium-connection-create :backend 'v8) - (let ((ws (wsc-connection-create))) - (setf (wsc-connection-state ws) :open) - (map-put (indium-current-connection-props) 'ws ws) - (indium-backend-close-connection 'v8) - (expect #'wsc-close :to-have-been-called-with ws))))) - -(describe "Sending requests" - (it "should not send requests if the connection is closed" - (spy-on 'wsc-send) - (spy-on 'message) - (with-fake-indium-connection - (indium-v8--send-request 'foo) - (expect #'wsc-send :not :to-have-been-called))) - - (it "should display a warning message if the connection is closed" - (spy-on 'message) - (with-fake-indium-connection - (indium-v8--send-request 'foo) - (expect #'message :to-have-been-called-with "Socket connection closed"))) - - (it "should send requests if the connection is active" - (spy-on 'wsc-send) - (spy-on 'indium-backend-active-connection-p :and-return-value t) - (spy-on 'indium-v8--next-request-id :and-return-value 'id) - (with-indium-connection (indium-connection-create :backend 'v8) - (map-put (indium-current-connection-props) 'ws 'ws) - (indium-v8--send-request '((message . "message"))) - (expect #'wsc-send :to-have-been-called-with - 'ws (json-encode '((id . id) (message . "message")))))) - - (it "should register callbacks when sending requests" - (spy-on 'wsc-send) - (spy-on 'indium-backend-active-connection-p :and-return-value t) - (spy-on 'indium-v8--next-request-id :and-return-value 'id) - (with-indium-connection (indium-connection-create :backend 'v8) - (indium-v8--send-request '((message . "message")) 'callback) - (expect (map-elt (indium-current-connection-callbacks) 'id) :to-equal 'callback)))) - -(describe "Receiving responses" - (it "should evaluate `indium-script-parsed-hook' when a script gets parsed" - (spy-on 'indium-script-add-script-parsed :and-return-value 'script) - ;; Don't actually update breakpoints - (spy-on 'indium-breakpoint--update-after-script-parsed) - (spy-on 'test-hook-run) - (add-hook 'indium-script-parsed-hook 'test-hook-run) - (with-indium-connection (indium-connection-create :backend 'v8) - (indium-v8--handle-script-parsed '(params)) - (remove-hook 'indium-script-parsed-hook 'test-hook-run) - (expect #'test-hook-run :to-have-been-called-with 'script)))) - -(describe "Making completion expressions" - (it "should return \"this\" if there is no property to complete" - (expect (indium-v8--completion-expression "foo") - :to-equal "this")) - - (it "should return the parent property" - (expect (indium-v8--completion-expression "foo.bar") - :to-equal "foo") - (expect (indium-v8--completion-expression "foo.bar.baz") - :to-equal "foo.bar"))) - -(describe "Evaluating code" - (it "calls Runtime.evaluate with the expression to evaluate" - (spy-on 'indium-v8--send-request) - (with-fake-indium-connection - (indium-backend-evaluate 'v8 "foo") - (expect #'indium-v8--send-request :to-have-been-called-with - '((method . "Runtime.evaluate") - (params . ((expression . "foo") - (callFrameId . nil) - (generatePreview . t)))) - nil))) - - (it "calls Debugger.evaluateOnCallFrame when there is stack frame" - (spy-on 'indium-v8--send-request) - (with-indium-connection (indium-connection-create - :current-frame (indium-frame-create :id 1)) - (indium-backend-evaluate 'v8 "foo") - (expect #'indium-v8--send-request :to-have-been-called-with - '((method . "Debugger.evaluateOnCallFrame") - (params . ((expression . "foo") - (callFrameId . 1) - (generatePreview . t)))) - nil)))) - -(describe "V8 backend result description string" - (it "can render boolean descriptions formatted as string values (GitHub issue #52)" - (expect (indium-v8--description '((type . "boolean") (value . t))) - :to-equal "true") - (expect (indium-v8--description '((type . "boolean") (value . :json-false))) - :to-equal "false"))) - -(describe "V8 backend object preview" - (it "can render array previews with booleans (GitHub issue #52)" - (expect (indium-v8--preview '((type . "object") - (subtype . "array") - (className . "Array") - (description . "Array[1]") - (objectId . "{\"injectedScriptId\":12,\"id\":38}") - (preview (type . "object") - (subtype . "array") - (description . "Array[1]") - (overflow . :json-false) - (properties . [((name . "0") (type . "boolean") (value . "true"))])))) - :to-equal "[ true ]")) - - (it "can render object previews with booleans (GitHub issue #52)" - (expect (indium-v8--preview '((type . "object") - (className . "Object") - (description . "Object") - (objectId . "{\"injectedScriptId\":12,\"id\":43}") - (preview (type . "object") - (description . "Object") - (overflow . :json-false) - (properties . [((name . "a") (type . "boolean") (value . "true"))])))) - :to-equal "{ a: true }"))) - -(describe "Location conversion" - (it "can convert a location struct into a v8 location" - (let ((location (indium-location-create :line 10 :column 5 :file "/foo/bar.js"))) - (spy-on #'indium-script-find-from-location :and-return-value nil) - (expect (indium-v8--convert-to-v8-location location) - :to-equal '((columnNumber . 5) - (lineNumber . 10))))) - - (it "can convert a location struct with file into a v8 location" - (with-fake-indium-connection - (let ((location (indium-location-create :line 10 :column 5 :file "foo"))) - (spy-on #'indium-location-url :and-return-value "foo") - (spy-on #'indium-script-find-from-location :and-return-value (indium-script-create :id "1")) - (expect (indium-v8--convert-to-v8-location location) - :to-equal '((scriptId . "1") - (columnNumber . 5) - (lineNumber . 10))))))) - -(provide 'indium-v8-test) -;;; indium-v8-test.el ends here diff --git a/test/unit/indium-workspace-test.el b/test/unit/indium-workspace-test.el deleted file mode 100644 index 0e9d627..0000000 --- a/test/unit/indium-workspace-test.el +++ /dev/null @@ -1,190 +0,0 @@ -;;; indium-workspace-test.el --- Tests for indium-workspace.el -*- lexical-binding: t; -*- - -;; Copyright (C) 2017-2018 Nicolas Petton - -;; Author: Nicolas Petton - -;; 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 . - -;;; Commentary: - -;;; Code: - -(require 'map) - -(require 'buttercup) -(require 'assess) -(require 'indium-workspace) - -(defvar indium-workspace--test-fs - '((".indium.json" "{\"configurations\": [{}]}") - ("js" ("app.js"))) - "Fake filesystem used in workspace tests.") - -(describe "Workspace root" - (after-each - (setq indium-workspace-configuration nil)) - - (it "Returns the current connection's project root when there is a connection" - (assess-with-filesystem indium-workspace--test-fs - (let* ((root (expand-file-name "js")) - (indium-current-connection (indium-connection-create :project-root root))) - (expect (indium-workspace-root) :to-be root)))) - - (it "should default to the project directory when no \"root\" is defined" - (assess-with-filesystem indium-workspace--test-fs - (expect (directory-file-name (indium-workspace-root)) :to-equal - (directory-file-name default-directory)))) - - (it "should take the directory set in the \"root\" option" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"root\": \"foo\"}]}") - ("foo" ("index.js"))) - (with-indium-workspace-configuration - (expect (directory-file-name (indium-workspace-root)) :to-equal - (directory-file-name (expand-file-name "foo" default-directory)))))) - - (it "webRoot should be an alias for root" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"webRoot\": \"foo\"}]}") - ("foo" ("index.js"))) - (with-indium-workspace-configuration - (expect (directory-file-name (indium-workspace-root)) :to-equal - (directory-file-name (expand-file-name "foo" default-directory))))))) - -(describe "Invalid root directory" - (after-each - (setq indium-workspace-configuration nil)) - - (it "should signal an error when the root directory does not exist" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"webRoot\": \"foo\"}]}")) - (with-indium-workspace-configuration - (expect (indium-workspace-root) :to-throw))))) - -(describe "Choosing a configuration" - (after-each - (setq indium-workspace-configuration nil)) - - (it "should not prompt for a configuration when there is only one" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{}]}")) - (spy-on #'completing-read) - (indium-workspace-read-configuration) - (expect #'completing-read :not :to-have-been-called))) - - (it "should prompt for a configuration when there are many" - (assess-with-filesystem '((".indium.json" "{\"configurations\": [{\"name\": \"a\"}, {\"name\": \"b\"}]}")) - (spy-on #'completing-read) - (indium-workspace-read-configuration) - (expect #'completing-read :to-have-been-called-with "Choose a configuration: " '("a" "b") nil t)))) - -(describe "Looking up files" - (it "cannot lookup file when no workspace it set" - (expect (indium-workspace-lookup-file "http://localhost:9229/foo/bar") - :to-throw)) - - (it "can lookup file with an empty .indium.json marker file" - (assess-with-filesystem indium-workspace--test-fs - (expect (indium-workspace-lookup-file "http://localhost:9229/js/app.js") - :to-equal (expand-file-name "js/app.js")))) - - (it "should ignore query strings from urls when looking up files" - (assess-with-filesystem indium-workspace--test-fs - (expect (indium-workspace-lookup-file "http://localhost:9229/js/app.js?foo=bar") - :to-equal (expand-file-name "js/app.js")))) - - (it "cannot find a file that does not exist" - (assess-with-filesystem indium-workspace--test-fs - (expect (indium-workspace-lookup-file "http://localhost:9229/non-existant-file-name.js") - :to-be nil)))) - -(describe "Looking up files safely" - (it "should fallback to the url when no file can be found" - (assess-with-filesystem indium-workspace--test-fs - (let ((url "http://localhost:9229/non-existant-file-name.js")) - (expect (indium-workspace-lookup-file-safe url) - :to-equal url)))) - - (it "can lookup files that exist" - (assess-with-filesystem indium-workspace--test-fs - (let ((url "http://localhost:9229/js/app.js") - (file (expand-file-name "js/app.js"))) - (expect (indium-workspace-lookup-file-safe url) - :to-equal file))))) - -(describe "Making workspace urls from file names" - (after-each - (setq indium-workspace-configuration nil)) - - (it "cannot make a url when no workspace is set" - (with-indium-connection (indium-connection-create :url "http://localhost:9229") - (expect (indium-workspace-make-url "js/app.js") - :to-throw))) - - (it "can make workspace urls" - (with-indium-connection (indium-connection-create :url "http://localhost:9229") - (assess-with-filesystem indium-workspace--test-fs - (expect (indium-workspace-make-url "js/app.js") - :to-equal "http://localhost:9229/js/app.js")))) - - (it "should strip query strings from computing urls" - (with-indium-connection (indium-connection-create :url "http://localhost:9229?foo=bar") - (assess-with-filesystem indium-workspace--test-fs - (expect (indium-workspace-make-url "js/app.js") - :to-equal "http://localhost:9229/js/app.js")))) - - (it "should strip paths based on the .indium marker when computing urls" - (with-indium-connection (indium-connection-create :url "http://localhost:9229/foo/bar") - (assess-with-filesystem indium-workspace--test-fs - (expect (indium-workspace-make-url "js/app.js") - :to-equal "http://localhost:9229/js/app.js")))) - - (it "should use the file path if the connection uses nodejs when computing urls" - (with-indium-connection (indium-connection-create) - (map-put (indium-current-connection-props) - 'nodejs t) - (assess-with-filesystem indium-workspace--test-fs - (let ((file (expand-file-name "js/app.js"))) - (expect (indium-workspace-make-url file) - :to-equal (expand-file-name "js/app.js")))))) - - ;; Regression test for GitHub issue #144 - (it "should use Windows file paths file path with nodejs on Windows" - (with-indium-connection (indium-connection-create) - (map-put (indium-current-connection-props) - 'nodejs t) - (spy-on #'convert-standard-filename :and-call-through) - (assess-with-filesystem indium-workspace--test-fs - (let ((file (expand-file-name "js/app.js"))) - (indium-workspace-make-url file) - (expect #'convert-standard-filename :to-have-been-called-with file)))))) - -(describe "File protocol" - (after-each - (setq indium-workspace-configuration nil)) - - (it "can lookup files using the file:// protocol" - (assess-with-filesystem indium-workspace--test-fs - (with-indium-connection (indium-connection-create :url "file:///foo/bar/index.html") - (let* ((file (expand-file-name "js/app.js")) - (url (format "file://%s" file))) - (expect (indium-workspace-lookup-file url) - :to-equal file))))) - - (it "can make a url when using the file protocol" - (assess-with-filesystem indium-workspace--test-fs - (with-indium-connection (indium-connection-create :url "file:///foo/bar/index.html") - (let* ((file (expand-file-name "js/app.js"))) - (expect (indium-workspace-make-url file) - :to-equal (format "file://%s" file))))))) - -(provide 'indium-workspace-test) -;;; indium-workspace-test.el ends here diff --git a/test/unit/wsc-test.el b/test/unit/wsc-test.el deleted file mode 100644 index 43502d8..0000000 --- a/test/unit/wsc-test.el +++ /dev/null @@ -1,158 +0,0 @@ -;;; wsc-test.el --- Tests for wsc.el - -;; Copyright (C) 2018 Nicolas Petton - -;; Author: Nicolas Petton - -;; This file is not part of GNU Emacs. - -;; 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 . - -;;; Commentary: - -;; Tests for wsc.el - -;;; Code: - -(require 'buttercup) -(require 'wsc) - -(describe "WSC" - - (describe "Client key" - (it "should generate a unique key" - (dotimes (_ 100) - (expect (wsc--make-key) :not :to-equal (wsc--make-key)))) - - (it "has a different client key for each connection" - (dotimes (_ 100) - (expect (wsc-connection-key (wsc-connection-create)) :not :to-equal - (wsc-connection-key (wsc-connection-create)))))) - - (describe "Connection state" - (it "should be in :connecting state by default" - (expect (wsc-connection-state (wsc-connection-create)) :to-be :connecting))) - - (describe "Opening TCP connections" - (it "should not use TLS protocol when over ws://" - (let ((url "ws://localhost:8889")) - (spy-on 'open-network-stream) - (spy-on 'wsc--create-data-buffer :and-return-value 'buffer) - (wsc--make-process (url-generic-parse-url url)) - (expect #'open-network-stream :to-have-been-called-with - "wsc process" - 'buffer - "localhost" - 8889 - :type 'plain))) - - (it "should use TLS when over wss://" - (let ((url "wss://localhost:8889")) - (spy-on 'open-network-stream) - (spy-on 'wsc--create-data-buffer :and-return-value 'buffer) - (wsc--make-process (url-generic-parse-url url)) - (expect #'open-network-stream :to-have-been-called-with - "wsc process" - 'buffer - "localhost" - 8889 - :type 'tls)))) - - (describe "Process buffer" - (it "should not use multibyte" - (with-current-buffer (wsc--create-data-buffer) - (expect enable-multibyte-characters :to-be nil))) - - (it "should generate unique process buffer names" - (let ((buf1 (wsc--create-data-buffer)) - (buf2 (wsc--create-data-buffer))) - (expect (buffer-name buf1) :not :to-equal (buffer-name buf2))))) - - (describe "Error handling" - (it "should signal an error" - (let ((conn (wsc-connection-create)) - (wsc--current-process 'process)) - (spy-on 'process-get :and-return-value conn) - (spy-on 'kill-process) - (expect (wsc--error-and-close "Oops") :to-throw))) - - (it "should close the connection when signaling an error" - (let ((conn (wsc-connection-create :url 'foobar)) - (wsc--current-process 'process)) - (spy-on 'process-get :and-return-value conn) - (spy-on 'kill-process) - (expect (wsc-connection-state conn) :to-be :connecting) - (ignore-errors - (wsc--error-and-close "Oops")) - (expect (wsc-connection-state conn) :to-be :closed))) - - (it "should kill the process when signaling an error" - (let ((conn (wsc-connection-create)) - (wsc--current-process 'process)) - (spy-on 'process-get :and-return-value conn) - (spy-on 'kill-process) - (ignore-errors - (wsc--error-and-close "Oops")) - (expect #'kill-process :to-have-been-called-with 'process)))) - - (describe "Handshake" - (it "should signal an error if the status code is not 101" - (spy-on 'wsc--error-and-close) - (with-temp-buffer - (insert "HTTP/1.1 500 - -") - (expect (wsc--handle-output :connecting) :to-throw))) - - (it "should signal an error when the accept value is wrong" - (let ((conn (wsc-connection-create))) - (spy-on 'wsc--current-connection :and-return-value conn) - (with-temp-buffer - (insert "HTTP/1.1 101 Yay -Sec-WebSocket-Accept: foobar - -") - (expect (wsc--handle-output :connecting) :to-throw)))) - - (it "should succeed if the accept value is correct" - (let ((conn (wsc-connection-create))) - (spy-on 'wsc--current-connection :and-return-value conn) - (with-temp-buffer - (insert (format "HTTP/1.1 101 Yay -Sec-WebSocket-Accept: %s - -" (wsc--accept-value conn))) - (expect (wsc--handle-output :connecting) :not :to-throw) - (expect (wsc-connection-state conn) :to-be :open))))) - - (describe "Masking data" - (it "should return a different string when masking" - (expect (seq-into (wsc--mask "hello world" (wsc--make-masking-key)) 'string) - :not :to-equal "hello world")) - - (it "should not modify the data if the key is null" - (expect (seq-into (wsc--mask "hello world" [0 0 0 0]) 'string) - :to-equal "hello world")) - - (it "should produce masked data of the same length" - (expect (length (wsc--mask "hello world" (wsc--make-masking-key))) - :to-be (length "hello world"))) - - (it "should get back the original data when unmasking" - (let ((key (wsc--make-masking-key))) - (expect (seq-into (wsc--mask (wsc--mask "hello world" key) key) 'string) - :to-equal "hello world"))))) - -(provide 'wsc-test) -;;; wsc-test.el ends here diff --git a/wsc.el b/wsc.el deleted file mode 100644 index aaf2087..0000000 --- a/wsc.el +++ /dev/null @@ -1,482 +0,0 @@ -;;; wsc.el --- WebSocket client -*- lexical-binding: t; -*- - -;; Copyright (C) 2018 Nicolas Petton - -;; Author: Nicolas Petton -;; Keywords: network - -;; 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 . - -;;; Commentary: - -;; WebSocket client built with fast data reception in mind. It implements -;; RFC6455 (https://tools.ietf.org/html/rfc6455). -;; -;; There is currently no support for: -;; - fragmented frames -;; - sending or receiving binary data -;; - protocol extensions - -;;; Code: - - -(require 'seq) -(require 'url-parse) -(require 'subr-x) - -(eval-when-compile (require 'cl-lib)) - -(defconst wsc-protocol-version 13 - "Version of the WebSocket protocol used.") - -(defconst wsc-guid "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - "The WebSocket GUID from the RFC6455.") - -(defvar wsc--current-process nil - "Current WebSocket process.") - -(defvar wsc--handshake-template (concat "GET %s HTTP/1.1\r\n" - "Host: %s\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Key: %s\r\n" - "Sec-WebSocket-Version: %s\r\n\r\n") - "Template string used for sending handshake requests.") - -(defvar wsc-debug nil - "When non-nil, output all frames to the *wsc-dbg* buffer.") - -(cl-defstruct (wsc-connection (:constructor wsc-connection-create) - (:copier nil)) - (state :connecting) - (url nil) - (process nil) - (key (wsc--make-key)) - (on-message nil) - (on-open nil) - (on-close nil) - (next-callbacks nil)) - -(defun wsc-connection-open-p (connection) - "Return non-nil if CONNECTION's state is open." - (eq (wsc-connection-state connection) :open)) - - -(defun wsc-open (url &optional on-open on-message on-close) - "Open a new WebSocket connection at URL and return it. -If non-nil, evaluate ON-OPEN when the connection is established. -If non-nil, evaluate ON-MESSAGE when a message is received. -If non-nil, evaluate ON-CLOSE when a the connection is closed." - (let* ((parsed-url (url-generic-parse-url url)) - (process (wsc--make-process parsed-url)) - (connection (wsc-connection-create - :process process - :url parsed-url - :on-open on-open - :on-message on-message - :on-close on-close))) - (process-put process :wsc-connection connection) - (set-process-filter process #'wsc--process-filter) - (set-process-coding-system process 'no-conversion) - (set-process-query-on-exit-flag process nil) - (set-process-sentinel process #'wsc--process-sentinel) - (wsc--open-handshake connection) - connection)) - -(defun wsc-close (connection) - "Close CONNECTION." - (unless (wsc-connection-open-p connection) - (error "Connection not open, cannot send data")) - (let ((input-buffer (wsc--create-data-buffer))) - (unwind-protect - (with-current-buffer input-buffer - (let ((wsc--current-process (wsc-connection-process connection))) - ;; The process might not be running, so ignore errors - (ignore-errors - (wsc--send-frame 8)))) - (kill-buffer input-buffer)))) - -(defun wsc-send (connection data) - "Send DATA to the WebSocket process associated to CONNECTION." - (unless (wsc-connection-open-p connection) - (error "Connection not open, cannot send data")) - (when wsc-debug - (with-current-buffer (get-buffer-create "*wsc-dbg*") - (goto-char (point-max)) - (insert (format "\n\nSENDING FRAME\n---\n%s\n---" data)))) - (let ((wsc--current-process (wsc-connection-process connection))) - (wsc--send-frame 1 (encode-coding-string data 'no-conversion)))) - - -(defun wsc--make-process (url) - "Open a TCP connection to URL, and return the process object." - (let* ((use-tls (string= (url-type url) "wss")) - (type (if use-tls 'tls 'plain)) - (host (url-host url)) - (port (if (zerop (url-port url)) - (if use-tls 443 80) - (url-port url))) - (name "wsc process") - (buf (wsc--create-data-buffer))) - (open-network-stream name buf host port :type type))) - -(defun wsc--process-sentinel (process _) - "Handle PROCESS closed." - (when (member (process-status process) '(closed failed exit signal)) - (when-let ((connection (wsc--current-connection)) - (handler (wsc-connection-on-close connection))) - (funcall handler connection)))) - -(defun wsc--process-filter (process output) - "Filter function for WebSocket network PROCESS. - -OUTPUT is always appended to the PROCESS buffer." - (when wsc-debug - (with-current-buffer (get-buffer-create "*wsc-dbg*") - (goto-char (point-max)) - (insert "\n\n" output))) - (with-current-buffer (process-buffer process) - (goto-char (point-max)) - (insert output) - (let* ((wsc--current-process process) - (state (wsc-connection-state (wsc--current-connection)))) - (goto-char (point-min)) - (wsc--handle-output state))) - ;; Do not call on-message, etc. callbacks within the process buffer, as they - ;; could manipulate that buffer by mistake. - (wsc--call-next-callbacks (process-get process :wsc-connection))) - -(defun wsc--call-next-callbacks (connection) - "Call all registede frame callbacks for CONNECTION. - -When frames are handled, callbacks are installed to be triggered -after all frame handling have completed." - (when-let ((callbacks (wsc-connection-next-callbacks connection))) - (unwind-protect - (dolist (callback callbacks) - (funcall callback)) - (setf (wsc-connection-next-callbacks connection) nil)))) - -(defun wsc--handle-output (state) - "Handle new output based on the STATE of a connection. - -Each time a full message is received and processed (either a -complete frame or handshake response), its frames are removed -from the process buffer." - (pcase state - (:connecting (wsc--handle-handshake)) - (:open (wsc--handle-data)) - (:closed (error "WebSocket closed")))) - -(defun wsc--handle-handshake () - "Validate the handshake server response. - -Wait for more data and do nothing if the handshake response is -not complete. - -Once the full handshake response has been outputed and validated, -erase it from the process buffer." - (when (wsc--handshake-response-complete-p) - (wsc--validate-handshake-status-code) - (wsc--validate-handshake-accept-value) - (setf (wsc-connection-state (wsc--current-connection)) :open) - (erase-buffer) - (when-let ((connection (wsc--current-connection)) - (handler (wsc-connection-on-open connection))) - (push (apply-partially handler (wsc--current-connection)) - (wsc-connection-next-callbacks connection))))) - -(defun wsc--handle-data () - "Handle incoming data in the process buffer. - -Read data frame by frame as long as frames can be read from the -process buffer." - (save-excursion - (let ((headers (wsc--read-frame-headers))) - (while headers - (apply #'wsc--handle-frame headers) - (setq headers (wsc--read-frame-headers)))))) - -(defun wsc--handle-frame (final opcode length) - "Handle a new frame output based on the value of OPCODE. -FINAL is non-nil if the message contains no more frame fragments. -LENGTH is the length of the payload in bytes. - -List of OPCODEs based on the RFC: - - 0 Continuation frame - 1 Text data frame - 2 Binary data frame - 3-7 Reserved (unused) - 8 Connection close - 9 Ping - 10 Pong - 11-15 Reserved (unused) - -If the frame is processed, delete its data from the process -buffer." - (let ((payload (wsc--read-frame-payload length))) - (unless (null final) - (delete-region (point-min) (point)) - (pcase opcode - (0 (wsc--handle-continuation-frame payload)) - (1 (wsc--handle-text-frame payload)) - (8 (wsc--handle-close-frame)) - (9 (wsc--handle-ping-frame)) - (10 (wsc--handle-pong-frame)) - (_ (wsc--error-and-close - (format "Unsupported frame with opcode %s" opcode))))))) - -(defun wsc--handle-close-frame () - "Handle a connnection close frame." - (wsc--ensure-closed)) - -(defun wsc--handle-text-frame (payload) - "Handle an unfragmented text data frame with PAYLOAD." - (when-let ((connection (wsc--current-connection)) - (handler (wsc-connection-on-message connection))) - (push (apply-partially handler payload) - (wsc-connection-next-callbacks connection)))) - -(defun wsc--handle-continuation-frame (_payload) - "Handle a final continuation frame." - (when-let ((handler (wsc-connection-on-message - (wsc--current-connection)))) - (wsc--read-fragmented-frame-payloads))) - -(defun wsc--handle-ping-frame () - "Handle a ping frame by sending a pong frame." - (wsc--send-frame 10)) - -(defun wsc--handle-pong-frame () - "Handle a pong frame." - ;; NO-OP - ) - - -;;; Handshake handling - -(defun wsc--open-handshake (connection) - "Open a handshake for CONNECTION." - (let* ((process (wsc-connection-process connection)) - (url (wsc-connection-url connection)) - (absolute-path (if (string-empty-p (url-filename url)) - "/" - (url-filename url))) - (host-and-port (if (url-port-if-non-default url) - (format "%s:%s" (url-host url) (url-port url)) - (url-host url)))) - (process-send-string process (format wsc--handshake-template - absolute-path - host-and-port - (wsc-connection-key connection) - wsc-protocol-version)))) - -(defun wsc--close-handshake (_connection) - "Send a closing frame for CONNECTION.") - -(defun wsc--handshake-response-complete-p () - "Return non-nil if the current buffer has all handshake headers." - (save-excursion - (save-match-data - (goto-char (point-max)) - (search-backward "\r\n\r\n" nil t)))) - -(defun wsc--validate-handshake-status-code () - "Check the status code in the server response. -If the status code is invalid, close the connection and signal an error." - (save-excursion - (save-match-data - (goto-char (point-min)) - (unless (search-forward "HTTP/1.1 101" nil t) - (wsc--error-and-close "Invalid status code from server"))))) - -(defun wsc--validate-handshake-accept-value () - "Check the accept value from the server based on the connection key. -If the accept value is invalid, close the connection and signal an error." - (save-excursion - (save-match-data - (goto-char (point-min)) - (let ((accept (wsc--accept-value (wsc--current-connection)))) - (unless (search-forward (format "Sec-WebSocket-Accept: %s" accept) nil t) - (wsc--error-and-close "Invalid websocket accept key from server")))))) - -;;; Sending/Reading frames - -(defun wsc--send-data (data) - "Send DATA as WebSocket frames to the current WebSocket process." - (wsc--send-frame 1 data)) - -(defun wsc--send-frame (opcode &optional payload) - "Send a frame with OPCODE and PAYLOAD." - (let ((input-buffer (wsc--create-data-buffer)) - (mask-key (wsc--make-masking-key))) - (unwind-protect - (with-current-buffer input-buffer - (insert (logior #b10000000 ;; FIN bit - #b00000000 ;; RSV1 (ignored, always 0) - #b00000000 ;; RSV2 (ignored, always 0) - #b00000000 ;; RSV3 (ignored, always 0) - opcode ;; OPCODE 4bits - )) - (wsc--insert-payload-length (length payload)) - (insert mask-key) - (apply #'insert (wsc--mask payload mask-key)) - (process-send-region wsc--current-process (point-min) (point-max))) - (kill-buffer input-buffer)))) - -(defun wsc--mask (payload key) - "Return a list of bytes masking PAYLOAD using masking KEY." - (seq-map-indexed (lambda (byte index) - (logxor byte (elt key (mod index 4)))) - payload)) - -(defun wsc--insert-payload-length (length) - "Insert the LENGTH of the payload in the current buffer. - -The first bit is the masking bit. - -The next 7 bits are the payload length if 0-125. - -If 126, the following 2 bytes are interpreted as a 16-bit -unsigned integer are the payload length. - -If 127, the following 8 bytes are interpreted as a 64-bit -unsigned integer." - ;; Masking bit + 7 first bits of length - (insert (logior #b10000000 ;; Masking bit - (cond ((< length 126) length) - ((< length 65536) 126) - (t 127)))) - (when (> length 125) - (if (< length 65536) - ;; Extended length on 2 bytes - (progn - (insert (logand (lsh length -8) #b11111111)) - (insert (logand length #b11111111))) - ;; Extended length on 8 bytes - (progn - (insert (logand (lsh length -56) #b11111111)) - (insert (logand (lsh length -48) #b11111111)) - (insert (logand (lsh length -40) #b11111111)) - (insert (logand (lsh length -32) #b11111111)) - (insert (logand (lsh length -24) #b11111111)) - (insert (logand (lsh length -16) #b11111111)) - (insert (logand (lsh length -8) #b11111111)) - (insert (logand length #b11111111)))))) - -(defun wsc--read-frame-headers () - "Return a list of headers for ther frame at point. - -The returned list contains: - - - non-nil if the frame is final, nil otherwise; - - the frame opcode; - - the length of the frame payload. - -The point is moved forward to the beginning of the frame payload. - -If there is no data or if the frame is incomplete (because all -the frame data has not yet been received), return nil." - (unless (eobp) - (let* ((byte (char-after)) - (final (not (zerop (logand #b10000000 byte)))) - (opcode (logand #b00001111 byte)) - (length (wsc--read-frame-payload-length))) - ;; Check if the frame is complete - (when (>= (point-max) (+ (point) length)) - (list final opcode length))))) - -(defun wsc--read-frame-payload-length () - "Return the payload length from the frame in the current buffer. - -The point is moved forward as the payload length is being read -from the buffer." - ;; Skip from FIN bit up to opcode - (forward-char) - (let ((initial-length (logand #b01111111 ;; Masking bit - (char-after)))) ;; 7 bits of initial length - (forward-char) - (if (<= initial-length 125) - initial-length - (let ((nbytes (if (= initial-length 126) 2 8)) - (length 0)) - (dotimes (i nbytes) - (cl-incf length (lsh (char-after) (* 8 (- nbytes i 1)))) - (forward-char)) - length)))) - -(defun wsc--read-frame-payload (length) - "Return the payload of LENGTH bytes of the frame at point. - -The point is moved forward to the end of the frame payload." - (let* ((start (point)) - (end (+ start length))) - (goto-char end) - (decode-coding-region start end 'utf-8 t))) - -(defun wsc--read-fragmented-frame-payloads () - "Not yet implemented." - (error "Fragmented frames are not yet supported")) - - -;;; Utilities - -(defun wsc--create-data-buffer () - "Return a new buffer to receive data." - (let ((buf (generate-new-buffer " wsc "))) - (with-current-buffer buf - (set-buffer-multibyte nil)) - buf)) - -(defun wsc--make-key () - "Generate a base64-encoded 16 bytes long connection key." - (base64-encode-string - (wsc--generate-bytes 16))) - -(defun wsc--make-masking-key () - "Generate a 4 bytes long masking key." - (wsc--generate-bytes 4)) - -(defun wsc--generate-bytes (n) - "Return a string of N bytes." - (seq-into (seq-map (lambda (_) - (random 256)) - (make-vector n 0)) - 'string)) - -(defun wsc--accept-value (connection) - "Return the computed accept value for CONNECTION based on its key." - (base64-encode-string - (sha1 (format "%s%s" (wsc-connection-key connection) wsc-guid) - nil nil t))) - -(defun wsc--error-and-close (reason) - "Sinal an error with REASON and close the websocket process." - (wsc--ensure-closed) - (error reason)) - -(defun wsc--ensure-closed () - "Ensure the TCP process is closed and set the connection state." - (setf (wsc-connection-state (wsc--current-connection)) :closed) - (ignore-errors - (kill-process wsc--current-process))) - -(defun wsc--current-connection () - "Return the connection associated with the current WebSocket process." - (when-let ((process wsc--current-process)) - (process-get process :wsc-connection))) - -(provide 'wsc) -;;; wsc.el ends here