aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJackson Ray Hamilton2019-02-17 00:38:01 -0800
committerJackson Ray Hamilton2019-04-08 22:48:21 -0700
commit52a3113b9beae6672c4bc981ee0c7bcc84ee58b5 (patch)
tree8bf2e97587f88df0ff6f071ef241cf317380c13a
parent6f535762df1f8f55faa36878d4a2a0a8b112f666 (diff)
downloademacs-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.el216
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.
1748Disambiguate JSX from equality operators by testing for syntax 1774Disambiguate JSX from inequality operators and arrow functions by
1749only valid as JSX." 1775testing 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
1771Disambiguate JSX from equality operators by testing for syntax 1797 (save-excursion
1772only 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.
1818Later, 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
1820since they are serving their usual function as some JS equality 1846 (forward-sexp)
1821operator 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 '(("=>" . ?⇒)