aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJackson Ray Hamilton2019-03-23 14:22:35 -0700
committerJackson Ray Hamilton2019-04-08 22:48:22 -0700
commit1a1ef2851844a9ae2edcfe0346fc457e90c24bc7 (patch)
treea4ed2aba89154761ea3a80066d11fee820d8f683
parent2bedd23358d2d7378eec78d526ba1435d3b4d122 (diff)
downloademacs-1a1ef2851844a9ae2edcfe0346fc457e90c24bc7.tar.gz
emacs-1a1ef2851844a9ae2edcfe0346fc457e90c24bc7.zip
Indent JSX as parsed in a JS context
Fixes the following issues (and re-fixes indentation issues initially fixed but later re-broken by previous commits in the process of adding comprehensive JSX support): - https://github.com/mooz/js2-mode/issues/389#issuecomment-390766873 - https://github.com/mooz/js2-mode/issues/482 - Bug#32158 - https://github.com/mooz/js2-mode/issues/462 Previously, we delegated to sgml-mode functions for JSX indentation. However, there were some problems with this approach: - sgml-mode does not anticipate tags inside attributes when indenting, which compromises JSX indentation inside JSXExpressionContainers inside JSXAttributes. - In previous iterations to provide comprehensive JSX support, it proved tedious to disambiguate “<” and “>” as JS inequality operators and arrow functions from opening and closing angle brackets as part of SGML tags. That code evolved into a more complete JSX parsing implementation for syntax-propertize rules for font-locking, discarding the superfluous “<”/“>” disambiguation in anticipation of using the improved JSX analysis for indentation. - Using sgml-mode functions, we controlled JSX indentation using SGML variables. However, JSX is a different thing than SGML; referencing SGML in JS was a leaky abstraction. To resolve these issues, use the text properties added by the JSX syntax-propertize code to determine the boundaries of various aspects of JSX syntax, and reimplement the sgml-mode indentation code in js-mode with better respect to JSX indentation conventions. * lisp/progmodes/js.el (js-jsx-attribute-offset): New variable to provide a way for users to still control JSX attribute offsets as they could with sgml-attribute-offset before. The value of this feature is dubious IMO, but it’s trivial to keep it, so let’s do it just in case. (js-jsx--goto-outermost-enclosing-curly): New function. (js-jsx--enclosing-tag-pos): Refactor to be unbounded by curlies, so this function can be used to find JSXExpressionContainers within JSX. Fix bug where an enclosing JSXElement couldn’t be found when point was at the start of its JSXClosingElement. Return the JSXClosingElement’s position as well, so the JSXClosingElement can be indentified when indenting and be indented like the matching JSXOpeningElement. (js-jsx--at-enclosing-tag-child-p): js-jsx--enclosing-tag-pos now returns a list rather than a cons, so retrieve the JSXOpeningElement’s end position from a list. (js-jsx--context, js-jsx--indenting): New function and variable. (js-jsx--indentation): New function replacing the prior js-jsx--indent* functions and js-jsx-indent-line’s implementation. Use the JSX parsing performed in a JS context to more accurately calculate JSX indentation than by delegating to sgml-mode functions. (js--proper-indentation): Use js-jsx--indentation as yet another type of indentation. (js-jsx--as-sgml, js-jsx--outermost-enclosing-tag-pos) (js-jsx--indentation-type, js-jsx--indent-line-in-expression) (js-jsx--indent-n+1th-line): Remove obsolete functions. (js-jsx-indent-line): Refactor nearly-obsolete function to behave the same as it usually would before these changes, without respect to the binding of js-jsx-syntax. (js-jsx-mode): Remove obsolete documentation about the use of SGML variables to control indentation, and don’t bind indent-line-function any more, because it is no longer necessary given the new implementation of js-jsx-indent-line.
-rw-r--r--lisp/progmodes/js.el307
1 files changed, 165 insertions, 142 deletions
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el
index 220cf97fdca..af83e04df42 100644
--- a/lisp/progmodes/js.el
+++ b/lisp/progmodes/js.el
@@ -584,6 +584,29 @@ be buffer-local when in `js-jsx-mode'."
584 :safe 'booleanp 584 :safe 'booleanp
585 :group 'js) 585 :group 'js)
586 586
587(defcustom js-jsx-attribute-offset 0
588 "Specifies a delta for JSXAttribute indentation.
589
590Let `js-indent-level' be 2. When this variable is also set to 0,
591JSXAttribute indentation looks like this:
592
593 <element
594 attribute=\"value\">
595 </element>
596
597Alternatively, when this variable is also set to 2, JSXAttribute
598indentation looks like this:
599
600 <element
601 attribute=\"value\">
602 </element>
603
604This variable is like `sgml-attribute-offset'."
605 :version "27.1"
606 :type 'integer
607 :safe 'integerp
608 :group 'js)
609
587;;; KeyMap 610;;; KeyMap
588 611
589(defvar js-mode-map 612(defvar js-mode-map
@@ -1938,14 +1961,21 @@ the match. Return nil if a match can’t be found."
1938 (setq parens (cdr parens)))) 1961 (setq parens (cdr parens))))
1939 curly-pos)) 1962 curly-pos))
1940 1963
1964(defun js-jsx--goto-outermost-enclosing-curly (limit)
1965 "Set point to enclosing “{” at or closest after LIMIT."
1966 (let (pos)
1967 (while
1968 (and
1969 (setq pos (js-jsx--enclosing-curly-pos))
1970 (if (>= pos limit) (goto-char pos))
1971 (> pos limit)))))
1972
1941(defun js-jsx--enclosing-tag-pos () 1973(defun js-jsx--enclosing-tag-pos ()
1942 "Return beginning and end of a JSXElement about point. 1974 "Return beginning and end of a JSXElement about point.
1943Look backward for a JSXElement that both starts before point and 1975Look backward for a JSXElement that both starts before point and
1944also ends after point. That may be either a self-closing 1976also ends after point. That may be either a self-closing
1945JSXElement or a JSXOpeningElement/JSXClosingElement pair." 1977JSXElement or a JSXOpeningElement/JSXClosingElement pair."
1946 (let ((start (point)) 1978 (let ((start (point)) tag-beg tag-beg-pos tag-end-pos close-tag-pos)
1947 (curly-pos (save-excursion (js-jsx--enclosing-curly-pos)))
1948 tag-beg tag-beg-pos tag-end-pos close-tag-pos)
1949 (while 1979 (while
1950 (and 1980 (and
1951 (setq tag-beg (js--backward-text-property 'js-jsx-tag-beg)) 1981 (setq tag-beg (js--backward-text-property 'js-jsx-tag-beg))
@@ -1957,25 +1987,24 @@ JSXElement or a JSXOpeningElement/JSXClosingElement pair."
1957 (and (eq (car tag-beg) 'self-closing) 1987 (and (eq (car tag-beg) 'self-closing)
1958 (< start tag-end-pos)) 1988 (< start tag-end-pos))
1959 (and (eq (car tag-beg) 'open) 1989 (and (eq (car tag-beg) 'open)
1960 (save-excursion 1990 (or (< start tag-end-pos)
1961 (goto-char tag-end-pos) 1991 (save-excursion
1962 (setq close-tag-pos (js-jsx--matching-close-tag-pos)) 1992 (goto-char tag-end-pos)
1963 ;; The JSXOpeningElement may either be unclosed, 1993 (setq close-tag-pos (js-jsx--matching-close-tag-pos))
1964 ;; else the closure must occur after the start 1994 ;; The JSXOpeningElement may be unclosed, else
1965 ;; point (otherwise, a miscellaneous previous 1995 ;; the closure must occur at/after the start
1966 ;; JSXOpeningElement has been found, and we should 1996 ;; point (otherwise, a miscellaneous previous
1967 ;; keep looking back for an enclosing one). 1997 ;; JSXOpeningElement has been found, so keep
1968 (or (not close-tag-pos) (< start close-tag-pos)))))))) 1998 ;; looking backwards for an enclosing one).
1969 ;; Don’t return the last tag pos (if any; it wasn’t enclosing). 1999 (or (not close-tag-pos) (<= start close-tag-pos)))))))))
1970 (setq tag-beg nil)) 2000 ;; Don’t return the last tag pos, as it wasn’t enclosing.
1971 (and tag-beg 2001 (setq tag-beg nil close-tag-pos nil))
1972 (or (not curly-pos) (> tag-beg-pos curly-pos)) 2002 (and tag-beg (list tag-beg-pos tag-end-pos close-tag-pos))))
1973 (cons tag-beg-pos tag-end-pos))))
1974 2003
1975(defun js-jsx--at-enclosing-tag-child-p () 2004(defun js-jsx--at-enclosing-tag-child-p ()
1976 "Return t if point is at an enclosing tag’s child." 2005 "Return t if point is at an enclosing tag’s child."
1977 (let ((pos (save-excursion (js-jsx--enclosing-tag-pos)))) 2006 (let ((pos (save-excursion (js-jsx--enclosing-tag-pos))))
1978 (and pos (>= (point) (cdr pos))))) 2007 (and pos (>= (point) (nth 1 pos)))))
1979 2008
1980(defun js-jsx--text-range (beg end) 2009(defun js-jsx--text-range (beg end)
1981 "Identify JSXText within a “>/{/}/<” pair." 2010 "Identify JSXText within a “>/{/}/<” pair."
@@ -2515,6 +2544,118 @@ current line is the \"=>\" token."
2515 (t (looking-at-p 2544 (t (looking-at-p
2516 (concat js--name-re js--line-terminating-arrow-re))))) 2545 (concat js--name-re js--line-terminating-arrow-re)))))
2517 2546
2547(defun js-jsx--context ()
2548 "Determine JSX context and move to enclosing JSX."
2549 (let ((pos (point))
2550 (parse-status (syntax-ppss))
2551 (enclosing-tag-pos (js-jsx--enclosing-tag-pos)))
2552 (when enclosing-tag-pos
2553 (if (< pos (nth 1 enclosing-tag-pos))
2554 (if (nth 3 parse-status)
2555 (list 'string (nth 8 parse-status))
2556 (list 'tag (nth 0 enclosing-tag-pos) (nth 1 enclosing-tag-pos)))
2557 (list 'text (nth 0 enclosing-tag-pos) (nth 2 enclosing-tag-pos))))))
2558
2559(defvar js-jsx--indenting nil
2560 "Flag to prevent infinite recursion while indenting JSX.")
2561
2562(defun js-jsx--indentation (parse-status)
2563 "Helper function for `js--proper-indentation'.
2564Return the proper indentation of the current line if it is part
2565of a JSXElement expression spanning multiple lines; otherwise,
2566return nil."
2567 (let ((current-line (line-number-at-pos))
2568 (curly-pos (js-jsx--enclosing-curly-pos))
2569 nth-context context expr-p beg-line col
2570 forward-sexp-function) ; Use the Lisp version.
2571 ;; Find the immediate context for indentation information, but
2572 ;; keep going to determine that point is at the N+1th line of
2573 ;; multiline JSX.
2574 (save-excursion
2575 (while
2576 (and
2577 (setq nth-context (js-jsx--context))
2578 (progn
2579 (unless context
2580 (setq context nth-context)
2581 (setq expr-p (and curly-pos (< (point) curly-pos))))
2582 (setq beg-line (line-number-at-pos))
2583 (and
2584 (= beg-line current-line)
2585 (or (not curly-pos) (> (point) curly-pos)))))))
2586 (when (and context (> current-line beg-line))
2587 (save-excursion
2588 ;; The column calculation is based on `sgml-calculate-indent'.
2589 (setq col (pcase (nth 0 context)
2590
2591 ('string
2592 ;; Go back to previous non-empty line.
2593 (while (and (> (point) (nth 1 context))
2594 (zerop (forward-line -1))
2595 (looking-at "[ \t]*$")))
2596 (if (> (point) (nth 1 context))
2597 ;; Previous line is inside the string.
2598 (current-indentation)
2599 (goto-char (nth 1 context))
2600 (1+ (current-column))))
2601
2602 ('tag
2603 ;; Special JSX indentation rule: a “dangling”
2604 ;; closing angle bracket on its own line is
2605 ;; indented at the same level as the opening
2606 ;; angle bracket of the JSXElement. Otherwise,
2607 ;; indent JSXAttribute space like SGML.
2608 (if (progn
2609 (goto-char (nth 2 context))
2610 (and (= current-line (line-number-at-pos))
2611 (looking-back "^\\s-*/?>" (line-beginning-position))))
2612 (progn
2613 (goto-char (nth 1 context))
2614 (current-column))
2615 ;; Indent JSXAttribute space like SGML.
2616 (goto-char (nth 1 context))
2617 ;; Skip tag name:
2618 (skip-chars-forward " \t")
2619 (skip-chars-forward "^ \t\n")
2620 (skip-chars-forward " \t")
2621 (if (not (eolp))
2622 (current-column)
2623 ;; This is the first attribute: indent.
2624 (goto-char (+ (nth 1 context) js-jsx-attribute-offset))
2625 (+ (current-column) js-indent-level))))
2626
2627 ('text
2628 ;; Indent to reflect nesting.
2629 (goto-char (nth 1 context))
2630 (+ (current-column)
2631 ;; The last line isn’t nested, but the rest are.
2632 (if (or (not (nth 2 context)) ; Unclosed.
2633 (< current-line (line-number-at-pos (nth 2 context))))
2634 js-indent-level
2635 0)))
2636
2637 )))
2638 ;; When indenting a JSXExpressionContainer expression, use JSX
2639 ;; indentation as a minimum, and use regular JS indentation if
2640 ;; it’s deeper.
2641 (if expr-p
2642 (max (+ col
2643 ;; An expression in a JSXExpressionContainer in a
2644 ;; JSXAttribute should be indented more, except on
2645 ;; the ending line of the JSXExpressionContainer.
2646 (if (and (eq (nth 0 context) 'tag)
2647 (< current-line
2648 (save-excursion
2649 (js-jsx--goto-outermost-enclosing-curly
2650 (nth 1 context))
2651 (forward-sexp)
2652 (line-number-at-pos))))
2653 js-indent-level
2654 0))
2655 (let ((js-jsx--indenting t)) ; Prevent recursion.
2656 (js--proper-indentation parse-status)))
2657 col))))
2658
2518(defun js--proper-indentation (parse-status) 2659(defun js--proper-indentation (parse-status)
2519 "Return the proper indentation for the current line." 2660 "Return the proper indentation for the current line."
2520 (save-excursion 2661 (save-excursion
@@ -2522,6 +2663,8 @@ current line is the \"=>\" token."
2522 (cond ((nth 4 parse-status) ; inside comment 2663 (cond ((nth 4 parse-status) ; inside comment
2523 (js--get-c-offset 'c (nth 8 parse-status))) 2664 (js--get-c-offset 'c (nth 8 parse-status)))
2524 ((nth 3 parse-status) 0) ; inside string 2665 ((nth 3 parse-status) 0) ; inside string
2666 ((when (and js-jsx-syntax (not js-jsx--indenting))
2667 (save-excursion (js-jsx--indentation parse-status))))
2525 ((eq (char-after) ?#) 0) 2668 ((eq (char-after) ?#) 0)
2526 ((save-excursion (js--beginning-of-macro)) 4) 2669 ((save-excursion (js--beginning-of-macro)) 4)
2527 ;; Indent array comprehension continuation lines specially. 2670 ;; Indent array comprehension continuation lines specially.
@@ -2584,111 +2727,6 @@ current line is the \"=>\" token."
2584 (+ js-indent-level js-expr-indent-offset)) 2727 (+ js-indent-level js-expr-indent-offset))
2585 (t (prog-first-column))))) 2728 (t (prog-first-column)))))
2586 2729
2587;;; JSX Indentation
2588
2589(defmacro js-jsx--as-sgml (&rest body)
2590 "Execute BODY as if in sgml-mode."
2591 `(with-syntax-table sgml-mode-syntax-table
2592 ,@body))
2593
2594(defun js-jsx--outermost-enclosing-tag-pos ()
2595 (let (context tag-pos last-tag-pos parse-status parens paren-pos curly-pos)
2596 (js-jsx--as-sgml
2597 ;; Search until we reach the top or encounter the start of a
2598 ;; JSXExpressionContainer (implying nested JSX).
2599 (while (and (setq context (sgml-get-context))
2600 (progn
2601 (setq tag-pos (sgml-tag-start (car (last context))))
2602 (or (not curly-pos)
2603 ;; Stop before curly brackets (start of a
2604 ;; JSXExpressionContainer).
2605 (> tag-pos curly-pos))))
2606 ;; Record this position so it can potentially be returned.
2607 (setq last-tag-pos tag-pos)
2608 ;; Always parse sexps / search for the next context from the
2609 ;; immediately enclosing tag (sgml-get-context may not leave
2610 ;; point there).
2611 (goto-char tag-pos)
2612 (unless parse-status ; Don’t needlessly reparse.
2613 ;; Search upward for an enclosing starting curly bracket.
2614 (setq parse-status (syntax-ppss))
2615 (setq parens (reverse (nth 9 parse-status)))
2616 (while (and (setq paren-pos (car parens))
2617 (not (when (= (char-after paren-pos) ?{)
2618 (setq curly-pos paren-pos))))
2619 (setq parens (cdr parens)))
2620 ;; Always search for the next context from the immediately
2621 ;; enclosing tag (calling syntax-ppss in the above loop
2622 ;; may move point from there).
2623 (goto-char tag-pos))))
2624 last-tag-pos))
2625
2626(defun js-jsx--indentation-type ()
2627 "Determine if/how the current line should be indented as JSX.
2628
2629Return nil for first JSXElement line (indent like JS).
2630Return `n+1th' for second+ JSXElement lines (indent like SGML).
2631Return `expression' for lines within embedded JS expressions
2632 (indent like JS inside SGML).
2633Return nil for non-JSX lines."
2634 (let ((current-pos (point))
2635 (current-line (line-number-at-pos))
2636 tag-start-pos parens paren type)
2637 (save-excursion
2638 ;; Determine if inside a JSXElement.
2639 (beginning-of-line) ; For exclusivity
2640 (when (setq tag-start-pos (js-jsx--outermost-enclosing-tag-pos))
2641 ;; Check if inside an embedded multi-line JS expression.
2642 (goto-char current-pos)
2643 (end-of-line) ; For exclusivity
2644 (setq parens (nth 9 (syntax-ppss)))
2645 (while
2646 (and
2647 (setq paren (car parens))
2648 (if (and
2649 (>= paren tag-start-pos)
2650 ;; A curly bracket indicates the start of an
2651 ;; embedded expression.
2652 (= (char-after paren) ?{)
2653 ;; The first line of the expression is indented
2654 ;; like SGML.
2655 (> current-line (line-number-at-pos paren))
2656 ;; Check if within a closing curly bracket (if any)
2657 ;; (exclusive, as the closing bracket is indented
2658 ;; like SGML).
2659 (if (progn
2660 (goto-char paren)
2661 (ignore-errors (let (forward-sexp-function)
2662 (forward-sexp))))
2663 (< current-line (line-number-at-pos))
2664 ;; No matching bracket implies we’re inside!
2665 t))
2666 ;; Indicate this will be indented specially. Return
2667 ;; nil to stop iterating too.
2668 (progn (setq type 'expression) nil)
2669 ;; Stop iterating when parens = nil.
2670 (setq parens (cdr parens)))))
2671 (or type 'n+1th)))))
2672
2673(defun js-jsx--indent-line-in-expression ()
2674 "Indent the current line as JavaScript within JSX."
2675 (let ((parse-status (save-excursion (syntax-ppss (point-at-bol))))
2676 offset indent-col)
2677 (unless (nth 3 parse-status)
2678 (save-excursion
2679 (setq offset (- (point) (progn (back-to-indentation) (point)))
2680 indent-col (js-jsx--as-sgml (sgml-calculate-indent))))
2681 (if (null indent-col) 'noindent ; Like in sgml-mode
2682 ;; Use whichever indentation column is greater, such that the
2683 ;; SGML column is effectively a minimum.
2684 (indent-line-to (max (js--proper-indentation parse-status)
2685 (+ indent-col js-indent-level)))
2686 (when (> offset 0) (forward-char offset))))))
2687
2688(defun js-jsx--indent-n+1th-line ()
2689 "Indent the current line as JSX within JavaScript."
2690 (js-jsx--as-sgml (sgml-indent-line)))
2691
2692(defun js-indent-line () 2730(defun js-indent-line ()
2693 "Indent the current line as JavaScript." 2731 "Indent the current line as JavaScript."
2694 (interactive) 2732 (interactive)
@@ -2700,15 +2738,9 @@ Return nil for non-JSX lines."
2700 (when (> offset 0) (forward-char offset))))) 2738 (when (> offset 0) (forward-char offset)))))
2701 2739
2702(defun js-jsx-indent-line () 2740(defun js-jsx-indent-line ()
2703 "Indent the current line as JSX (with SGML offsets). 2741 "Indent the current line as JavaScript+JSX."
2704i.e., customize JSX element indentation with `sgml-basic-offset',
2705`sgml-attribute-offset' et al."
2706 (interactive) 2742 (interactive)
2707 (let ((type (js-jsx--indentation-type))) 2743 (let ((js-jsx-syntax t)) (js-indent-line)))
2708 (if type
2709 (if (eq type 'n+1th) (js-jsx--indent-n+1th-line)
2710 (js-jsx--indent-line-in-expression))
2711 (js-indent-line))))
2712 2744
2713;;; Filling 2745;;; Filling
2714 2746
@@ -4281,18 +4313,9 @@ If one hasn't been set, or if it's stale, prompt for a new one."
4281 4313
4282;;;###autoload 4314;;;###autoload
4283(define-derived-mode js-jsx-mode js-mode "JSX" 4315(define-derived-mode js-jsx-mode js-mode "JSX"
4284 "Major mode for editing JSX. 4316 "Major mode for editing JSX."
4285
4286To customize the indentation for this mode, set the SGML offset
4287variables (`sgml-basic-offset', `sgml-attribute-offset' et al.)
4288locally, like so:
4289
4290 (defun set-jsx-indentation ()
4291 (setq-local sgml-basic-offset js-indent-level))
4292 (add-hook \\='js-jsx-mode-hook #\\='set-jsx-indentation)"
4293 :group 'js 4317 :group 'js
4294 (setq-local js-jsx-syntax t) 4318 (setq-local js-jsx-syntax t))
4295 (setq-local indent-line-function #'js-jsx-indent-line))
4296 4319
4297;;;###autoload (defalias 'javascript-mode 'js-mode) 4320;;;###autoload (defalias 'javascript-mode 'js-mode)
4298 4321