A JavaScript development environment for Emacs https://indium.readthedocs.io
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

434 lines
16 KiB

;;; indium-interaction.el --- Interaction functions for indium.el -*- lexical-binding: t; -*-
;; Copyright (C) 2016-2018 Nicolas Petton
;; Author: Nicolas Petton <nicolas@petton.fr>
;; Keywords: javascript, tools
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Minor mode for interacting with a JavaScript runtime. This mode provides
;; commands connecting to a runtime, managing breakpoints and evaluating code.
;;; Code:
(require 'js2-mode)
(require 'map)
(require 'seq)
(require 'subr-x)
(require 'xref)
(require 'easymenu)
(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-launch-chrome "indium-chrome.el")
(declare-function indium-launch-nodejs "indium-nodejs.el")
;;;###autoload
(defun indium-connect ()
"Open a new connection to a runtime."
(interactive)
(indium-maybe-quit)
(unless (indium-client-process-live-p)
(let ((dir (indium-interaction--current-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 new process and connect to it."
(interactive)
(indium-maybe-quit)
(unless (indium-client-process-live-p)
(let ((dir (indium-interaction--current-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."
(interactive)
(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."
(interactive)
(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)
"Evaluate STRING on the current backend.
When CALLBACK is non-nil, evaluate CALLBACK with the result.
When called interactively, prompt the user for the string to be
evaluated.
Evaluation happens in the context of the current debugger frame if any."
(interactive "sEvaluate JavaScript: ")
(indium-client-evaluate string indium-debugger-current-frame callback))
(defun indium-eval-buffer ()
"Evaluate the accessible portion of current buffer."
(interactive)
(indium-eval (buffer-string)
#'indium-interaction--handle-eval-result))
(defun indium-eval-region (start end)
"Evaluate the region between START and END."
(interactive "r")
(indium-eval (buffer-substring-no-properties start end)
#'indium-interaction--handle-eval-result))
(defun indium-eval-last-node (arg)
"Evaluate the node before point; print in the echo area.
This is similar to `eval-last-sexp', but for JavaScript buffers.
Interactively, with a prefix argument ARG, print the output into
the current buffer."
(interactive "P")
(indium-interaction--eval-node (indium-interaction-node-before-point) arg))
(defun indium-eval-defun ()
"Evaluate the innermost function enclosing the current point."
(interactive)
(if-let ((node (js2-mode-function-at-point)))
(indium-interaction--eval-node node)
(user-error "No function at point")))
(defun indium-switch-to-debugger ()
"Switch to the buffer containing the Indium debugger.
The point is moved to the top stack frame.
If there is no debugging session, signal an error."
(interactive)
(indium-debugger-switch-to-debugger-buffer))
(defvar indium-interaction-eval-hook nil
"Hooks to run after evaluating node before the point.")
(add-hook 'indium-interaction-eval-hook #'indium-message)
(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."
(js2-mode-wait-for-parse
(lambda ()
(indium-eval (js2-node-string node)
(lambda (value)
(indium-interaction--handle-eval-result
value
print))))))
(defun indium-interaction--handle-eval-result (value &optional print)
"Handle VALUE is the result of an evaluation.
The default behavior is to print it in the echo area.
If PRINT in non-nil, insert it in the current buffer instead."
(let ((description (indium-render-remote-object-to-string value)))
(if print
(save-excursion
(insert description))
(run-hook-with-args 'indium-interaction-eval-hook description))))
(defun indium-reload ()
"Reload the page."
(interactive)
(indium-client-evaluate "window.location.reload()"))
(defun indium-inspect-last-node ()
"Evaluate and inspect the node before point."
(interactive)
(js2-mode-wait-for-parse
(lambda ()
(indium-inspect-expression
(js2-node-string (indium-interaction-node-before-point))))))
(defun indium-inspect-expression (expression)
"Prompt for EXPRESSION to be inspected."
(interactive "sInspect expression: ")
(indium-eval expression
(lambda (result)
(indium-inspector-inspect result))))
(defun indium-switch-to-repl-buffer ()
"Switch to the repl buffer if any."
(interactive)
(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."
(interactive)
(if (indium-breakpoint-on-current-line-p)
(call-interactively #'indium-remove-breakpoint)
(call-interactively #'indium-add-breakpoint)))
(defun indium-mouse-toggle-breakpoint (event)
"Toggle breakpoint at mouse EVENT click point."
(interactive "e")
(let* ((posn (event-end event))
(pos (posn-point posn)))
(when (numberp pos)
(with-current-buffer (window-buffer (posn-window posn))
(save-excursion
(goto-char pos)
(call-interactively #'indium-toggle-breakpoint))))))
(defun indium-add-breakpoint (&optional condition)
"Add a breakpoint on the current line.
If there is already a breakpoint, signal an error.
When CONDITION is non-nil, add a conditional breakpoint with
CONDITION."
(interactive)
(indium-interaction--guard-no-breakpoint-at-point)
(save-excursion
(beginning-of-line)
(indium-breakpoint-add condition)))
(defun indium-add-conditional-breakpoint (condition)
"Add a breakpoint with CONDITION at point.
If there is already a breakpoint, signal an error."
(interactive "sBreakpoint condition: ")
(indium-add-breakpoint condition))
(defun indium-edit-breakpoint-condition ()
"Edit the condition of breakpoint at point.
Signal an error if there is no breakpoint."
(interactive)
(indium-interaction--guard-breakpoint-at-point)
(indium-breakpoint-edit-condition))
(defun indium-remove-breakpoint ()
"Remove the breakpoint at point.
If there is no breakpoint, signal an error."
(interactive)
(indium-interaction--guard-breakpoint-at-point)
(indium-breakpoint-remove))
(defun indium-remove-all-breakpoints-from-buffer ()
"Remove all breakpoints from the current buffer."
(interactive)
(indium-breakpoint-remove-breakpoints-from-current-buffer))
(defun indium-deactivate-breakpoints ()
"Deactivate all breakpoints in all buffers.
Breakpoints are not removed, but the runtime won't pause when
hitting a breakpoint."
(interactive)
(indium-client-deactivate-breakpoints)
(message "Breakpoints deactivated"))
(defun indium-activate-breakpoints ()
"Activate all breakpoints in all buffers."
(interactive)
(indium-client-activate-breakpoints)
(message "Breakpoints activated"))
(defun indium-list-breakpoints ()
"List all breakpoints in the current connection."
(interactive)
(if-let ((xrefs (indium--make-xrefs-from-breakpoints)))
(xref--show-xrefs (lambda () xrefs) nil)
(message "No breakpoint")))
(defun indium--make-xrefs-from-breakpoints ()
"Return a list of xref objects from all breakpoints."
(map-apply (lambda (breakpoint buffer)
(let ((line (with-current-buffer buffer
(line-number-at-pos
(overlay-start
(indium-breakpoint-overlay breakpoint))))))
(xref-make (indium--get-breakpoint-xref-match breakpoint buffer)
(xref-make-file-location (buffer-file-name buffer)
line
0))))
indium-breakpoint--local-breakpoints))
(defun indium--get-breakpoint-xref-match (breakpoint buffer)
"Return the source line where BREAKPOINT is set in BUFFER."
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(forward-line (1- (line-number-at-pos
(overlay-start
(indium-breakpoint-overlay breakpoint)))))
(buffer-substring (point-at-bol) (point-at-eol)))))
(defun indium-interaction-node-before-point ()
"Return the node before point to be evaluated."
(save-excursion
(forward-comment -1)
(while (looking-back "[:,]" nil)
(backward-char 1))
(backward-char 1)
(while (js2-empty-expr-node-p (js2-node-at-point))
(backward-char 1))
(let* ((node (js2-node-at-point))
(parent (js2-node-parent node)))
;; Heuristics for finding the node to evaluate: if the parent of the node
;; before point is a prop-get node (i.e. foo.bar) and if it starts before
;; the current node, meaning that the point is on the node following the
;; parent, then return the parent node:
;;
;; (underscore represents the point)
;; foo.ba_r // => evaluate foo.bar
;; foo_.bar // => evaluate foo
;; foo.bar.baz_() // => evaluate foo.bar.baz
;; foo.bar.baz()_ // => evaluate foo.bar.baz()
;;
;; If the node is a "block node" (i.e. the `{...}' part of a function
;; declaration, also return the parent node.
(while (or (and (js2-prop-get-node-p parent)
(< (js2-node-abs-pos parent)
(js2-node-abs-pos node)))
(and (not (js2-function-node-p node))
(not (js2-loop-node-p node))
(js2-block-node-p node)))
(setq node parent))
node)))
(defvar indium-interaction-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-x C-e") #'indium-eval-last-node)
(define-key map (kbd "C-M-x") #'indium-eval-defun)
(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 [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)
(define-key map (kbd "C-c b b") #'indium-add-breakpoint)
(define-key map (kbd "C-c b c") #'indium-add-conditional-breakpoint)
(define-key map (kbd "C-c b e") #'indium-edit-breakpoint-condition)
(define-key map (kbd "C-c b k") #'indium-remove-breakpoint)
(define-key map (kbd "C-c b K") #'indium-remove-all-breakpoints-from-buffer)
(define-key map (kbd "C-c b a") #'indium-activate-breakpoints)
(define-key map (kbd "C-c b d") #'indium-deactivate-breakpoints)
(define-key map (kbd "C-c b l") #'indium-list-breakpoints)
(define-key map (kbd "C-c d") #'indium-switch-to-debugger)
(easy-menu-define indium-interaction-mode-menu map
"Menu for Indium interaction mode"
'("Indium interaction"
["Switch to REPL" indium-switch-to-repl-buffer]
"--"
("Evaluation"
["Evaluate last node" indium-eval-last-node]
["Inspect last node" indium-inspect-last-node]
["Inspect expression" indium-inspect-expression]
["Evaluate function" indium-eval-defun])
"--"
("Breakpoints"
["Add breakpoint" indium-add-breakpoint]
["Add conditional breakpoint" indium-add-conditional-breakpoint]
["Remove breakpoint" indium-remove-breakpoint]
["Remove all breakpoints" indium-remove-all-breakpoints-from-buffer]
["Deactivate breakpoints" indium-deactivate-breakpoints]
["Activate breakpoints" indium-activate-breakpoints]
["List all breakpoints" indium-list-breakpoints])))
map))
(define-minor-mode indium-interaction-mode
"Mode for JavaScript evaluation.
\\{indium-interaction-mode-map}"
:lighter " js-interaction"
:keymap indium-interaction-mode-map
(unless indium-interaction-mode
(indium-interaction-mode-off)))
(defun indium-interaction-mode-off ()
"Function to be evaluated when `indium-interaction-mode' is turned off."
(indium-breakpoint-remove-overlays-from-current-buffer))
(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-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-on-current-line-p)
(user-error "No breakpoint on the current line")))
(defun indium-interaction--guard-no-breakpoint-at-point ()
"Signal an error if there is a breakpoint on the current line."
(when (indium-breakpoint-at-point)
(user-error "There is already a breakpoint on the current line")))
(defun indium-interaction--current-directory ()
"Return the true name of the current directory.
For the project root to be correctly set, symlinks are resolved."
(file-truename default-directory))
(add-hook 'kill-buffer-hook #'indium-interaction-kill-buffer)
(provide 'indium-interaction)
;;; indium-interaction.el ends here