emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] master 667d320 034/110: Merge pull request #282 from jacksonrayha


From: Dmitry Gutov
Subject: [elpa] master 667d320 034/110: Merge pull request #282 from jacksonrayhamilton/js2-jsx-mode
Date: Thu, 23 Jun 2016 01:12:55 +0000 (UTC)

branch: master
commit 667d320d3a3365a87ebb11a822ac19cd4133bdea
Merge: c63c700 1f95553
Author: Dmitry Gutov <address@hidden>
Commit: Dmitry Gutov <address@hidden>

    Merge pull request #282 from jacksonrayhamilton/js2-jsx-mode
    
    Add js2-jsx-mode
---
 NEWS.md           |    3 +
 js2-mode.el       |   21 ++++++
 js2-old-indent.el |  211 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/indent.el   |   97 +++++++++++++++++++++++-
 4 files changed, 330 insertions(+), 2 deletions(-)

diff --git a/NEWS.md b/NEWS.md
index 30500e0..33ba376 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -10,6 +10,9 @@
   name will get more confusing.
 * Support for default parameters in destructuring.  It should work for both
   objects and arrays, in both literals and function arguments.
+* New mode: `js2-jsx-mode`, deriving from `js2-mode`.  Supports indentation of
+  JSXElement expressions wrapped within parentheses or as function arguments.
+  Indentation is customizable via `sgml-attribute-offset`.
 
 ## 20150909
 
diff --git a/js2-mode.el b/js2-mode.el
index 643e921..2c59a40 100644
--- a/js2-mode.el
+++ b/js2-mode.el
@@ -60,6 +60,12 @@
 
 ;;   (add-to-list 'interpreter-mode-alist '("node" . js2-mode))
 
+;; Support for JSX is available via the derived mode `js2-jsx-mode'.  If you
+;; also want JSX support, use that mode instead:
+
+;;   (add-to-list 'auto-mode-alist '("\\.js\\'" . js2-jsx-mode))
+;;   (add-to-list 'interpreter-mode-alist '("node" . js2-jsx-mode))
+
 ;; To customize how it works:
 ;;   M-x customize-group RET js2-mode RET
 
@@ -95,6 +101,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)))
 
@@ -11373,6 +11380,20 @@ Selecting an error will jump it to the corresponding 
source-buffer error.
 
   (js2-reparse))
 
+;; We may eventually want js2-jsx-mode to derive from js-jsx-mode, but that'd 
be
+;; a bit more complicated and it doesn't net us much yet.
+;;;###autoload
+(define-derived-mode js2-jsx-mode js2-mode "JSX-IDE"
+  "Major mode for editing JSX code.
+
+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."
   (interactive)
diff --git a/js2-old-indent.el b/js2-old-indent.el
index 64a874e..82a00b4 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 "</" sgml-name-re ">\\|/>")
+  "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 = <div></div>;
+
+- Multi-line elements (enclosed in parentheses):
+
+  function () {
+    return (
+      <div>
+        <div></div>
+      </div>
+    );
+ }
+
+- Function arguments:
+
+  React.render(
+    <div></div>,
+    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 a0afeed..bacb614 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 = <div></div>;")
+
+(js2-deftest-indent jsx-children-parentheses
+  "return (
+  |  <div>
+  |  </div>
+  |  <div>
+  |    <div></div>
+  |    <div>
+  |      <div></div>
+  |    </div>
+  |  </div>
+  |);")
+
+(js2-deftest-indent jsx-children-unclosed
+  "return (
+  |  <div>
+  |    <div>")
+
+(js2-deftest-indent jsx-argument
+  "React.render(
+  |  <div>
+  |    <div></div>
+  |  </div>,
+  |  {
+  |    a: 1
+  |  },
+  |  <div>
+  |    <div></div>
+  |  </div>
+  |);")
+
+(js2-deftest-indent jsx-leading-comment
+  "return (
+  |  // Sneaky!
+  |  <div></div>
+  |);")
+
+(js2-deftest-indent jsx-trailing-comment
+  "return (
+  |  <div></div>
+  |  // Sneaky!
+  |);")
+
+(js2-deftest-indent jsx-self-closing
+  ;; This ensures we know the bounds of a self-closing element
+  "React.render(
+  |  <input
+  |     />,
+  |  {
+  |    a: 1
+  |  }
+  |);"
+  :bind ((sgml-attribute-offset 1))) ; Emacs 24.5 -> 25 compat
+
+(js2-deftest-indent jsx-embedded-js-content
+  "return (
+  |  <div>
+  |    {array.map(function () {
+  |      return {
+  |        a: 1
+  |      };
+  |    })}
+  |  </div>
+  |);")
+
+(js2-deftest-indent jsx-embedded-js-unclosed
+  "return (
+  |  <div>
+  |    {array.map(function () {
+  |      return {
+  |        a: 1")
+
+(js2-deftest-indent jsx-embedded-js-attribute
+  "return (
+  |  <div attribute={array.map(function () {
+  |         return {
+  |           a: 1
+  |         };
+  |
+  |         return {
+  |           a: 1
+  |         };
+  |
+  |         return {
+  |           a: 1
+  |         };
+  |       })}>
+  |  </div>
+  |);")



reply via email to

[Prev in Thread] Current Thread [Next in Thread]