From 7c12f2cf73cbbe39deab169a7e541e46a298e6a9 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sat, 31 Oct 2015 14:34:12 -0700 Subject: [PATCH] Copy JSX indentation logic to js2-old-indent.el --- js2-mode.el | 14 +-- js2-old-indent.el | 211 ++++++++++++++++++++++++++++++++++++++++++++++ tests/indent.el | 97 ++++++++++++++++++++- 3 files changed, 315 insertions(+), 7 deletions(-) diff --git a/js2-mode.el b/js2-mode.el index dd97aba50..64146e1a5 100644 --- a/js2-mode.el +++ b/js2-mode.el @@ -95,6 +95,7 @@ (require 'js2-old-indent) (defvaralias 'js2-basic-offset 'js-indent-level nil) (defalias 'js2-proper-indentation 'js--proper-indentation) + (defalias 'js2-jsx-indent-line 'js-jsx-indent-line) (defalias 'js2-indent-line 'js-indent-line) (defalias 'js2-re-search-forward 'js--re-search-forward))) @@ -11378,11 +11379,14 @@ Selecting an error will jump it to the corresponding source-buffer error. ;;;###autoload (define-derived-mode js2-jsx-mode js2-mode "JSX-IDE" "Major mode for editing JSX code. -Like `js-jsx-mode', which see. -Indentation support requires Emacs 25+ (or the latest js.el)." - ;; Feature-detect JSX indentation support (in js-mode from Emacs 25+). - (if (fboundp 'js-jsx-indent-line) - (set (make-local-variable 'indent-line-function) #'js-jsx-indent-line))) + +To customize the indentation for this mode, set the SGML offset +variables (`sgml-basic-offset' et al) locally, like so: + + (defun set-jsx-indentation () + (setq-local sgml-basic-offset js2-basic-offset)) + (add-hook 'js2-jsx-mode-hook #'set-jsx-indentation)" + (set (make-local-variable 'indent-line-function) #'js2-jsx-indent-line)) (defun js2-mode-exit () "Exit `js2-mode' and clean up." diff --git a/js2-old-indent.el b/js2-old-indent.el index 64a874e38..82a00b4a2 100644 --- a/js2-old-indent.el +++ b/js2-old-indent.el @@ -54,6 +54,8 @@ ;;; Code: +(require 'sgml-mode) + (defvar js2-language-version) (declare-function js2-mark-safe-local "js2-mode") @@ -493,6 +495,215 @@ indentation is aligned to that column." (when (cl-plusp offset) (forward-char offset))))) +;;; JSX Indentation + +;; The following JSX indentation code is copied basically verbatim from js.el at +;; 958da7f, except that the prefixes on the functions/variables are changed. + +(defsubst js2--jsx-find-before-tag () + "Find where JSX starts. + +Assume JSX appears in the following instances: +- Inside parentheses, when returned or as the first argument + to a function, and after a newline +- When assigned to variables or object properties, but only + on a single line +- As the N+1th argument to a function + +This is an optimized version of (re-search-backward \"[(,]\n\" +nil t), except set point to the end of the match. This logic +executes up to the number of lines in the file, so it should be +really fast to reduce that impact." + (let (pos) + (while (and (> (point) (point-min)) + (not (progn + (end-of-line 0) + (when (or (eq (char-before) 40) ; ( + (eq (char-before) 44)) ; , + (setq pos (1- (point)))))))) + pos)) + +(defconst js2--jsx-end-tag-re + (concat "\\|/>") + "Find the end of a JSX element.") + +(defconst js2--jsx-after-tag-re "[),]" + "Find where JSX ends. +This complements the assumption of where JSX appears from +`js--jsx-before-tag-re', which see.") + +(defun js2--jsx-indented-element-p () + "Determine if/how the current line should be indented as JSX. + +Return `first' for the first JSXElement on its own line. +Return `nth' for subsequent lines of the first JSXElement. +Return `expression' for an embedded JS expression. +Return `after' for anything after the last JSXElement. +Return nil for non-JSX lines. + +Currently, JSX indentation supports the following styles: + +- Single-line elements (indented like normal JS): + + var element =
; + +- Multi-line elements (enclosed in parentheses): + + function () { + return ( +
+
+
+ ); + } + +- Function arguments: + + React.render( +
, + document.querySelector('.root') + );" + (let ((current-pos (point)) + (current-line (line-number-at-pos)) + last-pos + before-tag-pos before-tag-line + tag-start-pos tag-start-line + tag-end-pos tag-end-line + after-tag-line + parens paren type) + (save-excursion + (and + ;; Determine if we're inside a jsx element + (progn + (end-of-line) + (while (and (not tag-start-pos) + (setq last-pos (js2--jsx-find-before-tag))) + (while (forward-comment 1)) + (when (= (char-after) 60) ; < + (setq before-tag-pos last-pos + tag-start-pos (point))) + (goto-char last-pos)) + tag-start-pos) + (progn + (setq before-tag-line (line-number-at-pos before-tag-pos) + tag-start-line (line-number-at-pos tag-start-pos)) + (and + ;; A "before" line which also starts an element begins with js, so + ;; indent it like js + (> current-line before-tag-line) + ;; Only indent the jsx lines like jsx + (>= current-line tag-start-line))) + (cond + ;; Analyze bounds if there are any + ((progn + (while (and (not tag-end-pos) + (setq last-pos (re-search-forward js2--jsx-end-tag-re nil t))) + (while (forward-comment 1)) + (when (looking-at js2--jsx-after-tag-re) + (setq tag-end-pos last-pos))) + tag-end-pos) + (setq tag-end-line (line-number-at-pos tag-end-pos) + after-tag-line (line-number-at-pos after-tag-line)) + (or (and + ;; Ensure we're actually within the bounds of the jsx + (<= current-line tag-end-line) + ;; An "after" line which does not end an element begins with + ;; js, so indent it like js + (<= current-line after-tag-line)) + (and + ;; Handle another case where there could be e.g. comments after + ;; the element + (> current-line tag-end-line) + (< current-line after-tag-line) + (setq type 'after)))) + ;; They may not be any bounds (yet) + (t)) + ;; Check if we're inside an embedded multi-line js expression + (cond + ((not type) + (goto-char current-pos) + (end-of-line) + (setq parens (nth 9 (syntax-ppss))) + (while (and parens (not type)) + (setq paren (car parens)) + (cond + ((and (>= paren tag-start-pos) + ;; Curly bracket indicates the start of an embedded expression + (= (char-after paren) 123) ; { + ;; The first line of the expression is indented like sgml + (> current-line (line-number-at-pos paren)) + ;; Check if within a closing curly bracket (if any) + ;; (exclusive, as the closing bracket is indented like sgml) + (cond + ((progn + (goto-char paren) + (ignore-errors (let (forward-sexp-function) + (forward-sexp)))) + (< current-line (line-number-at-pos))) + (t))) + ;; Indicate this guy will be indented specially + (setq type 'expression)) + (t (setq parens (cdr parens))))) + t) + (t)) + (cond + (type) + ;; Indent the first jsx thing like js so we can indent future jsx things + ;; like sgml relative to the first thing + ((= current-line tag-start-line) 'first) + ('nth)))))) + +(defmacro js2--as-sgml (&rest body) + "Execute BODY as if in sgml-mode." + `(with-syntax-table sgml-mode-syntax-table + (let (forward-sexp-function + parse-sexp-lookup-properties) + ,@body))) + +(defun js2--expression-in-sgml-indent-line () + "Indent the current line as JavaScript or SGML (whichever is farther)." + (let* (indent-col + (savep (point)) + ;; Don't whine about errors/warnings when we're indenting. + ;; This has to be set before calling parse-partial-sexp below. + (inhibit-point-motion-hooks t) + (parse-status (save-excursion + (syntax-ppss (point-at-bol))))) + ;; Don't touch multiline strings. + (unless (nth 3 parse-status) + (setq indent-col (save-excursion + (back-to-indentation) + (if (>= (point) savep) (setq savep nil)) + (js2--as-sgml (sgml-calculate-indent)))) + (if (null indent-col) + 'noindent + ;; Use whichever indentation column is greater, such that the sgml + ;; column is effectively a minimum + (setq indent-col (max (js2-proper-indentation parse-status) + (+ indent-col js2-basic-offset))) + (if savep + (save-excursion (indent-line-to indent-col)) + (indent-line-to indent-col)))))) + +(defun js2-jsx-indent-line () + "Indent the current line as JSX (with SGML offsets). +i.e., customize JSX element indentation with `sgml-basic-offset' +et al." + (interactive) + (let ((indentation-type (js2--jsx-indented-element-p))) + (cond + ((eq indentation-type 'expression) + (js2--expression-in-sgml-indent-line)) + ((or (eq indentation-type 'first) + (eq indentation-type 'after)) + ;; Don't treat this first thing as a continued expression (often a "<" or + ;; ">" causes this misinterpretation) + (cl-letf (((symbol-function #'js2-continued-expression-p) 'ignore)) + (js2-indent-line))) + ((eq indentation-type 'nth) + (js2--as-sgml (sgml-indent-line))) + (t (js2-indent-line))))) + (provide 'js2-old-indent) ;;; js2-old-indent.el ends here diff --git a/tests/indent.el b/tests/indent.el index a0afeed49..bacb61485 100644 --- a/tests/indent.el +++ b/tests/indent.el @@ -31,14 +31,15 @@ (if keep-indent s (replace-regexp-in-string "^ *" "" s))) - (js2-mode) + (js2-jsx-mode) (indent-region (point-min) (point-max)) (should (string= s (buffer-substring-no-properties (point-min) (point))))))) (cl-defmacro js2-deftest-indent (name content &key bind keep-indent) `(ert-deftest ,(intern (format "js2-%s" name)) () - (let ,(append '((js2-basic-offset 2) + (let ,(append '(indent-tabs-mode + (js2-basic-offset 2) (js2-pretty-multiline-declarations t) (inhibit-point-motion-hooks t)) bind) @@ -169,3 +170,95 @@ | return 1; |}" :bind ((js2-indent-switch-body t))) + +(js2-deftest-indent jsx-one-line + "var foo =
;") + +(js2-deftest-indent jsx-children-parentheses + "return ( + |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |);") + +(js2-deftest-indent jsx-children-unclosed + "return ( + |
+ |
") + +(js2-deftest-indent jsx-argument + "React.render( + |
+ |
+ |
, + | { + | a: 1 + | }, + |
+ |
+ |
+ |);") + +(js2-deftest-indent jsx-leading-comment + "return ( + | // Sneaky! + |
+ |);") + +(js2-deftest-indent jsx-trailing-comment + "return ( + |
+ | // Sneaky! + |);") + +(js2-deftest-indent jsx-self-closing + ;; This ensures we know the bounds of a self-closing element + "React.render( + | , + | { + | a: 1 + | } + |);" + :bind ((sgml-attribute-offset 1))) ; Emacs 24.5 -> 25 compat + +(js2-deftest-indent jsx-embedded-js-content + "return ( + |
+ | {array.map(function () { + | return { + | a: 1 + | }; + | })} + |
+ |);") + +(js2-deftest-indent jsx-embedded-js-unclosed + "return ( + |
+ | {array.map(function () { + | return { + | a: 1") + +(js2-deftest-indent jsx-embedded-js-attribute + "return ( + |
+ |
+ |);") -- 2.39.2