aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJackson Ray Hamilton2019-03-08 16:29:02 -0800
committerJackson Ray Hamilton2019-04-08 22:48:21 -0700
commit8dae74236df2059b3df571f71733e2916ef55a58 (patch)
treef82e294c6f6959c97846da4d6ed8ad3416ce7f37
parent4d2b5bbfebc040ca477f1156b44989b4e19bbc3e (diff)
downloademacs-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.el216
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'.
1877Return `close' for a JSXClosingElement/JSXClosingFragment match,
1878return `self-closing' for some self-closing JSXOpeningElements,
1879else 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.
1891Assuming a JSXOpeningElement or a JSXOpeningFragment is
1892immediately before point, find a matching JSXClosingElement or
1893JSXClosingFragment, skipping over any nested JSXElements to find
1894the 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.
1943Look backward for a JSXElement that both starts before point and
1944also ends after point. That may be either a self-closing
1945JSXElement 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.
2001Text within an open/close tag pair may be JSXText. Temporarily
2002interrupt JSXText by JSXExpressionContainers, and terminate
2003JSXText when another JSXBoundaryElement is encountered. Despite
2004terminations, all JSXText will be identified once all the
2005JSXBoundaryElements within an outermost JSXElement’s tree have
2006been 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.
1840Disambiguate JSX from inequality operators and arrow functions by 2040Disambiguate 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