diff options
| author | Jackson Ray Hamilton | 2019-03-08 16:29:02 -0800 |
|---|---|---|
| committer | Jackson Ray Hamilton | 2019-04-08 22:48:21 -0700 |
| commit | 8dae74236df2059b3df571f71733e2916ef55a58 (patch) | |
| tree | f82e294c6f6959c97846da4d6ed8ad3416ce7f37 | |
| parent | 4d2b5bbfebc040ca477f1156b44989b4e19bbc3e (diff) | |
| download | emacs-8dae74236df2059b3df571f71733e2916ef55a58.tar.gz emacs-8dae74236df2059b3df571f71733e2916ef55a58.zip | |
Propertize and font-lock JSXText and JSXExpressionContainers
This completes highlighting support for JSX, as requested in:
- https://github.com/mooz/js2-mode/issues/140
- https://github.com/mooz/js2-mode/issues/330
- https://github.com/mooz/js2-mode/issues/409
* lisp/progmodes/js.el (js--name-start-chars): Extract part of
js--name-start-re so it can be reused in another regexp.
(js--name-start-re): Use js--name-start-chars.
(js-jsx--font-lock-keywords): Use new matchers.
(js-jsx--match-text, js-jsx--match-expr): New matchers to remove
typical JS font-locking and extend the font-locked region,
respectively.
(js-jsx--tag-re, js-jsx--self-closing-re): New regexps matching JSX.
(js-jsx--matched-tag-type, js-jsx--matching-close-tag-pos)
(js-jsx--enclosing-curly-pos, js-jsx--enclosing-tag-pos)
(js-jsx--at-enclosing-tag-child-p): New functions for parsing and
analyzing JSX.
(js-jsx--text-range, js-jsx--syntax-propertize-tag-text): New
functions for propertizing JSXText.
(js-jsx--syntax-propertize-tag): Propertize JSXText children of tags.
(js-jsx--text-properties): Remove JSXText-related text properties when
repropertizing.
(js-mode): Extend the syntax-propertize region with
syntax-propertize-multiline; we are now adding the syntax-multiline
text property to buffer ranges that are JSXText to ensure the whole
multiline JSX construct is reidentified.
| -rw-r--r-- | lisp/progmodes/js.el | 216 |
1 files changed, 211 insertions, 5 deletions
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 7fb4bcc808a..220cf97fdca 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el | |||
| @@ -66,7 +66,10 @@ | |||
| 66 | 66 | ||
| 67 | ;;; Constants | 67 | ;;; Constants |
| 68 | 68 | ||
| 69 | (defconst js--name-start-re "[a-zA-Z_$]" | 69 | (defconst js--name-start-chars "a-zA-Z_$" |
| 70 | "Character class chars matching the start of a JavaScript identifier.") | ||
| 71 | |||
| 72 | (defconst js--name-start-re (concat "[" js--name-start-chars "]") | ||
| 70 | "Regexp matching the start of a JavaScript identifier, without grouping.") | 73 | "Regexp matching the start of a JavaScript identifier, without grouping.") |
| 71 | 74 | ||
| 72 | (defconst js--stmt-delim-chars "^;{}?:") | 75 | (defconst js--stmt-delim-chars "^;{}?:") |
| @@ -1497,8 +1500,10 @@ point of view of font-lock. It applies highlighting directly with | |||
| 1497 | (defconst js-jsx--font-lock-keywords | 1500 | (defconst js-jsx--font-lock-keywords |
| 1498 | `((js-jsx--match-tag-name 0 font-lock-function-name-face t) | 1501 | `((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) | 1502 | (js-jsx--match-attribute-name 0 font-lock-variable-name-face t) |
| 1503 | (js-jsx--match-text 0 'default t) ; “Undo” keyword fontification. | ||
| 1500 | (js-jsx--match-tag-beg) | 1504 | (js-jsx--match-tag-beg) |
| 1501 | (js-jsx--match-tag-end)) | 1505 | (js-jsx--match-tag-end) |
| 1506 | (js-jsx--match-expr)) | ||
| 1502 | "JSX font lock faces and multiline text properties.") | 1507 | "JSX font lock faces and multiline text properties.") |
| 1503 | 1508 | ||
| 1504 | (defun js-jsx--match-tag-name (limit) | 1509 | (defun js-jsx--match-tag-name (limit) |
| @@ -1523,6 +1528,19 @@ point of view of font-lock. It applies highlighting directly with | |||
| 1523 | (progn (set-match-data value) t)) | 1528 | (progn (set-match-data value) t)) |
| 1524 | (js-jsx--match-attribute-name limit)))))) | 1529 | (js-jsx--match-attribute-name limit)))))) |
| 1525 | 1530 | ||
| 1531 | (defun js-jsx--match-text (limit) | ||
| 1532 | "Match JSXText, until LIMIT." | ||
| 1533 | (when js-jsx-syntax | ||
| 1534 | (let ((pos (next-single-char-property-change (point) 'js-jsx-text nil limit)) | ||
| 1535 | value) | ||
| 1536 | (when (and pos (> pos (point))) | ||
| 1537 | (goto-char pos) | ||
| 1538 | (or (and (setq value (get-text-property pos 'js-jsx-text)) | ||
| 1539 | (progn (set-match-data value) | ||
| 1540 | (put-text-property (car value) (cadr value) 'font-lock-multiline t) | ||
| 1541 | t)) | ||
| 1542 | (js-jsx--match-text limit)))))) | ||
| 1543 | |||
| 1526 | (defun js-jsx--match-tag-beg (limit) | 1544 | (defun js-jsx--match-tag-beg (limit) |
| 1527 | "Match JSXBoundaryElements from start, until LIMIT." | 1545 | "Match JSXBoundaryElements from start, until LIMIT." |
| 1528 | (when js-jsx-syntax | 1546 | (when js-jsx-syntax |
| @@ -1545,6 +1563,17 @@ point of view of font-lock. It applies highlighting directly with | |||
| 1545 | (progn (put-text-property value pos 'font-lock-multiline t) t)) | 1563 | (progn (put-text-property value pos 'font-lock-multiline t) t)) |
| 1546 | (js-jsx--match-tag-end limit)))))) | 1564 | (js-jsx--match-tag-end limit)))))) |
| 1547 | 1565 | ||
| 1566 | (defun js-jsx--match-expr (limit) | ||
| 1567 | "Match JSXExpressionContainers, until LIMIT." | ||
| 1568 | (when js-jsx-syntax | ||
| 1569 | (let ((pos (next-single-char-property-change (point) 'js-jsx-expr nil limit)) | ||
| 1570 | value) | ||
| 1571 | (when (and pos (> pos (point))) | ||
| 1572 | (goto-char pos) | ||
| 1573 | (or (and (setq value (get-text-property pos 'js-jsx-expr)) | ||
| 1574 | (progn (put-text-property pos value 'font-lock-multiline t) t)) | ||
| 1575 | (js-jsx--match-expr limit)))))) | ||
| 1576 | |||
| 1548 | (defconst js--font-lock-keywords-3 | 1577 | (defconst js--font-lock-keywords-3 |
| 1549 | `( | 1578 | `( |
| 1550 | ;; This goes before keywords-2 so it gets used preferentially | 1579 | ;; This goes before keywords-2 so it gets used preferentially |
| @@ -1835,6 +1864,177 @@ For use by `syntax-propertize-extend-region-functions'." | |||
| 1835 | (throw 'stop nil))))))) | 1864 | (throw 'stop nil))))))) |
| 1836 | (if new-start (cons new-start end)))) | 1865 | (if new-start (cons new-start end)))) |
| 1837 | 1866 | ||
| 1867 | (defconst js-jsx--tag-re | ||
| 1868 | (concat "<\\s-*\\(" | ||
| 1869 | "[/>]" ; JSXClosingElement, or JSXOpeningFragment, or JSXClosingFragment | ||
| 1870 | "\\|" | ||
| 1871 | js--dotted-name-re "\\s-*[" js--name-start-chars "{/>]" ; JSXOpeningElement | ||
| 1872 | "\\)") | ||
| 1873 | "Regexp unambiguously matching a JSXBoundaryElement.") | ||
| 1874 | |||
| 1875 | (defun js-jsx--matched-tag-type () | ||
| 1876 | "Determine the tag type of the last match to `js-jsx--tag-re'. | ||
| 1877 | Return `close' for a JSXClosingElement/JSXClosingFragment match, | ||
| 1878 | return `self-closing' for some self-closing JSXOpeningElements, | ||
| 1879 | else return `other'." | ||
| 1880 | (let ((chars (vconcat (match-string 1)))) | ||
| 1881 | (cond | ||
| 1882 | ((= (aref chars 0) ?/) 'close) | ||
| 1883 | ((= (aref chars (1- (length chars))) ?/) 'self-closing) | ||
| 1884 | (t 'other)))) | ||
| 1885 | |||
| 1886 | (defconst js-jsx--self-closing-re "/\\s-*>" | ||
| 1887 | "Regexp matching the end of a self-closing JSXOpeningElement.") | ||
| 1888 | |||
| 1889 | (defun js-jsx--matching-close-tag-pos () | ||
| 1890 | "Return position of the closer of the opener before point. | ||
| 1891 | Assuming a JSXOpeningElement or a JSXOpeningFragment is | ||
| 1892 | immediately before point, find a matching JSXClosingElement or | ||
| 1893 | JSXClosingFragment, skipping over any nested JSXElements to find | ||
| 1894 | the match. Return nil if a match can’t be found." | ||
| 1895 | (let ((tag-stack 1) self-closing-pos type) | ||
| 1896 | (catch 'stop | ||
| 1897 | (while (re-search-forward js-jsx--tag-re nil t) | ||
| 1898 | (setq type (js-jsx--matched-tag-type)) | ||
| 1899 | ;; Balance the total of self-closing tags that we subtract | ||
| 1900 | ;; from the stack, ignoring those tags which are never added | ||
| 1901 | ;; to the stack (see below). | ||
| 1902 | (unless (eq type 'self-closing) | ||
| 1903 | (when (and self-closing-pos (> (point) self-closing-pos)) | ||
| 1904 | (setq tag-stack (1- tag-stack)))) | ||
| 1905 | (if (eq type 'close) | ||
| 1906 | (progn | ||
| 1907 | (setq tag-stack (1- tag-stack)) | ||
| 1908 | (when (= tag-stack 0) | ||
| 1909 | (throw 'stop (match-beginning 0)))) | ||
| 1910 | ;; Tags that we know are self-closing aren’t added to the | ||
| 1911 | ;; stack at all, because we only close the ones that we have | ||
| 1912 | ;; anticipated after moving past those anticipated tags’ | ||
| 1913 | ;; ends, and if a self-closing tag is the first tag we | ||
| 1914 | ;; encounter in this loop, then it will never be anticipated | ||
| 1915 | ;; (due to an optimization where we sometimes can avoid | ||
| 1916 | ;; looking for self-closing tags). | ||
| 1917 | (unless (eq type 'self-closing) | ||
| 1918 | (setq tag-stack (1+ tag-stack)))) | ||
| 1919 | ;; Don’t needlessly recalculate. | ||
| 1920 | (unless (and self-closing-pos (<= (point) self-closing-pos)) | ||
| 1921 | (setq self-closing-pos nil) ; Reset if recalculating. | ||
| 1922 | (save-excursion | ||
| 1923 | ;; Anticipate a self-closing tag that we should make sure | ||
| 1924 | ;; to subtract from the tag stack once we move past its | ||
| 1925 | ;; end; we might might miss the end otherwise, due to the | ||
| 1926 | ;; regexp-matching method we use to detect tags. | ||
| 1927 | (when (re-search-forward js-jsx--self-closing-re nil t) | ||
| 1928 | (setq self-closing-pos (match-beginning 0))))))))) | ||
| 1929 | |||
| 1930 | (defun js-jsx--enclosing-curly-pos () | ||
| 1931 | "Return position of enclosing “{” in a “{/}” pair about point." | ||
| 1932 | (let ((parens (reverse (nth 9 (syntax-ppss)))) paren-pos curly-pos) | ||
| 1933 | (while | ||
| 1934 | (and | ||
| 1935 | (setq paren-pos (car parens)) | ||
| 1936 | (not (when (= (char-after paren-pos) ?{) | ||
| 1937 | (setq curly-pos paren-pos))) | ||
| 1938 | (setq parens (cdr parens)))) | ||
| 1939 | curly-pos)) | ||
| 1940 | |||
| 1941 | (defun js-jsx--enclosing-tag-pos () | ||
| 1942 | "Return beginning and end of a JSXElement about point. | ||
| 1943 | Look backward for a JSXElement that both starts before point and | ||
| 1944 | also ends after point. That may be either a self-closing | ||
| 1945 | JSXElement or a JSXOpeningElement/JSXClosingElement pair." | ||
| 1946 | (let ((start (point)) | ||
| 1947 | (curly-pos (save-excursion (js-jsx--enclosing-curly-pos))) | ||
| 1948 | tag-beg tag-beg-pos tag-end-pos close-tag-pos) | ||
| 1949 | (while | ||
| 1950 | (and | ||
| 1951 | (setq tag-beg (js--backward-text-property 'js-jsx-tag-beg)) | ||
| 1952 | (progn | ||
| 1953 | (setq tag-beg-pos (point) | ||
| 1954 | tag-end-pos (cdr tag-beg)) | ||
| 1955 | (not | ||
| 1956 | (or | ||
| 1957 | (and (eq (car tag-beg) 'self-closing) | ||
| 1958 | (< start tag-end-pos)) | ||
| 1959 | (and (eq (car tag-beg) 'open) | ||
| 1960 | (save-excursion | ||
| 1961 | (goto-char tag-end-pos) | ||
| 1962 | (setq close-tag-pos (js-jsx--matching-close-tag-pos)) | ||
| 1963 | ;; The JSXOpeningElement may either be unclosed, | ||
| 1964 | ;; else the closure must occur after the start | ||
| 1965 | ;; point (otherwise, a miscellaneous previous | ||
| 1966 | ;; JSXOpeningElement has been found, and we should | ||
| 1967 | ;; keep looking back for an enclosing one). | ||
| 1968 | (or (not close-tag-pos) (< start close-tag-pos)))))))) | ||
| 1969 | ;; Don’t return the last tag pos (if any; it wasn’t enclosing). | ||
| 1970 | (setq tag-beg nil)) | ||
| 1971 | (and tag-beg | ||
| 1972 | (or (not curly-pos) (> tag-beg-pos curly-pos)) | ||
| 1973 | (cons tag-beg-pos tag-end-pos)))) | ||
| 1974 | |||
| 1975 | (defun js-jsx--at-enclosing-tag-child-p () | ||
| 1976 | "Return t if point is at an enclosing tag’s child." | ||
| 1977 | (let ((pos (save-excursion (js-jsx--enclosing-tag-pos)))) | ||
| 1978 | (and pos (>= (point) (cdr pos))))) | ||
| 1979 | |||
| 1980 | (defun js-jsx--text-range (beg end) | ||
| 1981 | "Identify JSXText within a “>/{/}/<” pair." | ||
| 1982 | (when (> (- end beg) 0) | ||
| 1983 | (save-excursion | ||
| 1984 | (goto-char beg) | ||
| 1985 | (while (and (skip-chars-forward " \t\n" end) (< (point) end)) | ||
| 1986 | ;; Comments and string quotes don’t serve their usual | ||
| 1987 | ;; syntactic roles in JSXText; make them plain punctuation to | ||
| 1988 | ;; negate those roles. | ||
| 1989 | (when (or (= (char-after) ?/) ; comment | ||
| 1990 | (= (syntax-class (syntax-after (point))) 7)) ; string quote | ||
| 1991 | (put-text-property (point) (1+ (point)) 'syntax-table '(1))) | ||
| 1992 | (forward-char))) | ||
| 1993 | ;; Mark JSXText so it can be font-locked as non-keywords. | ||
| 1994 | (put-text-property beg (1+ beg) 'js-jsx-text (list beg end (current-buffer))) | ||
| 1995 | ;; Ensure future propertization beginning from within the | ||
| 1996 | ;; JSXText determines JSXText context from earlier lines. | ||
| 1997 | (put-text-property beg end 'syntax-multiline t))) | ||
| 1998 | |||
| 1999 | (defun js-jsx--syntax-propertize-tag-text (end) | ||
| 2000 | "Determine if JSXText is before END and propertize it. | ||
| 2001 | Text within an open/close tag pair may be JSXText. Temporarily | ||
| 2002 | interrupt JSXText by JSXExpressionContainers, and terminate | ||
| 2003 | JSXText when another JSXBoundaryElement is encountered. Despite | ||
| 2004 | terminations, all JSXText will be identified once all the | ||
| 2005 | JSXBoundaryElements within an outermost JSXElement’s tree have | ||
| 2006 | been propertized." | ||
| 2007 | (let ((text-beg (point)) | ||
| 2008 | forward-sexp-function) ; Use Lisp version. | ||
| 2009 | (catch 'stop | ||
| 2010 | (while (re-search-forward "[{<]" end t) | ||
| 2011 | (js-jsx--text-range text-beg (1- (point))) | ||
| 2012 | (cond | ||
| 2013 | ((= (char-before) ?{) | ||
| 2014 | (let (expr-beg expr-end) | ||
| 2015 | (condition-case nil | ||
| 2016 | (save-excursion | ||
| 2017 | (backward-char) | ||
| 2018 | (setq expr-beg (point)) | ||
| 2019 | (forward-sexp) | ||
| 2020 | (setq expr-end (point))) | ||
| 2021 | (scan-error nil)) | ||
| 2022 | ;; Recursively propertize the JSXExpressionContainer’s | ||
| 2023 | ;; (possibly-incomplete) expression. | ||
| 2024 | (js-syntax-propertize (1+ expr-beg) (if expr-end (min (1- expr-end) end) end)) | ||
| 2025 | ;; Ensure future propertization beginning from within the | ||
| 2026 | ;; (possibly-incomplete) expression can determine JSXText | ||
| 2027 | ;; context from earlier lines. | ||
| 2028 | (put-text-property expr-beg (1+ expr-beg) 'js-jsx-expr (or expr-end end)) ; font-lock | ||
| 2029 | (put-text-property expr-beg (if expr-end (min expr-end end) end) 'syntax-multiline t) ; syntax-propertize | ||
| 2030 | ;; Exit the JSXExpressionContainer if that’s possible, | ||
| 2031 | ;; else move to the end of the propertized area. | ||
| 2032 | (goto-char (if expr-end (min expr-end end) end)))) | ||
| 2033 | ((= (char-before) ?<) | ||
| 2034 | (backward-char) ; Ensure the next tag can be propertized. | ||
| 2035 | (throw 'stop nil))) | ||
| 2036 | (setq text-beg (point)))))) | ||
| 2037 | |||
| 1838 | (defun js-jsx--syntax-propertize-tag (end) | 2038 | (defun js-jsx--syntax-propertize-tag (end) |
| 1839 | "Determine if a JSXBoundaryElement is before END and propertize it. | 2039 | "Determine if a JSXBoundaryElement is before END and propertize it. |
| 1840 | Disambiguate JSX from inequality operators and arrow functions by | 2040 | Disambiguate JSX from inequality operators and arrow functions by |
| @@ -1916,12 +2116,16 @@ testing for syntax only valid as JSX." | |||
| 1916 | (when unambiguous | 2116 | (when unambiguous |
| 1917 | ;; Save JSXBoundaryElement’s name’s match data for font-locking. | 2117 | ;; Save JSXBoundaryElement’s name’s match data for font-locking. |
| 1918 | (if name-beg (put-text-property name-beg (1+ name-beg) 'js-jsx-tag-name name-match-data)) | 2118 | (if name-beg (put-text-property name-beg (1+ name-beg) 'js-jsx-tag-name name-match-data)) |
| 1919 | ;; Mark beginning and end of tag for features like indentation. | 2119 | ;; Mark beginning and end of tag for font-locking. |
| 1920 | (put-text-property tag-beg (1+ tag-beg) 'js-jsx-tag-beg (cons type (point))) | 2120 | (put-text-property tag-beg (1+ tag-beg) 'js-jsx-tag-beg (cons type (point))) |
| 1921 | (put-text-property (point) (1+ (point)) 'js-jsx-tag-end tag-beg)))) | 2121 | (put-text-property (point) (1+ (point)) 'js-jsx-tag-end tag-beg)) |
| 2122 | (if (js-jsx--at-enclosing-tag-child-p) (js-jsx--syntax-propertize-tag-text end)))) | ||
| 1922 | 2123 | ||
| 1923 | (defconst js-jsx--text-properties | 2124 | (defconst js-jsx--text-properties |
| 1924 | '(js-jsx-tag-beg nil js-jsx-tag-end nil js-jsx-tag-name nil js-jsx-attribute-name nil) | 2125 | (list |
| 2126 | 'js-jsx-tag-beg nil 'js-jsx-tag-end nil | ||
| 2127 | 'js-jsx-tag-name nil 'js-jsx-attribute-name nil | ||
| 2128 | 'js-jsx-text nil 'js-jsx-expr nil) | ||
| 1925 | "Plist of text properties added by `js-syntax-propertize'.") | 2129 | "Plist of text properties added by `js-syntax-propertize'.") |
| 1926 | 2130 | ||
| 1927 | (defun js-syntax-propertize (start end) | 2131 | (defun js-syntax-propertize (start end) |
| @@ -4011,6 +4215,8 @@ If one hasn't been set, or if it's stale, prompt for a new one." | |||
| 4011 | . js-font-lock-syntactic-face-function))) | 4215 | . js-font-lock-syntactic-face-function))) |
| 4012 | (setq-local syntax-propertize-function #'js-syntax-propertize) | 4216 | (setq-local syntax-propertize-function #'js-syntax-propertize) |
| 4013 | (add-hook 'syntax-propertize-extend-region-functions | 4217 | (add-hook 'syntax-propertize-extend-region-functions |
| 4218 | #'syntax-propertize-multiline 'append 'local) | ||
| 4219 | (add-hook 'syntax-propertize-extend-region-functions | ||
| 4014 | #'js--syntax-propertize-extend-region 'append 'local) | 4220 | #'js--syntax-propertize-extend-region 'append 'local) |
| 4015 | (setq-local prettify-symbols-alist js--prettify-symbols-alist) | 4221 | (setq-local prettify-symbols-alist js--prettify-symbols-alist) |
| 4016 | 4222 | ||