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.

427 lines
16 KiB

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