-
Notifications
You must be signed in to change notification settings - Fork 12
/
tsx-mode.el
401 lines (354 loc) · 13.3 KB
/
tsx-mode.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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
;;; tsx-mode.el --- a batteries-included major mode for TSX and friends -*- lexical-binding: t -*-
;;; Version: 3.1.0
;;; Author: Dan Orzechowski
;;; URL: https://github.com/orzechowskid/tsx-mode.el
;;; Package-Requires: ((emacs "29.0") (corfu "0.33") (coverlay "3.0.2") (css-in-js-mode "1.0.0") (origami "1.0"))
;;; Commentary:
;;; Code:
(require 'eglot)
(require 'seq)
(require 'treesit)
(require 'typescript-ts-mode)
(require 'coverlay)
(require 'css-in-js-mode)
;; origami depends on some now-deprecated cl functions and there's not much we
;; can do about that
(let ((byte-compile-warnings '((not cl-functions))))
(require 'origami))
(defgroup tsx-mode nil
"Major mode for JSX webapp files."
:group 'programming
:prefix "tsx-mode-")
(defcustom tsx-mode-code-fold-queries
'("((statement_block) @fold)"
"(call_expression (template_string) @fold)")
"List of tree-sitter queries for which to create Origami code-folding nodes.
Any node with a capture name of 'fold' will be used as a fold node."
:type '(repeat string)
:group 'tsx-mode)
(defcustom tsx-mode-use-code-coverage
t
"Whether or not to configure code-coverage overlays."
:type 'boolean
:group 'tsx-mode)
(defcustom tsx-mode-use-code-folding
t
"Whether or not to configure code-folding."
:type 'boolean
:group 'tsx-mode)
(defcustom tsx-mode-use-completion
t
"Whether or not to configure a code-completion frontend."
:type 'boolean
:group 'tsx-mode)
(defcustom tsx-mode-use-css-in-js
t
"Whether or not to configure support for CSS-in-JS."
:type 'boolean
:group 'tsx-mode)
(defcustom tsx-mode-use-jsx-auto-tags
nil
"Whether or not to use automatic JSX tags. When set to t, typing an open
angle-bracket ('<') will also insert '/>` to create a self-closing element tag.
Typing a close angle-bracket ('>') will, if point is inside a self-closing tag,
turn that tag into separate opening and closing tags."
:type 'boolean
:group 'tsx-mode)
(defcustom tsx-mode-use-key-commands
t
"Whether or not to configure custom keyboard shortcuts."
:type 'boolean
:group 'tsx-mode)
(defcustom tsx-mode-use-lsp
t
"Whether or not to configure a Language Server Protocol server and client."
:type 'boolean
:group 'tsx-mode)
(defcustom tsx-mode-use-own-documentation-strategy
t
"Whether or not to configure how help text is displayed."
:type 'boolean
:group 'tsx-mode)
(define-obsolete-variable-alias
'tsx-mode-tsx-auto-tags 'tsx-mode-use-jsx-auto-tags
"3.0.0")
(define-obsolete-variable-alias
'tsx-mode-fold-tree-queries 'tsx-mode-code-fold-queries
"3.0.0")
(defvar tsx-mode-abbrev-table nil
"Abbrev table in use in `tsx-mode' buffers.")
(define-abbrev-table 'tsx-mode-abbrev-table ())
(defvar-local tsx-mode-debug
nil
"Debug boolean for tsx-mode. Causes a bunch of helpful(?) text to be spammed
to *Messages*.")
(defun tsx-mode--debug (&rest args)
"Internal function.
Calls `message' with ARGS only when `tsx-mode-debug` is `t` in this buffer."
(when tsx-mode-debug
(apply 'message args)))
(defun tsx-mode--make-captures-tree (captures create start end)
"Internal function.
Make a tree from CAPTURES using CREATE that are between START and END. The
CAPTURES, as returned by `treesit-query-capture', may nest, thus this generates
a tree (or multiple) as required by Origami.
Returns a pair (regions . captures) with the remaining captures."
(let (regions break)
(while (and (not break) captures)
(let* ((elt (cdar captures))
(e-start (treesit-node-start elt))
(e-end (treesit-node-end elt)))
(if (>= e-start end)
;; the current capture is outside of our range, we're done
;; here
(setq break t)
(let* ((e-tree (tsx-mode--make-captures-tree
(cdr captures)
create
e-start
e-end))
(e-children (car e-tree)))
(setq captures (cdr e-tree))
(setq regions
(cons
(funcall create
e-start
e-end
0
e-children)
regions))))))
(cons (reverse regions) captures)))
(defun tsx-mode--origami-parser (create)
"Internal function.
Returns a parser for origami.el code folding. The parser must return a list of
fold nodes, where each fold node is created by invoking CREATE."
(lambda (content)
(let* ((query-result
(treesit-query-capture
(treesit-buffer-root-node 'tsx)
tsx-mode--code-fold-query))
(captures
;; query-result is a list of (name . node) cons cells; we only care
;; about the nodes with a capture name of "fold" (since other
;; captures with other names may be required to properly select the
;; area to fold)
(seq-filter
(lambda (el)
(string= (car el) "fold"))
query-result)))
(car
(tsx-mode--make-captures-tree
captures
create
(point-min)
(point-max))))))
(defun tsx-mode--tsx-self-closing-tag-at-point-p ()
"Internal function.
Return t if a self-closing tag is allowed to be inserted at point."
(save-excursion
(tsx-mode--debug
"checking current named node %s for self-closing tag support..."
(when (treesit-node-at (point) 'tsx t) (treesit-node-type (treesit-node-at (point) 'tsx t))))
(re-search-backward "[^\r\n[:space:]]" nil t)
(let* ((last-named-node
(treesit-node-at (point) 'tsx t))
(last-named-node-type
(when last-named-node (treesit-node-type last-named-node)))
(last-anon-node
(treesit-node-at (point) 'tsx nil))
(last-anon-node-type
(when last-anon-node (treesit-node-text last-anon-node))))
(tsx-mode--debug
"checking named node %s and anon node %s for self-closing tag support..."
last-named-node-type last-anon-node-type)
(or (string= last-anon-node-type "=>")
(string= last-anon-node-type "(")
(string= last-anon-node-type "?")
(string= last-anon-node-type ":")
(string= last-anon-node-type "[")
(string= last-anon-node-type ",")
(string= last-anon-node-type "=")
(eq last-named-node-type "jsx_opening_element")
(eq last-named-node-type "jsx_closing_element")
(eq last-named-node-type "jsx_fragment")
(eq last-named-node-type "jsx_expression")))))
(defun tsx-mode-tsx-maybe-insert-self-closing-tag ()
"When `tsx-mode-use-jsx-auto-tags' is non-nil, insert a self-closing element
instead of a plain '<' character (where it makes sense to)."
(interactive)
(if (or (bobp)
(and tsx-mode-use-jsx-auto-tags
(tsx-mode--tsx-self-closing-tag-at-point-p)))
(progn
(insert "</>")
(backward-char 2))
(insert "<")))
(defun tsx-mode-tsx-maybe-close-tag ()
"When `tsx-mode-use-jsx-auto-tags' is non-nil, turn the current self-closing tag
(if any) into a regular tag instead of inserting a plain '>' character."
(interactive)
(if (and tsx-mode-use-jsx-auto-tags
(tsx-mode--tsx-tag-convert-at-point-p))
(let* ((node-element-name
(save-excursion
(goto-char (treesit-node-start (treesit-node-at (point) 'tsx t)))
;; TODO: there should be a way to use [:word:] here right?
(re-search-forward "<\\([-a-zA-Z0-9$_.]+\\)" nil t)
(tsx-mode--debug "tag match: %s" (match-data))
(match-string 1)))
;; the above will calculate the name of a fragment as "/"
(str (format "></%s>" (if (string= node-element-name "/") "" node-element-name))))
(re-search-forward "/>" nil t)
(delete-char -2)
(insert str)
(backward-char (- (length str) 1)))
(insert ">")))
(defun tsx-mode--tsx-tag-convert-at-point-p ()
"Internal function.
Return t if a self-closing tag at point can be turned into an opening tag and a
closing tag."
(or
;; self-closing tags can be turned into regular tag sets
(eq "jsx_self_closing_element"
(treesit-node-type (treesit-node-at-pos (point) 'tsx t)))
(eq current-named-node-type "jsx_self_closing_element")
;; a "</>" string inserted via `tsx-mode-auto-tags' can be turned into
;; a set of JSX fragment tags
(save-excursion
(backward-char 1)
(looking-at-p "</>"))))
(defun tsx-mode--is-in-jsx-p (&optional maybe-pos)
"Internal function.
Return t if MAYBE-POS is inside a JSX-related tree-sitter node. MAYBE-POS
defaults to (`point') if not provided."
(let ((pos (or maybe-pos (point))))
(and-let* ((current-node (treesit-node-at pos 'tsx t))
(current-node-type (treesit-node-type current-node))
(is-jsx (or (eq current-node-type "jsx_expression")
(eq current-node-type "jsx_self_closing_element")
(eq current-node-type "jsx_text")))))))
(defun tsx-mode-coverage-toggle ()
"Toggles code-coverage overlay."
(interactive)
(when tsx-mode-use-code-coverage
(if coverlay-minor-mode
(coverlay-minor-mode 'toggle)
(let* ((package-json
(locate-dominating-file
(buffer-file-name (current-buffer))
"package.json"))
(base-path
(when package-json
(expand-file-name package-json)))
(coverage-file
(when base-path
(concat
base-path
"coverage/lcov.info"))))
(setq-local coverlay:base-path base-path)
(when (and coverage-file
(file-exists-p coverage-file)) ; can't handle nil
(coverlay-watch-file coverage-file))
(coverlay-minor-mode 'toggle)))))
(defun tsx-mode-fold-toggle-node (buffer point)
"Delegates to `origami-toggle-node' when `tsx-mode-use-code-folding' is
enabled."
(interactive (list (current-buffer) (point)))
(when tsx-mode-use-code-folding
(origami-toggle-node buffer point)))
(defun tsx-mode-fold-toggle-all-nodes (buffer)
"Delegates to `origami-toggle-all-nodes' when `tsx-mode-use-code-folding' is
enabled."
(interactive (list (current-buffer)))
(when tsx-mode-use-code-folding
(origami-toggle-all-nodes buffer)))
(defun tsx-mode--eglot-configure ()
"Internal function. Configures some eglot-related variables after that minor
mode has been enabled."
;; eglot adds its own capf to the head of `completion-at-point-functions'
;; which always returns something, meaning other capfs never get invoked.
;; that is not what we want for css-in-js-mode
(setq-local
completion-at-point-functions
'(css-in-js-mode--capf eglot-completion-at-point t))
;; eglot sets its own value for `eldoc-documentation-strategy' which causes
;; diagnostic messages to be hidden in favor of docstrings. show both instead
(when tsx-mode-use-own-documentation-strategy
(tsx-mode--debug "configuring eldoc-documentation-strategy")
(setq-local
eldoc-documentation-strategy
'eldoc-documentation-compose)))
;;;###autoload
(define-derived-mode
tsx-mode tsx-ts-mode "TSX"
"A batteries-included major mode for TSX and friends."
:group 'tsx-mode
(when tsx-mode-use-key-commands
(tsx-mode--debug "configuring keyboard shurtcuts")
(define-key
tsx-mode-map
(kbd "C-c t f")
#'tsx-mode-fold-toggle-node)
(define-key
tsx-mode-map
(kbd "C-c t F")
#'tsx-mode-fold-toggle-all-nodes)
(define-key
tsx-mode-map
(kbd "C-c t c")
#'tsx-mode-coverage-toggle)
(define-key
tsx-mode-map
(kbd "<")
#'tsx-mode-tsx-maybe-insert-self-closing-tag)
(define-key
tsx-mode-map
(kbd ">")
#'tsx-mode-tsx-maybe-close-tag))
(when tsx-mode-use-code-folding
(tsx-mode--debug "configuring code-folding")
(add-to-list
'origami-parser-alist
'(tsx-mode . tsx-mode--origami-parser))
(setq tsx-mode--code-fold-query
(treesit-query-compile
'tsx
(string-join
tsx-mode-code-fold-queries)))
(origami-mode t))
(when tsx-mode-use-code-coverage
(tsx-mode--debug "configuring code-coverage overlay")
(coverlay-minor-mode t))
(when tsx-mode-use-completion
(tsx-mode--debug "configuring corfu")
(corfu-mode t)
(corfu-popupinfo-mode t))
(when tsx-mode-use-css-in-js
(tsx-mode--debug "configuring css-in-js-mode")
(css-in-js-mode-fetch-shared-library)
(css-in-js-mode t))
(when tsx-mode-use-lsp
(add-hook
'eglot-managed-mode-hook
#'tsx-mode--eglot-configure
nil t)
(eglot-ensure))
;; some extra tsx indent rules until emacs gets patched
(setf
(cdr (assoc 'tsx treesit-simple-indent-rules))
(append
(cdr (assoc 'tsx treesit-simple-indent-rules))
'(
((parent-is "jsx_opening_element") parent typescript-ts-mode-indent-offset)
((parent-is "jsx_self_closing_element") parent typescript-ts-mode-indent-offset)
((parent-is "switch_body") parent-bol typescript-ts-mode-indent-offset)
((parent-is "export_clause") parent-bol typescript-ts-mode-indent-offset)
)))
(treesit-major-mode-setup))
;;;###autoload
(with-eval-after-load 'eglot
(add-to-list
'eglot-server-programs
'(tsx-mode "typescript-language-server" "--stdio")))
(provide 'tsx-mode)
;;; tsx-mode.el ends here