aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJackson Ray Hamilton2019-04-08 22:40:51 -0700
committerJackson Ray Hamilton2019-04-08 22:48:25 -0700
commitcf416d96c2d5db2079ed37927f0926fe0386e68a (patch)
treed5154f874ac7966a83eb8f525ce92d3fe7de0090
parent7c3ffdaf4b17e9f93aa929fc9a5c154e8e68e5fb (diff)
downloademacs-cf416d96c2d5db2079ed37927f0926fe0386e68a.tar.gz
emacs-cf416d96c2d5db2079ed37927f0926fe0386e68a.zip
Explain reasonings for JSX syntax support design decisions
* lisp/progmodes/js.el: Throughout the code, provide explanations for why JSX support was implemented in the way that it was; in particular, address the overlap between syntax-propertize-function, font-lock, and indentation (as requested by Stefan).
-rw-r--r--lisp/progmodes/js.el109
1 files changed, 109 insertions, 0 deletions
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el
index a1f5e694ede..535b70317a7 100644
--- a/lisp/progmodes/js.el
+++ b/lisp/progmodes/js.el
@@ -1536,6 +1536,25 @@ point of view of font-lock. It applies highlighting directly with
1536 ;; Matcher always "fails" 1536 ;; Matcher always "fails"
1537 nil) 1537 nil)
1538 1538
1539;; It wouldn’t be sufficient to font-lock JSX with mere regexps, since
1540;; a JSXElement may be nested inside a JS expression within the
1541;; boundaries of a parent JSXOpeningElement, and such a hierarchy
1542;; ought to be fontified like JSX, JS, and JSX respectively:
1543;;
1544;; <div attr={void(<div></div>) && void(0)}></div>
1545;;
1546;; <div attr={ ← JSX
1547;; void( ← JS
1548;; <div></div> ← JSX
1549;; ) && void(0) ← JS
1550;; }></div> ← JSX
1551;;
1552;; `js-syntax-propertize' unambiguously identifies JSX syntax,
1553;; including when it’s nested.
1554;;
1555;; Using a matcher function for each relevant part, retrieve match
1556;; data recorded as syntax properties for fontification.
1557
1539(defconst js-jsx--font-lock-keywords 1558(defconst js-jsx--font-lock-keywords
1540 `((js-jsx--match-tag-name 0 font-lock-function-name-face t) 1559 `((js-jsx--match-tag-name 0 font-lock-function-name-face t)
1541 (js-jsx--match-attribute-name 0 font-lock-variable-name-face t) 1560 (js-jsx--match-attribute-name 0 font-lock-variable-name-face t)
@@ -1861,6 +1880,27 @@ This performs fontification according to `js--class-styles'."
1861 "Check if STRING is a unary operator keyword in JavaScript." 1880 "Check if STRING is a unary operator keyword in JavaScript."
1862 (string-match-p js--unary-keyword-re string)) 1881 (string-match-p js--unary-keyword-re string))
1863 1882
1883;; Adding `syntax-multiline' text properties to JSX isn’t sufficient
1884;; to identify multiline JSX when first typing it. For instance, if
1885;; the user is typing a JSXOpeningElement for the first time…
1886;;
1887;; <div
1888;; ^ (point)
1889;;
1890;; …and the user inserts a line break after the tag name (before the
1891;; JSXOpeningElement starting on that line has been unambiguously
1892;; identified as such), then the `syntax-propertize' region won’t be
1893;; extended backwards to the start of the JSXOpeningElement:
1894;;
1895;; <div ← This line wasn’t JSX when last edited.
1896;; attr=""> ← Despite completing the JSX, the next
1897;; ^ `syntax-propertize' region wouldn’t magically
1898;; extend back a few lines.
1899;;
1900;; Therefore, to try and recover from this scenario, parse backward
1901;; from “>” to try and find the start of JSXBoundaryElements, and
1902;; extend the `syntax-propertize' region there.
1903
1864(defun js--syntax-propertize-extend-region (start end) 1904(defun js--syntax-propertize-extend-region (start end)
1865 "Extend the START-END region for propertization, if necessary. 1905 "Extend the START-END region for propertization, if necessary.
1866For use by `syntax-propertize-extend-region-functions'." 1906For use by `syntax-propertize-extend-region-functions'."
@@ -1903,6 +1943,23 @@ For use by `syntax-propertize-extend-region-functions'."
1903 (throw 'stop nil))))))) 1943 (throw 'stop nil)))))))
1904 (if new-start (cons new-start end)))) 1944 (if new-start (cons new-start end))))
1905 1945
1946;; When applying syntax properties, since `js-syntax-propertize' uses
1947;; `syntax-propertize-rules' to parse JSXBoundaryElements iteratively
1948;; and statelessly, whenever we exit such an element, we need to
1949;; determine the JSX depth. If >0, then we know we to apply syntax
1950;; properties to JSXText up until the next JSXBoundaryElement occurs.
1951;; But if the JSX depth is 0, then—importantly—we know to NOT parse
1952;; the following code as JSXText, rather propertize it as regular JS
1953;; as long as warranted.
1954;;
1955;; Also, when indenting code, we need to know if the code we’re trying
1956;; to indent is on the 2nd or later line of multiline JSX, in which
1957;; case the code is indented according to XML-like JSX conventions.
1958;;
1959;; For the aforementioned reasons, we find ourselves needing to
1960;; determine whether point is enclosed in JSX or not; and, if so,
1961;; where the JSX is. The following functions provide that knowledge.
1962
1906(defconst js-jsx--tag-start-re 1963(defconst js-jsx--tag-start-re
1907 (concat "\\(" js--dotted-name-re "\\)\\(?:" 1964 (concat "\\(" js--dotted-name-re "\\)\\(?:"
1908 ;; Whitespace is only necessary if an attribute implies JSX. 1965 ;; Whitespace is only necessary if an attribute implies JSX.
@@ -2004,6 +2061,24 @@ JSXElement or a JSXOpeningElement/JSXClosingElement pair."
2004 (let ((pos (save-excursion (js-jsx--enclosing-tag-pos)))) 2061 (let ((pos (save-excursion (js-jsx--enclosing-tag-pos))))
2005 (and pos (>= (point) (nth 1 pos))))) 2062 (and pos (>= (point) (nth 1 pos)))))
2006 2063
2064;; We implement `syntax-propertize-function' logic fully parsing JSX
2065;; in order to provide very accurate JSX indentation, even in the most
2066;; complex cases (e.g. to indent JSX within a JS expression within a
2067;; JSXAttribute…), as over the years users have requested this. Since
2068;; we find so much information during this parse, we later use some of
2069;; the useful bits for font-locking, too.
2070;;
2071;; Some extra effort is devoted to ensuring that no code which could
2072;; possibly be valid JS is ever misinterpreted as partial JSX, since
2073;; that would be regressive.
2074;;
2075;; We first parse trying to find the minimum number of components
2076;; necessary to unambiguously identify a JSXBoundaryElement, even if
2077;; it is a partial one. If a complete one is parsed, we move on to
2078;; parse any JSXText. When that’s terminated, we unwind back to the
2079;; `syntax-propertize-rules' loop so the next JSXBoundaryElement can
2080;; be parsed, if any, be it an opening or closing one.
2081
2007(defun js-jsx--text-range (beg end) 2082(defun js-jsx--text-range (beg end)
2008 "Identify JSXText within a “>/{/}/<” pair." 2083 "Identify JSXText within a “>/{/}/<” pair."
2009 (when (> (- end beg) 0) 2084 (when (> (- end beg) 0)
@@ -2023,6 +2098,10 @@ JSXElement or a JSXOpeningElement/JSXClosingElement pair."
2023 ;; JSXText determines JSXText context from earlier lines. 2098 ;; JSXText determines JSXText context from earlier lines.
2024 (put-text-property beg end 'syntax-multiline t))) 2099 (put-text-property beg end 'syntax-multiline t)))
2025 2100
2101;; In order to respect the end boundary `syntax-propertize-function'
2102;; sets, care is taken in the following functions to abort parsing
2103;; whenever that boundary is reached.
2104
2026(defun js-jsx--syntax-propertize-tag-text (end) 2105(defun js-jsx--syntax-propertize-tag-text (end)
2027 "Determine if JSXText is before END and propertize it. 2106 "Determine if JSXText is before END and propertize it.
2028Text within an open/close tag pair may be JSXText. Temporarily 2107Text within an open/close tag pair may be JSXText. Temporarily
@@ -2562,6 +2641,21 @@ current line is the \"=>\" token (of an arrow function)."
2562 (end-of-line) 2641 (end-of-line)
2563 (re-search-backward js--line-terminating-arrow-re from t))) 2642 (re-search-backward js--line-terminating-arrow-re from t)))
2564 2643
2644;; When indenting, we want to know if the line is…
2645;;
2646;; - within a multiline JSXElement, or
2647;; - within a string in a JSXBoundaryElement, or
2648;; - within JSXText, or
2649;; - within a JSXAttribute’s multiline JSXExpressionContainer.
2650;;
2651;; In these cases, special XML-like indentation rules for JSX apply.
2652;; If JS is nested within JSX, then indentation calculations may be
2653;; combined, such that JS indentation is “relative” to the JSX’s.
2654;;
2655;; Therefore, functions below provide such contextual information, and
2656;; `js--proper-indentation' may call itself once recursively in order
2657;; to finish calculating that “relative” JS+JSX indentation.
2658
2565(defun js-jsx--context () 2659(defun js-jsx--context ()
2566 "Determine JSX context and move to enclosing JSX." 2660 "Determine JSX context and move to enclosing JSX."
2567 (let ((pos (point)) 2661 (let ((pos (point))
@@ -4319,6 +4413,10 @@ their `mode-name' updates to show enabled syntax extensions."
4319 (interactive) 4413 (interactive)
4320 (setq-local js-jsx-syntax t)) 4414 (setq-local js-jsx-syntax t))
4321 4415
4416;; To make discovering and using syntax extensions features easier for
4417;; users (who might not read the docs), try to safely and
4418;; automatically enable syntax extensions based on heuristics.
4419
4322(defvar js-jsx-regexps 4420(defvar js-jsx-regexps
4323 (list "\\_<\\(?:var\\|let\\|const\\|import\\)\\_>.*?React") 4421 (list "\\_<\\(?:var\\|let\\|const\\|import\\)\\_>.*?React")
4324 "Regexps for detecting JSX in JavaScript buffers. 4422 "Regexps for detecting JSX in JavaScript buffers.
@@ -4444,6 +4542,17 @@ This function is intended for use in `after-change-functions'."
4444 ;;(syntax-propertize (point-max)) 4542 ;;(syntax-propertize (point-max))
4445 ) 4543 )
4446 4544
4545;; Since we made JSX support available and automatically-enabled in
4546;; the base `js-mode' (for ease of use), now `js-jsx-mode' simply
4547;; serves as one other interface to unconditionally enable JSX in
4548;; buffers, mostly for backwards-compatibility.
4549;;
4550;; Since it is probably more common for packages to integrate with
4551;; `js-mode' than with `js-jsx-mode', it is therefore probably
4552;; slightly better for users to use one of the many other methods for
4553;; enabling JSX syntax. But using `js-jsx-mode' can’t be that bad
4554;; either, so we won’t bother users with an obsoletion warning.
4555
4447;;;###autoload 4556;;;###autoload
4448(define-derived-mode js-jsx-mode js-mode "JavaScript" 4557(define-derived-mode js-jsx-mode js-mode "JavaScript"
4449 "Major mode for editing JavaScript+JSX. 4558 "Major mode for editing JavaScript+JSX.