Personal emacs config
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.

208 lines
7.8 KiB

  1. ;;; skewer-repl.el --- create a REPL in a visiting browser -*- lexical-binding: t; -*-
  2. ;; This is free and unencumbered software released into the public domain.
  3. ;;; Commentary:
  4. ;; This is largely based on of IELM's code. Run `skewer-repl' to
  5. ;; switch to the REPL buffer and evaluate code. Use
  6. ;; `skewer-repl-toggle-strict-mode' to turn strict mode on and off.
  7. ;; If `compilation-search-path' is set up properly, along with
  8. ;; `skewer-path-strip-level', asynchronous errors will provide
  9. ;; clickable error messages that will take you to the source file of
  10. ;; the error. This is done using `compilation-shell-minor-mode'.
  11. ;;; Code:
  12. (require 'comint)
  13. (require 'compile)
  14. (require 'skewer-mode)
  15. (defcustom skewer-repl-strict-p nil
  16. "When non-NIL, all REPL evaluations are done in strict mode."
  17. :type 'boolean
  18. :group 'skewer)
  19. (defcustom skewer-repl-prompt "js> "
  20. "Prompt string for JavaScript REPL."
  21. :type 'string
  22. :group 'skewer)
  23. (defvar skewer-repl-welcome
  24. (propertize "*** Welcome to Skewer ***\n"
  25. 'font-lock-face 'font-lock-comment-face)
  26. "Header line to show at the top of the REPL buffer. Hack
  27. notice: this allows log messages to appear before anything is
  28. evaluated because it provides insertable space at the top of the
  29. buffer.")
  30. (defun skewer-repl-process ()
  31. "Return the process for the skewer REPL."
  32. (get-buffer-process (current-buffer)))
  33. (defface skewer-repl-log-face
  34. '((((class color) (background light))
  35. :foreground "#77F")
  36. (((class color) (background dark))
  37. :foreground "#77F"))
  38. "Face for skewer.log() messages."
  39. :group 'skewer)
  40. (define-derived-mode skewer-repl-mode comint-mode "js-REPL"
  41. "Provide a REPL into the visiting browser."
  42. :group 'skewer
  43. :syntax-table emacs-lisp-mode-syntax-table
  44. (setq comint-prompt-regexp (concat "^" (regexp-quote skewer-repl-prompt))
  45. comint-input-sender 'skewer-input-sender
  46. comint-process-echoes nil)
  47. ;; Make opportunistic use of company-mode, but don't require it.
  48. ;; This means company-backends may be undeclared, so don't emit a
  49. ;; warning about it.
  50. (with-no-warnings
  51. (setq-local company-backends '(company-skewer-repl)))
  52. (unless (comint-check-proc (current-buffer))
  53. (insert skewer-repl-welcome)
  54. (start-process "skewer-repl" (current-buffer) nil)
  55. (set-process-query-on-exit-flag (skewer-repl-process) nil)
  56. (goto-char (point-max))
  57. (set (make-local-variable 'comint-inhibit-carriage-motion) t)
  58. (comint-output-filter (skewer-repl-process) skewer-repl-prompt)
  59. (set-process-filter (skewer-repl-process) 'comint-output-filter)))
  60. (defun skewer-repl-toggle-strict-mode ()
  61. "Toggle strict mode for expressions evaluated by the REPL."
  62. (interactive)
  63. (setq skewer-repl-strict-p (not skewer-repl-strict-p))
  64. (message "REPL strict mode %s"
  65. (if skewer-repl-strict-p "enabled" "disabled")))
  66. (defun skewer-input-sender (_ input)
  67. "REPL comint handler."
  68. (skewer-eval input 'skewer-post-repl
  69. :verbose t :strict skewer-repl-strict-p))
  70. (defun skewer-post-repl (result)
  71. "Callback for reporting results in the REPL."
  72. (let ((buffer (get-buffer "*skewer-repl*"))
  73. (output (cdr (assoc 'value result))))
  74. (when buffer
  75. (with-current-buffer buffer
  76. (comint-output-filter (skewer-repl-process)
  77. (concat output "\n" skewer-repl-prompt))))))
  78. (defvar skewer-repl-types
  79. '(("log" . skewer-repl-log-face)
  80. ("error" . skewer-error-face))
  81. "Faces to use for different types of log messages.")
  82. (defun skewer-log-filename (log)
  83. "Create a log string for the source file in LOG if present."
  84. (let ((name (cdr (assoc 'filename log)))
  85. (line (cdr (assoc 'line log)))
  86. (column (cdr (assoc 'column log))))
  87. (when name
  88. (concat (format "\n at %s:%s" name line)
  89. (if column (format ":%s" column))))))
  90. (defun skewer-post-log (log)
  91. "Callback for logging messages to the REPL."
  92. (let* ((buffer (get-buffer "*skewer-repl*"))
  93. (face (cdr (assoc (cdr (assoc 'type log)) skewer-repl-types)))
  94. (value (or (cdr (assoc 'value log)) "<unspecified error>"))
  95. (output (propertize value 'font-lock-face face)))
  96. (when buffer
  97. (with-current-buffer buffer
  98. (save-excursion
  99. (goto-char (point-max))
  100. (forward-line 0)
  101. (backward-char)
  102. (insert (concat "\n" output (skewer-log-filename log))))))))
  103. (defcustom skewer-path-strip-level 1
  104. "Number of folders which will be stripped from url when discovering paths.
  105. Use this to limit path matching to files in your filesystem. You
  106. may want to add some folders to `compilation-search-path', so
  107. matched files can be found."
  108. :type 'number
  109. :group 'skewer)
  110. (defun skewer-repl-mode-compilation-shell-hook ()
  111. "Setup compilation shell minor mode for highlighting files"
  112. (let ((error-re (format "^[ ]*at https?://[^/]+/\\(?:[^/]+/\\)\\{%d\\}\\([^:?#]+\\)\\(?:[?#][^:]*\\)?:\\([[:digit:]]+\\)\\(?::\\([[:digit:]]+\\)\\)?$" skewer-path-strip-level)))
  113. (setq-local compilation-error-regexp-alist `((,error-re 1 2 3 2))))
  114. (compilation-shell-minor-mode 1))
  115. ;;;###autoload
  116. (defun skewer-repl--response-hook (response)
  117. "Catches all browser messages logging some to the REPL."
  118. (let ((type (cdr (assoc 'type response))))
  119. (when (member type '("log" "error"))
  120. (skewer-post-log response))))
  121. ;;;###autoload
  122. (defun skewer-repl ()
  123. "Start a JavaScript REPL to be evaluated in the visiting browser."
  124. (interactive)
  125. (when (not (get-buffer "*skewer-repl*"))
  126. (with-current-buffer (get-buffer-create "*skewer-repl*")
  127. (skewer-repl-mode)))
  128. (pop-to-buffer (get-buffer "*skewer-repl*")))
  129. (defun company-skewer-repl (command &optional arg &rest _args)
  130. "Skewerl REPL backend for company-mode.
  131. See `company-backends' for more info about COMMAND and ARG."
  132. (interactive (list 'interactive))
  133. (cl-case command
  134. (interactive
  135. (with-no-warnings ;; opportunistic use of company-mode
  136. (company-begin-backend 'company-skewer-repl)))
  137. (prefix (skewer-repl-company-prefix))
  138. (ignore-case t)
  139. (sorted t)
  140. (candidates (cons :async
  141. (lambda (callback)
  142. (skewer-repl-get-completions arg callback))))))
  143. (defun skewer-repl-get-completions (arg callback)
  144. "Get the completion list matching the prefix ARG.
  145. Evaluate CALLBACK with the completion candidates."
  146. (let* ((expression (skewer-repl--get-completion-expression arg))
  147. (pattern (if expression
  148. (substring arg (1+ (length expression)))
  149. arg)))
  150. (skewer-eval (or expression "window")
  151. (lambda (result)
  152. (cl-loop with value = (cdr (assoc 'value result))
  153. for key being the elements of value
  154. when expression
  155. collect (concat expression "." key) into results
  156. else
  157. collect key into results
  158. finally (funcall callback results)))
  159. :type "completions"
  160. :extra `((regexp . ,pattern)))))
  161. (defun skewer-repl--get-completion-expression (arg)
  162. "Get completion expression from ARG."
  163. (let ((components (split-string arg "\\.")))
  164. (when (> (length components) 1)
  165. (mapconcat #'identity (cl-subseq components 0 -1) "."))))
  166. (defun skewer-repl-company-prefix ()
  167. "Prefix for company."
  168. (and (eq major-mode 'skewer-repl-mode)
  169. (or (with-no-warnings ;; opportunistic use of company-mode
  170. (company-grab-symbol-cons "\\." 1))
  171. 'stop)))
  172. ;;;###autoload
  173. (eval-after-load 'skewer-mode
  174. '(progn
  175. (add-hook 'skewer-response-hook #'skewer-repl--response-hook)
  176. (add-hook 'skewer-repl-mode-hook #'skewer-repl-mode-compilation-shell-hook)
  177. (define-key skewer-mode-map (kbd "C-c C-z") #'skewer-repl)))
  178. (provide 'skewer-repl)
  179. ;;; skewer-repl.el ends here