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