diff options
| -rw-r--r-- | lisp/progmodes/js.el | 307 |
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 | |||
| 590 | Let `js-indent-level' be 2. When this variable is also set to 0, | ||
| 591 | JSXAttribute indentation looks like this: | ||
| 592 | |||
| 593 | <element | ||
| 594 | attribute=\"value\"> | ||
| 595 | </element> | ||
| 596 | |||
| 597 | Alternatively, when this variable is also set to 2, JSXAttribute | ||
| 598 | indentation looks like this: | ||
| 599 | |||
| 600 | <element | ||
| 601 | attribute=\"value\"> | ||
| 602 | </element> | ||
| 603 | |||
| 604 | This 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. |
| 1943 | Look backward for a JSXElement that both starts before point and | 1975 | Look backward for a JSXElement that both starts before point and |
| 1944 | also ends after point. That may be either a self-closing | 1976 | also ends after point. That may be either a self-closing |
| 1945 | JSXElement or a JSXOpeningElement/JSXClosingElement pair." | 1977 | JSXElement 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'. | ||
| 2564 | Return the proper indentation of the current line if it is part | ||
| 2565 | of a JSXElement expression spanning multiple lines; otherwise, | ||
| 2566 | return 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 | |||
| 2629 | Return nil for first JSXElement line (indent like JS). | ||
| 2630 | Return `n+1th' for second+ JSXElement lines (indent like SGML). | ||
| 2631 | Return `expression' for lines within embedded JS expressions | ||
| 2632 | (indent like JS inside SGML). | ||
| 2633 | Return 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." |
| 2704 | i.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 | |||
| 4286 | To customize the indentation for this mode, set the SGML offset | ||
| 4287 | variables (`sgml-basic-offset', `sgml-attribute-offset' et al.) | ||
| 4288 | locally, 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 | ||