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.
 
 
 
 
 

426 lines
15 KiB

  1. ;;; indium-repl.el --- JavaScript REPL connected to a browser tab -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2016-2018 Nicolas Petton
  3. ;; Author: Nicolas Petton <nicolas@petton.fr>
  4. ;; Keywords: convenience, tools, javascript
  5. ;; This program is free software; you can redistribute it and/or modify
  6. ;; it under the terms of the GNU General Public License as published by
  7. ;; the Free Software Foundation, either version 3 of the License, or
  8. ;; (at your option) any later version.
  9. ;; This program is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; REPL interactions with a browser connection.
  17. ;;; Code:
  18. (require 'indium-render)
  19. (require 'indium-faces)
  20. (require 'indium-client)
  21. (require 'indium-debugger)
  22. (require 'company)
  23. (require 'easymenu)
  24. (require 'map)
  25. (require 'js)
  26. (require 'subr-x)
  27. (require 'ansi-color)
  28. (declare-function indium-inspector-inspect "indium-inspector.el")
  29. (declare-function indium-maybe-quit "indium-interaction.el")
  30. (defgroup indium-repl nil
  31. "Interaction with the REPL."
  32. :prefix "indium-repl-"
  33. :group 'indium)
  34. (defvar indium-repl-evaluate-hook nil
  35. "Hook run when input is evaluated in the repl.")
  36. (defvar indium-repl-switch-from-buffer nil
  37. "The buffer from which repl was activated last time.")
  38. (defvar indium-repl-history nil "History of the REPL inputs.")
  39. (make-variable-buffer-local 'indium-repl-history)
  40. (defvar indium-repl-history-position -1 "Position in the REPL history.")
  41. (make-variable-buffer-local 'indium-repl-history-position)
  42. (defvar-local indium-repl-input-start-marker nil)
  43. (defvar-local indium-repl-output-start-marker nil)
  44. (defvar-local indium-repl-output-end-marker nil)
  45. (defmacro indium-save-marker (marker &rest body)
  46. "Save MARKER and execute BODY."
  47. (declare (indent 1) (debug t))
  48. (let ((pos (make-symbol "pos")))
  49. `(let ((,pos (marker-position ,marker)))
  50. (prog1 (progn . ,body)
  51. (set-marker ,marker ,pos)))))
  52. (defun indium-repl-setup ()
  53. "Create and switch to the REPL buffer."
  54. (switch-to-buffer (indium-repl-get-buffer-create)))
  55. (defun indium-repl-get-buffer-create ()
  56. "Return a new REPL buffer."
  57. (let* ((buf (get-buffer-create (indium-repl-buffer-name))))
  58. (indium-repl-setup-buffer buf)
  59. buf))
  60. (defun indium-repl-get-buffer ()
  61. "Return the REPL buffer, or nil."
  62. (get-buffer (indium-repl-buffer-name)))
  63. (defun indium-repl-buffer-name ()
  64. "Return the name of the REPL buffer."
  65. "*JS REPL*")
  66. (defun indium-repl-setup-buffer (buffer)
  67. "Setup the REPL BUFFER."
  68. (with-current-buffer buffer
  69. (unless (eq major-mode 'indium-repl-mode)
  70. (indium-repl-mode)
  71. (indium-repl-setup-markers)
  72. (indium-repl-mark-output-start)
  73. (indium-repl-insert-prompt)
  74. (indium-repl-mark-input-start)
  75. (indium-repl-emit-console-message
  76. `((result . ,(indium-remote-object-create
  77. :description (indium-repl--welcome-message))))))))
  78. (defun indium-repl--welcome-message ()
  79. "Return the welcome message displayed in new REPL buffers."
  80. (substitute-command-keys
  81. "/* Welcome to Indium!
  82. Getting started:
  83. - Press <\\[indium-repl-return]> on links to open an inspector
  84. - Press <\\[indium-repl-previous-input]> and <\\[indium-repl-next-input]> to navigate in the history
  85. - Use <\\[indium-scratch]> to open a scratch buffer for JS evaluation
  86. - Press <\\[describe-mode]> to see a list of available keybindings
  87. - Press <\\[indium-repl-clear-output]> to clear the output
  88. To disconnect from the JavaScript process, press <\\[indium-quit]>.
  89. Doing this will also close all inspectors and debugger buffers
  90. connected to the process.
  91. */"))
  92. (defun indium-repl-setup-markers ()
  93. "Setup the initial markers for the current REPL buffer."
  94. (dolist (marker '(indium-repl-output-start-marker
  95. indium-repl-output-end-marker
  96. indium-repl-input-start-marker))
  97. (set marker (make-marker))
  98. (set-marker (symbol-value marker) (point))))
  99. (defun indium-repl-mark-output-start ()
  100. "Mark the output start."
  101. (set-marker indium-repl-output-start-marker (point))
  102. (set-marker indium-repl-output-end-marker (point)))
  103. (defun indium-repl-mark-input-start ()
  104. "Mark the input start."
  105. (set-marker indium-repl-input-start-marker (point)))
  106. (defun indium-repl-insert-prompt ()
  107. "Insert the prompt in the REPL buffer."
  108. (goto-char indium-repl-input-start-marker)
  109. (indium-save-marker indium-repl-output-start-marker
  110. (indium-save-marker indium-repl-output-end-marker
  111. (unless (bolp)
  112. (insert-before-markers "\n"))
  113. (insert-before-markers "js> ")
  114. (let ((beg (save-excursion
  115. (beginning-of-line)
  116. (point)))
  117. (end (point)))
  118. (set-text-properties beg end
  119. '(font-lock-face indium-repl-prompt-face
  120. read-only t
  121. intangible t
  122. field indium-repl-prompt
  123. rear-nonsticky (read-only font-lock-face intangible field)))))))
  124. (defun indium-repl-return ()
  125. "Depending on the position of point, jump to a reference of evaluate the input."
  126. (interactive)
  127. (cond
  128. ((get-text-property (point) 'indium-reference) (indium-follow-link))
  129. ((get-text-property (point) 'indium-action) (indium-perform-action))
  130. ((indium-repl--in-input-area-p) (indium-repl-evaluate (indium-repl--input-content)))
  131. (t (error "No input or action at point"))))
  132. (defun indium-repl-inspect ()
  133. "Inspect the result of the evaluation of the input at point."
  134. (interactive)
  135. (indium-client-evaluate (indium-repl--input-content)
  136. indium-debugger-current-frame
  137. (lambda (result)
  138. (indium-inspector-inspect result))))
  139. (defun indium-repl--input-content ()
  140. "Return the content of the current input."
  141. (buffer-substring-no-properties indium-repl-input-start-marker (point-max)))
  142. (defun indium-repl--in-input-area-p ()
  143. "Return t if in input area."
  144. (<= indium-repl-input-start-marker (point)))
  145. (defun indium-repl-evaluate (string)
  146. "Evaluate STRING in the browser tab and emit the output."
  147. (push string indium-repl-history)
  148. (indium-client-evaluate string indium-debugger-current-frame #'indium-repl-emit-value)
  149. ;; move the output markers so that output is put after the current prompt
  150. (save-excursion
  151. (goto-char (point-max))
  152. (set-marker indium-repl-output-start-marker (point))
  153. (set-marker indium-repl-output-end-marker (point))))
  154. (defun indium-repl-emit-value (value)
  155. "Emit a string representation of the remote object VALUE."
  156. (with-current-buffer (indium-repl-get-buffer)
  157. (save-excursion
  158. (goto-char (point-max))
  159. (insert-before-markers "\n")
  160. (set-marker indium-repl-output-start-marker (point))
  161. (indium-render-remote-object value)
  162. (insert "\n")
  163. (indium-repl-mark-input-start)
  164. (set-marker indium-repl-output-end-marker (point)))
  165. (indium-repl-insert-prompt)
  166. (run-hooks 'indium-repl-evaluate-hook)))
  167. (defun indium-repl-emit-console-message (message &optional error)
  168. "Emit a console MESSAGE.
  169. When ERROR is non-nil, display MESSAGE as an error.
  170. MESSAGE is a map (alist/hash-table) with the following keys:
  171. type type of message
  172. url url of the message origin
  173. line line number in the resource that generated this message
  174. result object to be logged
  175. MESSAGE must contain `result'. Other fields are
  176. optional."
  177. (with-current-buffer (indium-repl-get-buffer)
  178. (let-alist message
  179. (when (string= .type 'error)
  180. (setq error t)))
  181. (save-excursion
  182. (goto-char indium-repl-output-end-marker)
  183. (set-marker indium-repl-output-start-marker (point))
  184. (insert "\n")
  185. (when error
  186. (indium-repl--emit-logging-error))
  187. (indium-repl--emit-message message)
  188. (set-marker indium-repl-output-end-marker (point))
  189. (unless (eolp)
  190. (insert "\n")))))
  191. (defun indium-repl--emit-message (message)
  192. "Emit the value of console MESSAGE."
  193. (let-alist message
  194. (indium-render-remote-object .result)
  195. (indium-repl--emit-message-url-line .url .line)))
  196. (defun indium-repl--emit-logging-error ()
  197. "Emit a red \"Error\" label."
  198. (insert
  199. (ansi-color-apply
  200. (propertize "Error:"
  201. 'font-lock-face 'indium-repl-error-face
  202. 'rear-nonsticky '(font-lock-face)))
  203. " "))
  204. (defun indium-repl--emit-message-url-line (url line)
  205. "Emit the URL and LINE for a message."
  206. (unless (seq-empty-p url)
  207. (insert "\nFrom "
  208. (propertize (if line
  209. (format "%s:%s" url line)
  210. url)
  211. 'font-lock-face 'indium-link-face
  212. 'indium-action (lambda ()
  213. (if (file-regular-p url)
  214. (find-file url)
  215. (browse-url url)))
  216. 'rear-nonsticky '(font-lock-face indium-action)))))
  217. (defun indium-repl-next-input ()
  218. "Insert the content of the next input in the history."
  219. (interactive)
  220. (indium-repl--history-replace 'forward))
  221. (defun indium-repl-previous-input ()
  222. "Insert the content of the previous input in the history."
  223. (interactive)
  224. (indium-repl--history-replace 'backward))
  225. (defun indium-repl--history-replace (direction)
  226. "Replace the current input with one the next one in DIRECTION.
  227. DIRECTION is `forward' or `backard' (in the history list)."
  228. (let* ((history (seq-reverse indium-repl-history))
  229. (search-in-progress (or (eq last-command 'indium-repl-previous-input)
  230. (eq last-command 'indium-repl-next-input)))
  231. (step (pcase direction
  232. (`forward 1)
  233. (`backward -1)))
  234. (pos (or (and search-in-progress (+ indium-repl-history-position step))
  235. (1- (seq-length history)))))
  236. (unless (>= pos 0)
  237. (user-error "Beginning of history"))
  238. (unless (< pos (seq-length history))
  239. (user-error "End of history"))
  240. (setq indium-repl-history-position pos)
  241. (indium-repl--replace-input (seq-elt history pos))))
  242. (defun indium-repl--replace-input (input)
  243. "Replace the current input with INPUT."
  244. (goto-char (point-max))
  245. (delete-region indium-repl-input-start-marker (point))
  246. (insert input))
  247. (defun indium-repl-clear-output ()
  248. "Clear all output contents of the current buffer."
  249. (interactive)
  250. (let ((inhibit-read-only t))
  251. (save-excursion
  252. (goto-char (point-min))
  253. (delete-region (point) indium-repl-output-end-marker))))
  254. (defun indium-repl-pop-buffer ()
  255. "Switch to the buffer from which repl was opened buffer if any."
  256. (interactive)
  257. (when indium-repl-switch-from-buffer
  258. (pop-to-buffer indium-repl-switch-from-buffer t)))
  259. (defun company-indium-repl (command &optional arg &rest _args)
  260. "Indium REPL backend for company-mode.
  261. See `company-backends' for more info about COMMAND and ARG."
  262. (interactive (list 'interactive))
  263. (cl-case command
  264. (interactive (company-begin-backend 'company-indium-repl))
  265. (prefix (indium-repl-company-prefix))
  266. (ignore-case t)
  267. (sorted t)
  268. (candidates (cons :async
  269. (lambda (callback)
  270. (indium-repl-get-completions arg callback))))))
  271. (defun indium-repl-get-completions (prefix callback)
  272. "Get the completion list matching PREFIX.
  273. Evaluate CALLBACK with the completion candidates."
  274. (let* ((input (buffer-substring-no-properties
  275. (let ((bol (point-at-bol))
  276. (prev-delimiter (1+ (save-excursion
  277. (re-search-backward "[([:space:]]" nil t)))))
  278. (if prev-delimiter
  279. (max bol prev-delimiter)
  280. bol))
  281. (point)))
  282. (expression (if (string-match-p "\\." input)
  283. (replace-regexp-in-string "\\.[^\\.]*$" "" input)
  284. "this")))
  285. (indium-client-get-completion
  286. expression
  287. indium-debugger-current-frame
  288. (lambda (candidates)
  289. (funcall callback
  290. (seq-filter (lambda (candidate)
  291. (string-prefix-p prefix candidate))
  292. candidates))))))
  293. (defun indium-repl--complete-or-indent ()
  294. "Complete or indent at point."
  295. (interactive)
  296. (if (company-manual-begin)
  297. (company-complete-common)
  298. (indent-according-to-mode)))
  299. (defun indium-repl-company-prefix ()
  300. "Prefix for company."
  301. (and (or (eq major-mode 'indium-repl-mode)
  302. (bound-and-true-p indium-interaction-mode))
  303. (or (company-grab-symbol-cons "\\." 1)
  304. 'stop)))
  305. (defvar indium-repl-mode-hook nil
  306. "Hook executed when entering `indium-repl-mode'.")
  307. (defvar indium-repl-mode-map
  308. (let ((map (make-sparse-keymap)))
  309. (define-key map [return] #'indium-repl-return)
  310. (define-key map "\C-m" #'indium-repl-return)
  311. (define-key map (kbd "TAB") #'indium-repl--complete-or-indent)
  312. (define-key map [mouse-1] #'indium-follow-link)
  313. (define-key map (kbd "C-<return>") #'newline)
  314. (define-key map (kbd "C-c M-i") #'indium-repl-inspect)
  315. (define-key map (kbd "C-c C-o") #'indium-repl-clear-output)
  316. (define-key map (kbd "C-c C-z") #'indium-repl-pop-buffer)
  317. (define-key map (kbd "C-c C-q") #'indium-maybe-quit)
  318. (define-key map (kbd "M-p") #'indium-repl-previous-input)
  319. (define-key map (kbd "M-n") #'indium-repl-next-input)
  320. (define-key map (kbd "C-<up>") #'indium-repl-previous-input)
  321. (define-key map (kbd "C-<down>") #'indium-repl-next-input)
  322. (easy-menu-define indium-repl-mode-menu map
  323. "Menu for Indium REPL"
  324. '("Indium REPL"
  325. ["Clear output" indium-repl-clear-output]
  326. ["Inspect" indium-repl-inspect]
  327. "--"
  328. ["Switch to source buffer" indium-repl-pop-buffer]
  329. "--"
  330. ["Quit" indium-maybe-quit]))
  331. map))
  332. (define-derived-mode indium-repl-mode fundamental-mode "JS-REPL"
  333. "Major mode for indium REPL interactions.
  334. \\{indium-repl-mode-map}"
  335. (font-lock-add-keywords nil '(indium-repl--fontify-output))
  336. (setq-local company-backends '(company-indium-repl))
  337. (company-mode 1))
  338. (defun indium-repl--fontify-output (&rest _)
  339. "Fontify JS code output."
  340. (let* ((start indium-repl-input-start-marker)
  341. (end (point-max))
  342. (repl-buffer (current-buffer))
  343. (string (buffer-substring-no-properties start end)))
  344. (with-current-buffer
  345. (get-buffer-create " indium-fontification ")
  346. (let ((inhibit-modification-hooks nil))
  347. (js-mode)
  348. (erase-buffer)
  349. (insert string " ")
  350. (font-lock-ensure)
  351. (let ((pos (point-min)) next)
  352. (while (setq next (next-property-change pos))
  353. ;; Handle additional properties from font-lock, so as to
  354. ;; preserve, e.g., composition.
  355. (dolist (prop (cons 'face font-lock-extra-managed-props))
  356. (let ((new-prop (get-text-property pos prop)))
  357. (put-text-property
  358. (+ start (1- pos)) (1- (+ start next)) prop new-prop
  359. repl-buffer)))
  360. (setq pos next)))))))
  361. (add-hook 'indium-client-connected-hook #'indium-repl-setup)
  362. (add-hook 'indium-client-log-hook #'indium-repl-emit-console-message)
  363. (provide 'indium-repl)
  364. ;;; indium-repl.el ends here