Fix JSX tag angle-bracket pairing in tsx-ts-mode

* lisp/progmodes/typescript-ts-mode.el (tsx-ts--s-p-query):
Capture 'jsx_self_closing_element' as well.
(tsx-ts--syntax-propertize-captures): For JSX tag elements, give
only the leading '<' and trailing '>' matching-pair syntax and
leave the interior untouched; for 'jsx_text', neutralize
balanced pair characters to punctuation (bug#81460).
* test/lisp/progmodes/typescript-ts-mode-tests.el
(tsx-ts-mode-test-jsx-tag-syntax-propertize): New test.
This commit is contained in:
Binbin Ye 2026-07-23 20:38:58 +08:00 committed by Sean Whitton
parent 8b521b98ed
commit cc210dbf14
2 changed files with 78 additions and 18 deletions

View file

@ -824,7 +824,8 @@ at least 3 (which is the default value)."
'(((regex pattern: (regex_pattern) @regexp))
((jsx_text) @jsx)
((jsx_opening_element) @jsx)
((jsx_closing_element) @jsx)))))
((jsx_closing_element) @jsx)
((jsx_self_closing_element) @jsx)))))
(defun typescript-ts--syntax-propertize (beg end)
(let ((captures (treesit-query-capture 'typescript typescript-ts--s-p-query beg end)))
@ -845,24 +846,35 @@ at least 3 (which is the default value)."
(incf ne)
(put-text-property ns (1+ ns) 'syntax-table syntax)
(put-text-property (1- ne) ne 'syntax-table syntax)))
;; We put punctuation syntax on all the balanced pair
;; characters so they don't mess up syntax-ppss. We can't put
;; string syntax on the whole thing because a) it doesn't work
;; if the text is one character long, and b) it interferes
;; forward/backward-sexp.
('jsx
(save-excursion
(goto-char ns)
(while (re-search-forward (rx (or "{" "}" "[" "]"
"(" ")" "<" ">"))
ne t)
(put-text-property
(match-beginning 0) (match-end 0)
'syntax-table (string-to-syntax
(cond
((equal (match-string 0) "<") "(>")
((equal (match-string 0) ">") ")<")
(t ".")))))))))))
(if (member (treesit-node-type node)
'("jsx_opening_element"
"jsx_closing_element"
"jsx_self_closing_element"))
;; A JSX tag's only real delimiters are its own outermost
;; '<' and '>'. Mark just those two as a matching pair and
;; leave the interior alone: attribute values are '{...}'
;; expressions of ordinary code (possibly with nested JSX)
;; whose brackets must keep their normal syntax so they
;; nest, match and highlight correctly.
(progn
(put-text-property ns (1+ ns)
'syntax-table (string-to-syntax "(>"))
(put-text-property (1- ne) ne
'syntax-table (string-to-syntax ")<")))
;; jsx_text is raw text with no nesting to preserve, so
;; neutralize its balanced-pair characters to punctuation;
;; otherwise stray or unbalanced brackets in the text would
;; confuse syntax-ppss.
(save-excursion
(goto-char ns)
(while (re-search-forward (rx (or "{" "}" "[" "]"
"(" ")" "<" ">"))
ne t)
(put-text-property
(match-beginning 0) (match-end 0)
'syntax-table (string-to-syntax "."))))))))))
;;;###autoload
(defun tsx-ts-mode-maybe ()

View file

@ -28,5 +28,53 @@
(treesit-ready-p 'tsx)))
(ert-test-erts-file (ert-resource-file "indent.erts")))
(ert-deftest tsx-ts-mode-test-jsx-tag-syntax-propertize ()
"Every JSX tag's angle brackets must pair with that tag's own bounds.
Make sure jsx element's `<' pair with the closing `>' correctly instead
of the wrong `>' from its attributes contained an arrow function `=>'."
(skip-unless (treesit-ready-p 'tsx))
(dolist (src '(;; A fragment, arrow-function attributes, and an arrow
;; that returns a nested `<div></div>' element -- all
;; inside the opening tag's range.
"const A = () => (
<>
<Component
onClick={() => {}}
panel={() => {
return <div></div>;
}}
>
{children}
</Component>
</>
);
"
;; A self-closing element nested inside an attribute
;; expression.
"const B = () => (
<Component slot={<Panel onClick={() => cb()}/>}>
{children}
</Component>
);
"))
(with-temp-buffer
(insert src)
(tsx-ts-mode)
(syntax-propertize (point-max))
(pcase-dolist (`(,_ . ,node)
(treesit-query-capture
'tsx '(((jsx_opening_element) @el)
((jsx_closing_element) @el)
((jsx_self_closing_element) @el))))
(let ((ns (treesit-node-start node))
(ne (treesit-node-end node)))
(should (eq (char-after ns) ?<))
(should (eq (char-before ne) ?>))
;; Forward from the tag's `<' lands exactly after its own
;; `>', and backward round-trips to the `<'.
(should (= (scan-sexps ns 1) ne))
(should (= (scan-sexps ne -1) ns)))))))
(provide 'typescript-ts-mode-tests)
;;; typescript-ts-mode-tests.el ends here