diff options
| author | Jackson Ray Hamilton | 2019-04-08 22:40:51 -0700 |
|---|---|---|
| committer | Jackson Ray Hamilton | 2019-04-08 22:48:25 -0700 |
| commit | cf416d96c2d5db2079ed37927f0926fe0386e68a (patch) | |
| tree | d5154f874ac7966a83eb8f525ce92d3fe7de0090 | |
| parent | 7c3ffdaf4b17e9f93aa929fc9a5c154e8e68e5fb (diff) | |
| download | emacs-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.el | 109 |
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. |
| 1866 | For use by `syntax-propertize-extend-region-functions'." | 1906 | For 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. |
| 2028 | Text within an open/close tag pair may be JSXText. Temporarily | 2107 | Text 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. |