diff options
| author | Jackson Ray Hamilton | 2019-02-17 00:38:01 -0800 |
|---|---|---|
| committer | Jackson Ray Hamilton | 2019-04-08 22:48:21 -0700 |
| commit | 52a3113b9beae6672c4bc981ee0c7bcc84ee58b5 (patch) | |
| tree | 8bf2e97587f88df0ff6f071ef241cf317380c13a | |
| parent | 6f535762df1f8f55faa36878d4a2a0a8b112f666 (diff) | |
| download | emacs-52a3113b9beae6672c4bc981ee0c7bcc84ee58b5.tar.gz emacs-52a3113b9beae6672c4bc981ee0c7bcc84ee58b5.zip | |
Add basic JSX font-locking
Font-lock JSX from the beginning of the buffer to the end. Tends to
break temporarily when editing lines, because the parser doesn’t yet
look backwards to determine if the end of a tag in the current range
starts before the range.
This also re-breaks some tests fixed by previous commits, as we begin
to take a different direction in our parsing code, looking for JSX,
rather than for non-JSX. The parsing code will eventually provide
information for indentation again.
* lisp/progmodes/js.el (js--dotted-captured-name-re)
(js-jsx--disambiguate-beginning-of-tag)
(js-jsx--disambiguate-end-of-tag, js-jsx--disambiguate-syntax):
Remove.
(js-jsx--font-lock-keywords): New variable.
(js--font-lock-keywords-3): Add JSX matchers.
(js-jsx--match-tag-name, js-jsx--match-attribute-name): New functions.
(js-jsx--syntax-propertize-tag): New function to aid in JSX
font-locking and eventually indentation.
(js-jsx--text-properties): New variable.
(js-syntax-propertize): Propertize JSX properly using
syntax-propertize-rules.
| -rw-r--r-- | lisp/progmodes/js.el | 216 |
1 files changed, 124 insertions, 92 deletions
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 4404ea04a03..1319fa19394 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el | |||
| @@ -82,10 +82,6 @@ | |||
| 82 | (concat js--name-re "\\(?:\\." js--name-re "\\)*") | 82 | (concat js--name-re "\\(?:\\." js--name-re "\\)*") |
| 83 | "Regexp matching a dot-separated sequence of JavaScript names.") | 83 | "Regexp matching a dot-separated sequence of JavaScript names.") |
| 84 | 84 | ||
| 85 | (defconst js--dotted-captured-name-re | ||
| 86 | (concat "\\(" js--name-re "\\)\\(?:\\." js--name-re "\\)*") | ||
| 87 | "Like `js--dotted-name-re', but capture the first name.") | ||
| 88 | |||
| 89 | (defconst js--cpp-name-re js--name-re | 85 | (defconst js--cpp-name-re js--name-re |
| 90 | "Regexp matching a C preprocessor name.") | 86 | "Regexp matching a C preprocessor name.") |
| 91 | 87 | ||
| @@ -1498,6 +1494,33 @@ point of view of font-lock. It applies highlighting directly with | |||
| 1498 | ;; Matcher always "fails" | 1494 | ;; Matcher always "fails" |
| 1499 | nil) | 1495 | nil) |
| 1500 | 1496 | ||
| 1497 | (defconst js-jsx--font-lock-keywords | ||
| 1498 | `((js-jsx--match-tag-name 0 font-lock-function-name-face t) | ||
| 1499 | (js-jsx--match-attribute-name 0 font-lock-variable-name-face t)) | ||
| 1500 | "JSX font lock faces.") | ||
| 1501 | |||
| 1502 | (defun js-jsx--match-tag-name (limit) | ||
| 1503 | "Match JSXBoundaryElement names, until LIMIT." | ||
| 1504 | (when js-jsx-syntax | ||
| 1505 | (let ((pos (next-single-char-property-change (point) 'js-jsx-tag-name nil limit)) | ||
| 1506 | value) | ||
| 1507 | (when (and pos (> pos (point))) | ||
| 1508 | (goto-char pos) | ||
| 1509 | (or (and (setq value (get-text-property pos 'js-jsx-tag-name)) | ||
| 1510 | (progn (set-match-data value) t)) | ||
| 1511 | (js-jsx--match-tag-name limit)))))) | ||
| 1512 | |||
| 1513 | (defun js-jsx--match-attribute-name (limit) | ||
| 1514 | "Match JSXAttribute names, until LIMIT." | ||
| 1515 | (when js-jsx-syntax | ||
| 1516 | (let ((pos (next-single-char-property-change (point) 'js-jsx-attribute-name nil limit)) | ||
| 1517 | value) | ||
| 1518 | (when (and pos (> pos (point))) | ||
| 1519 | (goto-char pos) | ||
| 1520 | (or (and (setq value (get-text-property pos 'js-jsx-attribute-name)) | ||
| 1521 | (progn (set-match-data value) t)) | ||
| 1522 | (js-jsx--match-attribute-name limit)))))) | ||
| 1523 | |||
| 1501 | (defconst js--font-lock-keywords-3 | 1524 | (defconst js--font-lock-keywords-3 |
| 1502 | `( | 1525 | `( |
| 1503 | ;; This goes before keywords-2 so it gets used preferentially | 1526 | ;; This goes before keywords-2 so it gets used preferentially |
| @@ -1609,7 +1632,10 @@ point of view of font-lock. It applies highlighting directly with | |||
| 1609 | (forward-symbol -1) | 1632 | (forward-symbol -1) |
| 1610 | (end-of-line)) | 1633 | (end-of-line)) |
| 1611 | '(end-of-line) | 1634 | '(end-of-line) |
| 1612 | '(0 font-lock-variable-name-face)))) | 1635 | '(0 font-lock-variable-name-face))) |
| 1636 | |||
| 1637 | ;; jsx (when enabled) | ||
| 1638 | ,@js-jsx--font-lock-keywords) | ||
| 1613 | "Level three font lock for `js-mode'.") | 1639 | "Level three font lock for `js-mode'.") |
| 1614 | 1640 | ||
| 1615 | (defun js--inside-pitem-p (pitem) | 1641 | (defun js--inside-pitem-p (pitem) |
| @@ -1743,94 +1769,100 @@ This performs fontification according to `js--class-styles'." | |||
| 1743 | "Check if STRING is a unary operator keyword in JavaScript." | 1769 | "Check if STRING is a unary operator keyword in JavaScript." |
| 1744 | (string-match-p js--unary-keyword-re string)) | 1770 | (string-match-p js--unary-keyword-re string)) |
| 1745 | 1771 | ||
| 1746 | (defun js-jsx--disambiguate-beginning-of-tag () | 1772 | (defun js-jsx--syntax-propertize-tag (end) |
| 1747 | "Parse enough to determine if a JSX tag starts here. | 1773 | "Determine if a JSXBoundaryElement is before END and propertize it. |
| 1748 | Disambiguate JSX from equality operators by testing for syntax | 1774 | Disambiguate JSX from inequality operators and arrow functions by |
| 1749 | only valid as JSX." | 1775 | testing for syntax only valid as JSX." |
| 1750 | ;; “</…” - a JSXClosingElement. | 1776 | (let ((tag-beg (1- (point))) tag-end (type 'open) |
| 1751 | ;; “<>” - a JSXOpeningFragment. | 1777 | name-beg name-match-data unambiguous |
| 1752 | (if (memq (char-after) '(?\/ ?\>)) t | 1778 | forward-sexp-function) ; Use Lisp version. |
| 1753 | (save-excursion | 1779 | (catch 'stop |
| 1754 | (skip-chars-forward " \t\n") | 1780 | (while (and (< (point) end) |
| 1755 | (and | 1781 | (progn (skip-chars-forward " \t\n" end) |
| 1756 | (looking-at js--dotted-captured-name-re) | 1782 | (< (point) end))) |
| 1757 | ;; Don’t match code like “if (i < await foo)” | 1783 | (cond |
| 1758 | (not (js--unary-keyword-p (match-string 1))) | 1784 | ((= (char-after) ?>) |
| 1759 | (progn | 1785 | (forward-char) |
| 1760 | (goto-char (match-end 0)) | 1786 | (setq unambiguous t |
| 1761 | (skip-chars-forward " \t\n") | 1787 | tag-end (point)) |
| 1762 | (or | 1788 | (throw 'stop nil)) |
| 1763 | ;; “>”, “/>” - tag enders. | 1789 | ;; Handle a JSXSpreadChild (“<Foo {...bar}”) or a |
| 1764 | ;; “{” - a JSXExpressionContainer. | 1790 | ;; JSXExpressionContainer as a JSXAttribute value |
| 1765 | (memq (char-after) '(?\> ?\/ ?\{)) | 1791 | ;; (“<Foo bar={…}”). Check this early in case continuing a |
| 1766 | ;; Check if a JSXAttribute follows. | 1792 | ;; JSXAttribute parse. |
| 1767 | (looking-at js--name-start-re))))))) | 1793 | ((and name-beg (= (char-after) ?{)) |
| 1768 | 1794 | (setq unambiguous t) ; JSXExpressionContainer post tag name ⇒ JSX | |
| 1769 | (defun js-jsx--disambiguate-end-of-tag () | 1795 | (let (expr-end) |
| 1770 | "Parse enough to determine if a JSX tag ends here. | 1796 | (condition-case nil |
| 1771 | Disambiguate JSX from equality operators by testing for syntax | 1797 | (save-excursion |
| 1772 | only valid as JSX, or extremely unlikely except as JSX." | 1798 | (forward-sexp) |
| 1773 | (save-excursion | 1799 | (setq expr-end (point))) |
| 1774 | (backward-char) | 1800 | (scan-error nil)) |
| 1775 | ;; “…/>” - a self-closing JSXOpeningElement. | 1801 | (forward-char) |
| 1776 | ;; “</>” - a JSXClosingFragment. | 1802 | (if (>= (point) end) (throw 'stop nil)) |
| 1777 | (if (= (char-before) ?/) t | 1803 | (skip-chars-forward " \t\n" end) |
| 1778 | (let (last-tag-or-attr-name last-non-unary-p) | 1804 | (if (>= (point) end) (throw 'stop nil)) |
| 1779 | (catch 'match | 1805 | (if (= (char-after) ?}) (forward-char) ; Shortcut to bail. |
| 1780 | (while t | 1806 | ;; Recursively propertize the JSXExpressionContainer’s |
| 1781 | (skip-chars-backward " \t\n") | 1807 | ;; expression. |
| 1782 | ;; Check if the end of a JSXAttribute value or | 1808 | (js-syntax-propertize (point) (if expr-end (min (1- expr-end) end) end)) |
| 1783 | ;; JSXExpressionContainer almost certainly precedes. | 1809 | ;; Exit the JSXExpressionContainer if that’s possible, |
| 1784 | ;; The only valid JS this misses is | 1810 | ;; else move to the end of the propertized area. |
| 1785 | ;; - {} > foo | 1811 | (goto-char (if expr-end (min expr-end end) end))))) |
| 1786 | ;; - "bar" > foo | 1812 | ((= (char-after) ?/) |
| 1787 | ;; which is no great loss, IMHO… | 1813 | ;; Assume a tag is an open tag until a slash is found, then |
| 1788 | (if (memq (char-before) '(?\} ?\" ?\' ?\`)) (throw 'match t) | 1814 | ;; figure out what type it actually is. |
| 1789 | (if (and last-tag-or-attr-name last-non-unary-p | 1815 | (if (eq type 'open) (setq type (if name-beg 'self-closing 'close))) |
| 1790 | ;; “<”, “</” - tag starters. | 1816 | (forward-char)) |
| 1791 | (memq (char-before) '(?\< ?\/))) | 1817 | ((looking-at js--dotted-name-re) |
| 1792 | ;; Leftmost name parsed was the name of a | 1818 | (if (not name-beg) |
| 1793 | ;; JSXOpeningElement. | 1819 | (progn |
| 1794 | (throw 'match t)) | 1820 | ;; Don’t match code like “if (i < await foo)” |
| 1795 | ;; Technically the dotted name could span multiple | 1821 | (if (js--unary-keyword-p (match-string 0)) (throw 'stop nil)) |
| 1796 | ;; lines, but dear God WHY?! Also, match greedily to | 1822 | ;; Save boundaries for later fontification after |
| 1797 | ;; ensure the entire name is valid. | 1823 | ;; unambiguously determining the code is JSX. |
| 1798 | (if (looking-back js--dotted-captured-name-re (point-at-bol) t) | 1824 | (setq name-beg (match-beginning 0) |
| 1799 | (if (and (setq last-non-unary-p (not (js--unary-keyword-p (match-string 1)))) | 1825 | name-match-data (match-data)) |
| 1800 | last-tag-or-attr-name) | 1826 | (goto-char (match-end 0))) |
| 1801 | ;; Valid (non-unary) name followed rightwards by | 1827 | (setq unambiguous t) ; Non-unary name followed by 2nd name ⇒ JSX |
| 1802 | ;; another name (any will do, including | 1828 | ;; Save JSXAttribute’s name’s match data for font-locking later. |
| 1803 | ;; keywords) is invalid JS, but valid JSX. | 1829 | (put-text-property (match-beginning 0) (1+ (match-beginning 0)) |
| 1804 | (throw 'match t) | 1830 | 'js-jsx-attribute-name (match-data)) |
| 1805 | ;; Remember match and skip backwards over it when | 1831 | (goto-char (match-end 0)) |
| 1806 | ;; it is the first matched name or the N+1th | 1832 | (if (>= (point) end) (throw 'stop nil)) |
| 1807 | ;; matched unary name (unary names on the left are | 1833 | (skip-chars-forward " \t\n" end) |
| 1808 | ;; still ambiguously JS or JSX, so keep parsing to | 1834 | (if (>= (point) end) (throw 'stop nil)) |
| 1809 | ;; disambiguate). | 1835 | ;; “=” is optional for null-valued JSXAttributes. |
| 1810 | (setq last-tag-or-attr-name (match-string 1)) | 1836 | (when (= (char-after) ?=) |
| 1811 | (goto-char (match-beginning 0))) | 1837 | (forward-char) |
| 1812 | ;; Nothing else to look for; give up parsing. | 1838 | (if (>= (point) end) (throw 'stop nil)) |
| 1813 | (throw 'match nil))))))))) | 1839 | (skip-chars-forward " \t\n" end) |
| 1814 | 1840 | (if (>= (point) end) (throw 'stop nil)) | |
| 1815 | (defun js-jsx--disambiguate-syntax (start end) | 1841 | ;; Skip over strings (if possible). Any |
| 1816 | "Figure out which ‘<’ and ‘>’ chars (from START to END) aren’t JSX. | 1842 | ;; JSXExpressionContainer here will be parsed in the |
| 1817 | 1843 | ;; next iteration of the loop. | |
| 1818 | Later, this info prevents ‘sgml-’ functions from treating some | 1844 | (when (memq (char-after) '(?\" ?\' ?\`)) |
| 1819 | ‘<’ and ‘>’ chars as parts of tokens of SGML tags — a good thing, | 1845 | (condition-case nil |
| 1820 | since they are serving their usual function as some JS equality | 1846 | (forward-sexp) |
| 1821 | operator or arrow function, instead." | 1847 | (scan-error (throw 'stop nil))))))) |
| 1822 | (goto-char start) | 1848 | ;; There is nothing more to check; this either isn’t JSX, or |
| 1823 | (while (re-search-forward "[<>]" end t) | 1849 | ;; the tag is incomplete. |
| 1824 | (unless (if (eq (char-before) ?<) (js-jsx--disambiguate-beginning-of-tag) | 1850 | (t (throw 'stop nil))))) |
| 1825 | (js-jsx--disambiguate-end-of-tag)) | 1851 | (when unambiguous |
| 1826 | ;; Inform sgml- functions that this >, >=, >>>, <, <=, <<<, or | 1852 | ;; Save JSXBoundaryElement’s name’s match data for font-locking. |
| 1827 | ;; => token is punctuation (and not an open or close parenthesis | 1853 | (if name-beg (put-text-property name-beg (1+ name-beg) 'js-jsx-tag-name name-match-data)) |
| 1828 | ;; as per usual in sgml-mode). | 1854 | ;; Mark beginning and end of tag for features like indentation. |
| 1829 | (put-text-property (1- (point)) (point) 'syntax-table '(1))))) | 1855 | (put-text-property tag-beg (1+ tag-beg) 'js-jsx-tag-beg type) |
| 1856 | (if tag-end (put-text-property (1- tag-end) tag-end 'js-jsx-tag-end tag-beg))))) | ||
| 1857 | |||
| 1858 | (defconst js-jsx--text-properties | ||
| 1859 | '(js-jsx-tag-beg nil js-jsx-tag-end nil js-jsx-tag-name nil js-jsx-attribute-name nil) | ||
| 1860 | "Plist of text properties added by `js-syntax-propertize'.") | ||
| 1830 | 1861 | ||
| 1831 | (defun js-syntax-propertize (start end) | 1862 | (defun js-syntax-propertize (start end) |
| 1832 | ;; JavaScript allows immediate regular expression objects, written /.../. | 1863 | ;; JavaScript allows immediate regular expression objects, written /.../. |
| 1833 | (goto-char start) | 1864 | (goto-char start) |
| 1865 | (if js-jsx-syntax (remove-text-properties start end js-jsx--text-properties)) | ||
| 1834 | (js-syntax-propertize-regexp end) | 1866 | (js-syntax-propertize-regexp end) |
| 1835 | (funcall | 1867 | (funcall |
| 1836 | (syntax-propertize-rules | 1868 | (syntax-propertize-rules |
| @@ -1854,9 +1886,9 @@ operator or arrow function, instead." | |||
| 1854 | (put-text-property (match-beginning 1) (match-end 1) | 1886 | (put-text-property (match-beginning 1) (match-end 1) |
| 1855 | 'syntax-table (string-to-syntax "\"/")) | 1887 | 'syntax-table (string-to-syntax "\"/")) |
| 1856 | (js-syntax-propertize-regexp end))))) | 1888 | (js-syntax-propertize-regexp end))))) |
| 1857 | ("\\`\\(#\\)!" (1 "< b"))) | 1889 | ("\\`\\(#\\)!" (1 "< b")) |
| 1858 | (point) end) | 1890 | ("<" (0 (ignore (if js-jsx-syntax (js-jsx--syntax-propertize-tag end)))))) |
| 1859 | (if js-jsx-syntax (js-jsx--disambiguate-syntax start end))) | 1891 | (point) end)) |
| 1860 | 1892 | ||
| 1861 | (defconst js--prettify-symbols-alist | 1893 | (defconst js--prettify-symbols-alist |
| 1862 | '(("=>" . ?⇒) | 1894 | '(("=>" . ?⇒) |