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.
 
 
 
 
 

435 lines
16 KiB

  1. ;;; indium-interaction.el --- Interaction functions for indium.el -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2016-2018 Nicolas Petton
  3. ;; Author: Nicolas Petton <nicolas@petton.fr>
  4. ;; Keywords: javascript, tools
  5. ;; This program is free software; you can redistribute it and/or modify
  6. ;; it under the terms of the GNU General Public License as published by
  7. ;; the Free Software Foundation, either version 3 of the License, or
  8. ;; (at your option) any later version.
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; Minor mode for interacting with a JavaScript runtime. This mode provides
  17. ;; commands connecting to a runtime, managing breakpoints and evaluating code.
  18. ;;; Code:
  19. (require 'js2-mode)
  20. (require 'map)
  21. (require 'seq)
  22. (require 'subr-x)
  23. (require 'xref)
  24. (require 'easymenu)
  25. (require 'indium-client)
  26. (require 'indium-inspector)
  27. (require 'indium-breakpoint)
  28. (require 'indium-repl)
  29. (require 'indium-render)
  30. (require 'indium-nodejs)
  31. (require 'indium-chrome)
  32. (require 'indium-debugger)
  33. (declare-function indium-launch-chrome "indium-chrome.el")
  34. (declare-function indium-launch-nodejs "indium-nodejs.el")
  35. ;;;###autoload
  36. (defun indium-connect ()
  37. "Open a new connection to a runtime."
  38. (interactive)
  39. (indium-maybe-quit)
  40. (unless (indium-client-process-live-p)
  41. (let ((dir (indium-interaction--current-directory)))
  42. (indium-client-start
  43. (lambda ()
  44. (indium-client-list-configurations
  45. dir
  46. (lambda (configurations)
  47. (when-let ((conf (indium-interaction--read-configuration configurations)))
  48. (indium-client-connect dir (map-elt conf 'name))))))))))
  49. ;;;###autoload
  50. (defun indium-launch ()
  51. "Start a new process and connect to it."
  52. (interactive)
  53. (indium-maybe-quit)
  54. (unless (indium-client-process-live-p)
  55. (let ((dir (indium-interaction--current-directory)))
  56. (indium-client-start
  57. (lambda ()
  58. (indium-client-list-configurations
  59. dir
  60. (lambda (configurations)
  61. (when-let ((conf (indium-interaction--read-configuration configurations)))
  62. (pcase (map-elt conf 'type)
  63. ("node" (indium-launch-nodejs conf))
  64. ("chrome" (indium-launch-chrome conf))
  65. (_ (error "Unsupported configuration")))))))))))
  66. (defun indium-interaction--read-configuration (configurations)
  67. "Prompt the user for a configuration from CONFIGURATIONS."
  68. (let ((configuration-names (seq-map (lambda (configuration)
  69. (map-elt configuration 'name))
  70. configurations)))
  71. (unless configuration-names
  72. (user-error "No configuration name provided in the project file"))
  73. (if (= (seq-length configuration-names) 1)
  74. (seq-elt configurations 0)
  75. (when-let ((name (completing-read "Choose a configuration: "
  76. configuration-names nil t)))
  77. (seq-find (lambda (conf)
  78. (equal (map-elt conf 'name) name))
  79. configurations)))))
  80. (defun indium-quit ()
  81. "Close the current connection and kill its REPL buffer if any."
  82. (interactive)
  83. (indium-client-stop)
  84. (indium-interaction--cleanup-buffers))
  85. (defun indium-maybe-quit ()
  86. "Close the current connection.
  87. Unlike `indium-quit', do not signal an error when there is no
  88. active connection."
  89. (interactive)
  90. (when (and (indium-client-process-live-p)
  91. (yes-or-no-p "Do you want to close the current Indium process?"))
  92. (indium-quit)))
  93. (defun indium-eval (string &optional callback)
  94. "Evaluate STRING on the current backend.
  95. When CALLBACK is non-nil, evaluate CALLBACK with the result.
  96. When called interactively, prompt the user for the string to be
  97. evaluated.
  98. Evaluation happens in the context of the current debugger frame if any."
  99. (interactive "sEvaluate JavaScript: ")
  100. (indium-client-evaluate string indium-debugger-current-frame callback))
  101. (defun indium-eval-buffer ()
  102. "Evaluate the accessible portion of current buffer."
  103. (interactive)
  104. (indium-eval (buffer-string)
  105. #'indium-interaction--handle-eval-result))
  106. (defun indium-eval-region (start end)
  107. "Evaluate the region between START and END."
  108. (interactive "r")
  109. (indium-eval (buffer-substring-no-properties start end)
  110. #'indium-interaction--handle-eval-result))
  111. (defun indium-eval-last-node (arg)
  112. "Evaluate the node before point; print in the echo area.
  113. This is similar to `eval-last-sexp', but for JavaScript buffers.
  114. Interactively, with a prefix argument ARG, print the output into
  115. the current buffer."
  116. (interactive "P")
  117. (indium-interaction--eval-node (indium-interaction-node-before-point) arg))
  118. (defun indium-eval-defun ()
  119. "Evaluate the innermost function enclosing the current point."
  120. (interactive)
  121. (if-let ((node (js2-mode-function-at-point)))
  122. (indium-interaction--eval-node node)
  123. (user-error "No function at point")))
  124. (defun indium-switch-to-debugger ()
  125. "Switch to the buffer containing the Indium debugger.
  126. The point is moved to the top stack frame.
  127. If there is no debugging session, signal an error."
  128. (interactive)
  129. (indium-debugger-switch-to-debugger-buffer))
  130. (defvar indium-interaction-eval-hook nil
  131. "Hooks to run after evaluating node before the point.")
  132. (add-hook 'indium-interaction-eval-hook #'indium-message)
  133. (defun indium-interaction--eval-node (node &optional print)
  134. "Evaluate the AST node NODE.
  135. If PRINT is non-nil, print the output into the current buffer."
  136. (js2-mode-wait-for-parse
  137. (lambda ()
  138. (indium-eval (js2-node-string node)
  139. (lambda (value)
  140. (indium-interaction--handle-eval-result
  141. value
  142. print))))))
  143. (defun indium-interaction--handle-eval-result (value &optional print)
  144. "Handle VALUE is the result of an evaluation.
  145. The default behavior is to print it in the echo area.
  146. If PRINT in non-nil, insert it in the current buffer instead."
  147. (let ((description (indium-render-remote-object-to-string value)))
  148. (if print
  149. (save-excursion
  150. (insert description))
  151. (run-hook-with-args 'indium-interaction-eval-hook description))))
  152. (defun indium-reload ()
  153. "Reload the page."
  154. (interactive)
  155. (indium-client-evaluate "window.location.reload()"))
  156. (defun indium-inspect-last-node ()
  157. "Evaluate and inspect the node before point."
  158. (interactive)
  159. (js2-mode-wait-for-parse
  160. (lambda ()
  161. (indium-inspect-expression
  162. (js2-node-string (indium-interaction-node-before-point))))))
  163. (defun indium-inspect-expression (expression)
  164. "Prompt for EXPRESSION to be inspected."
  165. (interactive "sInspect expression: ")
  166. (indium-eval expression
  167. (lambda (result)
  168. (indium-inspector-inspect result))))
  169. (defun indium-switch-to-repl-buffer ()
  170. "Switch to the repl buffer if any."
  171. (interactive)
  172. (if (indium-client-process-live-p)
  173. (let ((buf (indium-repl-get-buffer-create)))
  174. (progn
  175. (setq indium-repl-switch-from-buffer (current-buffer))
  176. (pop-to-buffer buf t)))
  177. (user-error "Not connected, cannot open REPL buffer")))
  178. (defun indium-toggle-breakpoint ()
  179. "Add or remove a breakpoint on current line."
  180. (interactive)
  181. (if (indium-breakpoint-on-current-line-p)
  182. (call-interactively #'indium-remove-breakpoint)
  183. (call-interactively #'indium-add-breakpoint)))
  184. (defun indium-mouse-toggle-breakpoint (event)
  185. "Toggle breakpoint at mouse EVENT click point."
  186. (interactive "e")
  187. (let* ((posn (event-end event))
  188. (pos (posn-point posn)))
  189. (when (numberp pos)
  190. (with-current-buffer (window-buffer (posn-window posn))
  191. (save-excursion
  192. (goto-char pos)
  193. (call-interactively #'indium-toggle-breakpoint))))))
  194. (defun indium-add-breakpoint (&optional condition)
  195. "Add a breakpoint on the current line.
  196. If there is already a breakpoint, signal an error.
  197. When CONDITION is non-nil, add a conditional breakpoint with
  198. CONDITION."
  199. (interactive)
  200. (indium-interaction--guard-no-breakpoint-at-point)
  201. (save-excursion
  202. (beginning-of-line)
  203. (indium-breakpoint-add condition)))
  204. (defun indium-add-conditional-breakpoint (condition)
  205. "Add a breakpoint with CONDITION at point.
  206. If there is already a breakpoint, signal an error."
  207. (interactive "sBreakpoint condition: ")
  208. (indium-add-breakpoint condition))
  209. (defun indium-edit-breakpoint-condition ()
  210. "Edit the condition of breakpoint at point.
  211. Signal an error if there is no breakpoint."
  212. (interactive)
  213. (indium-interaction--guard-breakpoint-at-point)
  214. (indium-breakpoint-edit-condition))
  215. (defun indium-remove-breakpoint ()
  216. "Remove the breakpoint at point.
  217. If there is no breakpoint, signal an error."
  218. (interactive)
  219. (indium-interaction--guard-breakpoint-at-point)
  220. (indium-breakpoint-remove))
  221. (defun indium-remove-all-breakpoints-from-buffer ()
  222. "Remove all breakpoints from the current buffer."
  223. (interactive)
  224. (indium-breakpoint-remove-breakpoints-from-current-buffer))
  225. (defun indium-deactivate-breakpoints ()
  226. "Deactivate all breakpoints in all buffers.
  227. Breakpoints are not removed, but the runtime won't pause when
  228. hitting a breakpoint."
  229. (interactive)
  230. (indium-client-deactivate-breakpoints)
  231. (message "Breakpoints deactivated"))
  232. (defun indium-activate-breakpoints ()
  233. "Activate all breakpoints in all buffers."
  234. (interactive)
  235. (indium-client-activate-breakpoints)
  236. (message "Breakpoints activated"))
  237. (defun indium-list-breakpoints ()
  238. "List all breakpoints in the current connection."
  239. (interactive)
  240. (if-let ((xrefs (indium--make-xrefs-from-breakpoints)))
  241. (xref--show-xrefs xrefs nil)
  242. (message "No breakpoint")))
  243. (defun indium--make-xrefs-from-breakpoints ()
  244. "Return a list of xref objects from all breakpoints."
  245. (map-apply (lambda (breakpoint buffer)
  246. (let ((line (with-current-buffer buffer
  247. (line-number-at-pos
  248. (overlay-start
  249. (indium-breakpoint-overlay breakpoint))))))
  250. (xref-make (indium--get-breakpoint-xref-match breakpoint buffer)
  251. (xref-make-file-location (buffer-file-name buffer)
  252. line
  253. 0))))
  254. indium-breakpoint--local-breakpoints))
  255. (defun indium--get-breakpoint-xref-match (breakpoint buffer)
  256. "Return the source line where BREAKPOINT is set in BUFFER."
  257. (with-current-buffer buffer
  258. (save-excursion
  259. (goto-char (point-min))
  260. (forward-line (1- (line-number-at-pos
  261. (overlay-start
  262. (indium-breakpoint-overlay breakpoint)))))
  263. (buffer-substring (point-at-bol) (point-at-eol)))))
  264. (defun indium-interaction-node-before-point ()
  265. "Return the node before point to be evaluated."
  266. (save-excursion
  267. (forward-comment -1)
  268. (while (looking-back "[:,]" nil)
  269. (backward-char 1))
  270. (backward-char 1)
  271. (while (js2-empty-expr-node-p (js2-node-at-point))
  272. (backward-char 1))
  273. (let* ((node (js2-node-at-point))
  274. (parent (js2-node-parent node)))
  275. ;; Heuristics for finding the node to evaluate: if the parent of the node
  276. ;; before point is a prop-get node (i.e. foo.bar) and if it starts before
  277. ;; the current node, meaning that the point is on the node following the
  278. ;; parent, then return the parent node:
  279. ;;
  280. ;; (underscore represents the point)
  281. ;; foo.ba_r // => evaluate foo.bar
  282. ;; foo_.bar // => evaluate foo
  283. ;; foo.bar.baz_() // => evaluate foo.bar.baz
  284. ;; foo.bar.baz()_ // => evaluate foo.bar.baz()
  285. ;;
  286. ;; If the node is a "block node" (i.e. the `{...}' part of a function
  287. ;; declaration, also return the parent node.
  288. (while (or (and (js2-prop-get-node-p parent)
  289. (< (js2-node-abs-pos parent)
  290. (js2-node-abs-pos node)))
  291. (and (not (js2-function-node-p node))
  292. (not (js2-loop-node-p node))
  293. (js2-block-node-p node)))
  294. (setq node parent))
  295. node)))
  296. (defvar indium-interaction-mode-map
  297. (let ((map (make-sparse-keymap)))
  298. (define-key map (kbd "C-x C-e") #'indium-eval-last-node)
  299. (define-key map (kbd "C-M-x") #'indium-eval-defun)
  300. (define-key map (kbd "C-c M-i") #'indium-inspect-last-node)
  301. (define-key map (kbd "C-c M-:") #'indium-inspect-expression)
  302. (define-key map (kbd "C-c C-z") #'indium-switch-to-repl-buffer)
  303. (define-key map [left-fringe mouse-1] #'indium-mouse-toggle-breakpoint)
  304. (define-key map [left-margin mouse-1] #'indium-mouse-toggle-breakpoint)
  305. (define-key map (kbd "C-c b t") #'indium-toggle-breakpoint)
  306. (define-key map (kbd "C-c b b") #'indium-add-breakpoint)
  307. (define-key map (kbd "C-c b c") #'indium-add-conditional-breakpoint)
  308. (define-key map (kbd "C-c b e") #'indium-edit-breakpoint-condition)
  309. (define-key map (kbd "C-c b k") #'indium-remove-breakpoint)
  310. (define-key map (kbd "C-c b K") #'indium-remove-all-breakpoints-from-buffer)
  311. (define-key map (kbd "C-c b a") #'indium-activate-breakpoints)
  312. (define-key map (kbd "C-c b d") #'indium-deactivate-breakpoints)
  313. (define-key map (kbd "C-c b l") #'indium-list-breakpoints)
  314. (define-key map (kbd "C-c d") #'indium-switch-to-debugger)
  315. (easy-menu-define indium-interaction-mode-menu map
  316. "Menu for Indium interaction mode"
  317. '("Indium interaction"
  318. ["Switch to REPL" indium-switch-to-repl-buffer]
  319. "--"
  320. ("Evaluation"
  321. ["Evaluate last node" indium-eval-last-node]
  322. ["Inspect last node" indium-inspect-last-node]
  323. ["Inspect expression" indium-inspect-expression]
  324. ["Evaluate function" indium-eval-defun])
  325. "--"
  326. ("Breakpoints"
  327. ["Add breakpoint" indium-add-breakpoint]
  328. ["Add conditional breakpoint" indium-add-conditional-breakpoint]
  329. ["Remove breakpoint" indium-remove-breakpoint]
  330. ["Remove all breakpoints" indium-remove-all-breakpoints-from-buffer]
  331. ["Deactivate breakpoints" indium-deactivate-breakpoints]
  332. ["Activate breakpoints" indium-activate-breakpoints]
  333. ["List all breakpoints" indium-list-breakpoints])))
  334. map))
  335. (define-minor-mode indium-interaction-mode
  336. "Mode for JavaScript evaluation.
  337. \\{indium-interaction-mode-map}"
  338. :lighter " js-interaction"
  339. :keymap indium-interaction-mode-map
  340. (unless indium-interaction-mode
  341. (indium-interaction-mode-off)))
  342. (defun indium-interaction-mode-off ()
  343. "Function to be evaluated when `indium-interaction-mode' is turned off."
  344. (indium-breakpoint-remove-overlays-from-current-buffer))
  345. (defun indium-interaction-kill-buffer ()
  346. "Remove all breakpoints prior to killing the current buffer."
  347. (when indium-interaction-mode
  348. (indium-breakpoint-remove-breakpoints-from-current-buffer)))
  349. (defun indium-interaction--cleanup-buffers ()
  350. "Cleanup all Indium buffers after a connection is closed."
  351. (seq-map (lambda (buf)
  352. (with-current-buffer buf
  353. (when buffer-file-name
  354. (indium-debugger-unset-current-buffer))))
  355. (buffer-list))
  356. (when-let ((buf (indium-repl-get-buffer)))
  357. (kill-buffer buf)))
  358. (defun indium-interaction--guard-breakpoint-at-point ()
  359. "Signal an error if there is no breakpoint on the current line."
  360. (unless (indium-breakpoint-on-current-line-p)
  361. (user-error "No breakpoint on the current line")))
  362. (defun indium-interaction--guard-no-breakpoint-at-point ()
  363. "Signal an error if there is a breakpoint on the current line."
  364. (when (indium-breakpoint-at-point)
  365. (user-error "There is already a breakpoint on the current line")))
  366. (defun indium-interaction--current-directory ()
  367. "Return the true name of the current directory.
  368. For the project root to be correctly set, symlinks are resolved."
  369. (file-truename default-directory))
  370. (add-hook 'kill-buffer-hook #'indium-interaction-kill-buffer)
  371. (provide 'indium-interaction)
  372. ;;; indium-interaction.el ends here