emacs-31 cc210dbf14d: Fix JSX tag angle-bracket pairing in tsx-ts-mode
Sean Whitton <[email protected]> Wed, 5 Aug 2026 06:23:08 -0400 (EDT)
| Newsgroups | gmane.emacs.diffs |
|---|---|
| Message-ID | <[email protected]> |
branch: emacs-31 commit cc210dbf14d5430b988dcf30662914012eced278 Author: Binbin Ye <[email protected]> Commit: Sean Whitton <[email protected]> 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. --- lisp/progmodes/typescript-ts-mode.el | 48 +++++++++++++++---------- test/lisp/progmodes/typescript-ts-mode-tests.el | 48 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 18 deletions(-) diff --git a/lisp/progmodes/typescript-ts-mode.el b/lisp/progmodes/typescript-ts-mode.el index dc951811bb3..83a996ad629 100644 --- a/lisp/progmodes/typescript-ts-mode.el +++ b/lisp/progmodes/typescript-ts-mode.el @@ -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 () diff --git a/test/lisp/progmodes/typescript-ts-mode-tests.el b/test/lisp/progmodes/typescript-ts-mode-tests.el index 977a0f44a20..1b065c409fe 100644 --- a/test/lisp/progmodes/typescript-ts-mode-tests.el +++ b/test/lisp/progmodes/typescript-ts-mode-tests.el @@ -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