unofficial mirror of emacs-devel@gnu.org 
 help / color / mirror / code / Atom feed
* Comprehensive JSX support in Emacs
@ 2019-02-14  5:06 Jackson Ray Hamilton
  2019-02-14  8:03 ` Marcin Borkowski
                   ` (4 more replies)
  0 siblings, 5 replies; 33+ messages in thread
From: Jackson Ray Hamilton @ 2019-02-14  5:06 UTC (permalink / raw)
  To: emacs-devel


[-- Attachment #1.1: Type: text/plain, Size: 10279 bytes --]

Hello Emacs maintainers,

A few years ago, I took up the challenge of implementing indentation support for the JavaScript extension “JSX” (by Facebook, for their “React” UI library), and we got my patches merged into js-mode.  Now I think it’s time we reexamined and began to improve Emacs’ JSX+React support.

My initial implementation was not too great, because it failed to indent JSX code properly in many common scenarios.  My hueristics for finding HTML-like code inside a JS buffer were too conservative, due to a (perhaps excessive/misguided) attention to performance in particular areas of the code.  Since my patch’s release, I’ve been watching from a distance as the bug reports have piled up.

A package called “rjsx-mode” came onto the scene not too later, which seems to solve indentation problems using an AST (building on js2-mode’s AST); however, according to Steve Yegge’s post-mortem on js2-mode (http://steve-yegge.blogspot.com/2008/03/js2-mode-new-javascript-mode-for-emacs.html), relying on an AST being available after every keystroke is probably not optimal for performance, especially with large files.  rjsx-mode also performs syntax highlighting (again, using its AST), which js-mode still does not do at all for JSX.

Recently, I finally resolved to “do my laundry,” with respect to addressing the JSX indentation issues accumulated within debbugs and the js2-mode GitHub repo (where lots of js-mode bugs accidentally get filed).  In the past week, I assembled some test cases, drafted some algorithms, and proceeded to implement a number of improvements to the indentation logic.

As I started playing with the code again, many new ideas for improving JSX+React support began popping into my head, beyond just indentation improvements.  I wanted to share those ideas here, and see if anyone wants to thumbs-up them before I go ahead and implement them (perhaps avoiding some bad ideas and/or rework).

Also, I wanted to share my work-in-progress indentation reimplementation, and determine if I am not going completely in the wrong direction, and if an alternate plan I’ll also present for indentation is at all viable in comparison.


Proposition: Comprehensive JSX support in Emacs


First: Consider reworking obtuse APIs

JSX indentation support is currently enabled by activating “js-jsx-mode”, using a function called “js-jsx-indent-line”, and “sgml-” variables control the indentation of the JSX code.

I think introducing JSX support in the form of a derivative mode may have been a mistake.  It wasn’t initially obvious to some people to use this mode.  There was not an auto-mode-alist item for it, either.  It also had the effect of creating a “matrix” of modes, where, for instance, Flycheck has to support all the different JavaScript modes like this:

    :modes (js-mode js-jsx-mode js2-mode js2-jsx-mode js3-mode rjsx-mode)

which seems messy to me.

And what if we want to support more JavaScript syntax extensions, like Facebook’s “Flow,” and support the use of multiple extensions at once?  We wouldn’t want to also have a js-flow-mode, js-jsx-flow-mode, js2-flow-mode, js2-jsx-flow-mode… therefore, I think it’d be better to start scaling back, and instead enable JSX, Flow, et al support with a variable.

Also, upon reflection, I’m becoming more certain that controlling JavaScript indentation (even HTML-like indentation) with sgml- variables was unintuitive and therefore also a mistake.  I think JSX should simply be indented using js-indent-level, perhaps optionally in combination with a js-jsx-attribute-offset, to complement the sgml-attribute-offset that we would otherwise be eliminating as an option.  However, having added sgml-attribute-offset to Emacs myself merely as a precaution after a minor breaking change I made to SGML indentation a few years ago, I think we should let users actually demand JSX attribute indentation deltas, rather than assume they desire that level of granularity (considering we went so long without it in sgml-mode).  Rather, users have indicated in bug reports that they’d prefer better defaults for the indentation (assuming indentation not matching “official” conventions is simply “erroneous;” and what a blessing to have conventions).

I think JSX shoud “just work” in Emacs; i.e., with js-mode, whenever a user opens a file using JSX syntax, adapting to the user’s conventions for JS.


Deprecation: Guide users to new, nicer APIs

* Deprecate js-jsx-mode (and js2-jsx-mode downstream, and have rjsx-mode derive from js2-mode).  Both modes will still exist, but will be marked obsolete, and will simply make a file-local copy of js-syntax-extensions (see below) with 'jsx added to the front of the list, instead of binding indentation like they currently do.
* Deprecate js-jsx-indent-line, marking it obsolete.  It will still exist, but it will simply call through to js-indent-line, with a copy of js-syntax-extensions let-bound, with 'jsx added to the front of the list.


New features:

* js-syntax-extensions: A list that will include symbols representing syntax extensions for which we will enable indentation/fontification.
o In theory, if we supported more syntax extensions, those would go here as well; also, if any of the extensions happened to conflict with each other in some ways but not others, we could use the order of the list to determine the “precedence” of the extensions.
o Having one variable for this gives us an opportunity to document all the supported syntaxes in one place, so more people will discover what’s all available.
o Add a Customize interface for adding to this list like a set, picking from a set of options.  (Not sure which widget would be best to use for that.)
* js-indent-line will switch to a JSX indentation strategy when called while js-syntax-extensions contains 'jsx.
* js-mode will automatically detect whether a file uses JSX syntax using some heuristic.  Suggestions:
o 'jsx is in js-syntax-extensions via init file, mode hook, or in .dir-locals.el
o buffer-file-name has extension “.jsx” (some people use this)
o “^\(var\|let\|const\|import\) React” matches within the first 1000 lines after any keystroke.  (Under typical compiler configuration, “React” must be a JavaScript variable in scope in order for the compiled JSX to function properly, and most people import or require() stuff at the beginning of their files.  React is usually used in combination with a JavaScript compiler, implying the user probably isn’t using React as a global variable, because compilers make that practice obsolete, so I expect this check will be pretty reliable)
* Add auto-mode-alist item mapping “.jsx” files to js-mode, enabling the buffer-file-name heuristic.
* As syntax extensions are enabled/disabled, update the mode name; ex:
o No syntax extensions: "JavaScript"
o js-syntax-extensions = (jsx): "JavaScript[JSX]"
o js-syntax-extensions = (flow jsx): "JavaScript[Flow,JSX]"
* Fill in current gaps in indentation support for JSX.
* Add font locking for JSX syntax, similar if not the same as sgml-mode’s font locking.  It is enabled when js-syntax-extensions contains 'jsx.

Would appreciate feedback on these feature propositions; thanks!


Indentation: WIP

As I mentioned, I’ve already started hacking on the indentation.  WIP patches are attached for preliminary review of approach (please don’t merge these yet, I haven’t vetted them thoroughly outside make test cases).

I started by reworking my “JSX detection” code to use sgml-get-context to figure out if we were inside JSX, which was a lot more reliable than the previous heuristic (JSX pretty much always had to be wrapped in parens).  I then proceeded to mostly solve Bug#24896 by distinguishing “<” and “>” in JS and JSX, so the sgml-mode indentation code could parse the code without tripping over arrow functions (“=>”), thinking they were part of HTML “<elements>”.  I disambiguated the symbols in a syntax-propertize-function by doing a quick parse near every “<” and “>” char to determine if the surrounding code could only be syntactically valid as JSX, and not as JS.  This seems to have fixed a lot of the failing cases.  However, it still fails in a weird case where JSX appears nested inside JS, where that JS is also nested inside an outer JSX, a situation that just isn’t relevant in SGML, and thus the indenter doesn’t seem to support it.

// JS expressions should not break indentation
// (https://github.com/mooz/js2-mode/issues/462).
return (
  <Router>
    <Bar>
      <Route exact path="/foo" render={() => (
        <div>nothing</div>
      )} />
      <Route exact path="/bar" />
    </Bar>
  </Router>
)

Maybe I could fix this recursive indentation issue by parsing the JSX further, and marking any “{”, “}” pairs therein with some syntax property that would get sgml-indent-line to treat it like some nested thing.  However, that also might not work, and I don’t think I want to jump through any more hoops to make sgml-indent-line work inside js-mode.

Also, since I’ve already started parsing JSX to disambiguate it from JS, I figure I may as well finish the parse (it’s pretty simple), since I could use that parse as an opportunity to mark characters for JSX-contextual font locking.  And, since I’m adding these markings to buffer positions, I now have a great new way to figure out if I’m inside JSX when I’m indenting.  So I’m thinking it may be time to graduate from sgml-indent-line delegation, and use the new data we now have to implement the JSX indentation logic in its entirety in js-mode.  This way we could definitely support recursive JSX indentation, and also align all the indentation conventions with those used by the React community, and potentially maximize performance by avoiding a lot of checks that poor sgml-mode has to do for HTML’s loose and more complex semantics.

Please let me know what you think of my current patches and the direction they’re going in, and if porting a simplified sgml-indent-line to js-mode is not too crazy of an idea!

Thanks, Jackson

[-- Attachment #1.2: Type: text/html, Size: 11719 bytes --]

[-- Attachment #2: 0001-Add-failing-tests-for-JSX-indentation-bugs.patch --]
[-- Type: text/x-patch, Size: 6488 bytes --]

From 896e196246b87f65bd65ebc29a96347792b578e5 Mon Sep 17 00:00:00 2001
From: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
Date: Sat, 9 Feb 2019 15:42:42 -0800
Subject: [PATCH] Add failing tests for JSX indentation bugs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* test/manual/indent/js-jsx.js: Add failing tests for all the js-mode
and js2-mode JSX indentation bugs reported over the years that I could
find.  Some may be duplicates, so I have grouped similar reports
together, for now; we’ll see for certain which distinct cases we need
once we start actually implementing fixes.
* test/manual/indent/js-jsx-quote.js: New file with a nasty test.
---
 test/manual/indent/js-jsx-quote.js |  18 ++++
 test/manual/indent/js-jsx.js       | 183 +++++++++++++++++++++++++++++++++++++
 2 files changed, 201 insertions(+)
 create mode 100644 test/manual/indent/js-jsx-quote.js

diff --git a/test/manual/indent/js-jsx-quote.js b/test/manual/indent/js-jsx-quote.js
new file mode 100644
index 0000000000..4b71a65674
--- /dev/null
+++ b/test/manual/indent/js-jsx-quote.js
@@ -0,0 +1,18 @@
+// -*- mode: js-jsx; -*-
+
+// JSX text node values should be strings, but only JS string syntax
+// is considered, so quote marks delimit strings like normal, with
+// disastrous results (https://github.com/mooz/js2-mode/issues/409).
+function Bug() {
+  return <div>C'est Montréal</div>;
+}
+function Test(foo = /'/,
+              bar = 123) {}
+
+// This test is in a separate file because it can break other tests
+// when indenting the whole buffer (not sure why).
+
+// Local Variables:
+// indent-tabs-mode: nil
+// js-indent-level: 2
+// End:
diff --git a/test/manual/indent/js-jsx.js b/test/manual/indent/js-jsx.js
index 7401939d28..35ca4b275a 100644
--- a/test/manual/indent/js-jsx.js
+++ b/test/manual/indent/js-jsx.js
@@ -70,6 +70,189 @@ return (
   </div>
 );
 
+// Indent void expressions (no need for contextual parens / commas)
+// (https://github.com/mooz/js2-mode/issues/140#issuecomment-166250016).
+<div className="class-name">
+  <h2>Title</h2>
+  {array.map(() => {
+    return <Element />;
+  })}
+  {message}
+</div>
+// Another example of above issue
+// (https://github.com/mooz/js2-mode/issues/490).
+<App>
+  <div>
+    {variable1}
+    <Component/>
+  </div>
+</App>
+
+// Comments and arrows can break indentation (Bug#24896 /
+// https://github.com/mooz/js2-mode/issues/389).
+const Component = props => (
+  <FatArrow a={e => c}
+            b={123}>
+  </FatArrow>
+);
+const Component = props => (
+  <NoFatArrow a={123}
+              b={123}>
+  </NoFatArrow>
+);
+const Component = props => ( // Parse this comment, please.
+  <FatArrow a={e => c}
+            b={123}>
+  </FatArrow>
+);
+const Component = props => ( // Parse this comment, please.
+  <NoFatArrow a={123}
+              b={123}>
+  </NoFatArrow>
+);
+// Another example of above issue (Bug#30225).
+class {
+  render() {
+    return (
+      <select style={{paddingRight: "10px"}}
+              onChange={e => this.setState({value: e.target.value})}
+              value={this.state.value}>
+        <option>Hi</option>
+      </select>
+    );
+  }
+}
+
+// JSX attributes of an arrow function’s expression body’s JSX
+// expression should be indented with respect to the JSX opening
+// element (Bug#26001 /
+// https://github.com/mooz/js2-mode/issues/389#issuecomment-271869380).
+class {
+  render() {
+    const messages = this.state.messages.map(
+      message => <Message key={message.id}
+                          text={message.text}
+                          mine={message.mine} />
+    );    return messages;
+  }
+  render() {
+    const messages = this.state.messages.map(message =>
+      <Message key={message.timestamp}
+               text={message.text}
+               mine={message.mine} />
+    );    return messages;
+  }
+}
+
+// Users expect tag closers to align with the tag’s start; this is the
+// style used in the React docs, so it should be the default.
+// - https://github.com/mooz/js2-mode/issues/389#issuecomment-390766873
+// - https://github.com/mooz/js2-mode/issues/482
+// - Bug#32158
+const foo = (props) => (
+  <div>
+    <input
+      cat={i => i}
+    />
+    <button
+      className="square"
+    >
+      {this.state.value}
+    </button>
+  </div>
+);
+
+// Embedded JSX in parens breaks indentation
+// (https://github.com/mooz/js2-mode/issues/411).
+let a = (
+  <div>
+    {condition && <Component/>}
+    {condition && <Component/>}
+    <div/>
+  </div>
+)
+let b = (
+  <div>
+    {condition && (<Component/>)}
+    <div/>
+  </div>
+)
+let c = (
+  <div>
+    {condition && (<Component/>)}
+    {condition && "something"}
+  </div>
+)
+let d = (
+  <div>
+    {(<Component/>)}
+    {condition && "something"}
+  </div>
+)
+// Another example of the above issue (Bug#27000).
+function testA() {
+  return (
+    <div>
+      <div> { ( <div/> ) } </div>
+    </div>
+  );
+}
+function testB() {
+  return (
+    <div>
+      <div> { <div/> } </div>
+    </div>
+  );
+}
+// Another example of the above issue
+// (https://github.com/mooz/js2-mode/issues/451).
+class Classy extends React.Component {
+  render () {
+    return (
+      <div>
+        <ul className="tocListRoot">
+          { this.state.list.map((item) => {
+            return (<div />)
+          })}
+        </ul>
+      </div>
+    )
+  }
+}
+
+// Self-closing tags should be indented properly
+// (https://github.com/mooz/js2-mode/issues/459).
+export default ({ stars }) => (
+  <div className='overlay__container'>
+    <div className='overlay__header overlay--text'>
+      Congratulations!
+    </div>
+    <div className='overlay__reward'>
+      <Icon {...createIconProps(stars > 0)} size='large' />
+      <div className='overlay__reward__bottom'>
+        <Icon {...createIconProps(stars > 1)} size='small' />
+        <Icon {...createIconProps(stars > 2)} size='small' />
+      </div>
+    </div>
+    <div className='overlay__description overlay--text'>
+      You have created <large>1</large> reminder
+    </div>
+  </div>
+)
+
+// JS expressions should not break indentation
+// (https://github.com/mooz/js2-mode/issues/462).
+return (
+  <Router>
+    <Bar>
+      <Route exact path="/foo" render={() => (
+        <div>nothing</div>
+      )} />
+      <Route exact path="/bar" />
+    </Bar>
+  </Router>
+)
+
 // Local Variables:
 // indent-tabs-mode: nil
 // js-indent-level: 2
-- 
2.11.0


[-- Attachment #3: 0002-Refactor-JSX-indentation-code-to-improve-enclosing-J.patch --]
[-- Type: text/x-patch, Size: 19599 bytes --]

From c232c5f9dba926d8a80f0ebfe620fdfa7bfcbcbf Mon Sep 17 00:00:00 2001
From: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
Date: Sat, 9 Feb 2019 20:06:29 -0800
Subject: [PATCH] Refactor JSX indentation code to improve enclosing JSX
 discovery
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix a number of bugs reported for JSX indentation (caused by poor JSX
detection):

- https://github.com/mooz/js2-mode/issues/140#issuecomment-166250016
- https://github.com/mooz/js2-mode/issues/490
- Bug#24896 / https://github.com/mooz/js2-mode/issues/389 (with
respect to comments)
- Bug#26001 /
https://github.com/mooz/js2-mode/issues/389#issuecomment-271869380
- https://github.com/mooz/js2-mode/issues/411 / Bug#27000 /
https://github.com/mooz/js2-mode/issues/451

Potentially manifest some new bugs (due to false positives with ‘<’
and ‘>’ and SGML detection).  Slow down indentation a fair bit.

* list/progmodes/js.el (js-jsx-syntax, js--jsx-start-tag-re)
(js--looking-at-jsx-start-tag-p, js--looking-back-at-jsx-end-tag-p):
New variables and functions.
(js--jsx-find-before-tag, js--jsx-after-tag-re): Deleted.

(js--looking-at-operator-p): Don’t mistake a JSXOpeningElement for the
‘<’ operator.
(js--continued-expression-p): Don’t mistake a JSXClosingElement as a
fragment of a continued expression including the ‘>’ operator.

(js--as-sgml): Simplify.  Probably needn’t bind forward-sexp-function
to nil (sgml-mode already does) and probably shouldn’t bind
parse-sexp-lookup-properties to nil either (see Bug#24896).

(js--outermost-enclosing-jsx-tag-pos): Find enclosing JSX more
accurately than js--jsx-find-before-tag.  Use sgml-mode’s parsing
logic, rather than unreliable heuristics like paren-wrapping.  This
implementation is much slower; the previous implementation was fast,
but at the expense of accuracy.  To make up for all the grief we’ve
caused users, we will prefer accuracy over speed from now on.  That
said, this can still probably be optimized a lot.

(js--jsx-indented-element-p): Rename to js--jsx-indentation, since it
doesn’t just return a boolean.
(js--jsx-indentation): Refactor js--jsx-indented-element-p to simplify
the implementation as the improved accuracy of other code allows (and
to repent for some awful stylistic choices I made earlier).

(js--expression-in-sgml-indent-line): Rename to
js--indent-line-in-jsx-expression, since it’s a private function and
we can give it a name that reads more like English.
(js--indent-line-in-jsx-expression): Restructure point adjustment
logic more like js-indent-line.

(js--indent-n+1th-jsx-line): New function to complement
js--indent-line-in-jsx-expression.

(js-jsx-indent-line): Refactor.  Don’t bind js--continued-expression-p
to ignore any more; instead, rely on the improved accuracy of
js--continued-expression-p.

(js-jsx-mode): Set js-jsx-syntax to t.  For now, this will be the flag
we use to determine whether ‘JSX is enabled.’  (Maybe later, we will
refactor the code to use this variable instead of requiring
js-jsx-mode to be enabled, thus rendering the mode obsolete.)
---
 lisp/progmodes/js.el | 337 +++++++++++++++++++++------------------------------
 1 file changed, 141 insertions(+), 196 deletions(-)

diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el
index b0bb8213dc..e16ed98023 100644
--- a/lisp/progmodes/js.el
+++ b/lisp/progmodes/js.el
@@ -572,6 +572,15 @@ js-chain-indent
   :safe 'booleanp
   :group 'js)
 
+(defcustom js-jsx-syntax nil
+  "When non-nil, parse JavaScript with consideration for JSX syntax.
+This fixes indentation of JSX code in some cases.  It is set to
+be buffer-local when in `js-jsx-mode'."
+  :version "27.1"
+  :type 'boolean
+  :safe 'booleanp
+  :group 'js)
+
 ;;; KeyMap
 
 (defvar js-mode-map
@@ -1774,6 +1783,14 @@ js--indent-operator-re
           (js--regexp-opt-symbol '("in" "instanceof")))
   "Regexp matching operators that affect indentation of continued expressions.")
 
+(defconst js--jsx-start-tag-re
+  (concat "<" sgml-name-re)
+  "Regexp matching code that looks like a JSXOpeningElement.")
+
+(defun js--looking-at-jsx-start-tag-p ()
+  "Non-nil if a JSXOpeningElement immediately follows point."
+  (looking-at js--jsx-start-tag-re))
+
 (defun js--looking-at-operator-p ()
   "Return non-nil if point is on a JavaScript operator, other than a comma."
   (save-match-data
@@ -1796,7 +1813,9 @@ js--looking-at-operator-p
                  (js--backward-syntactic-ws)
                  ;; We might misindent some expressions that would
                  ;; return NaN anyway.  Shouldn't be a problem.
-                 (memq (char-before) '(?, ?} ?{))))))))
+                 (memq (char-before) '(?, ?} ?{)))))
+         ;; “<” isn’t necessarily an operator in JSX.
+         (not (and js-jsx-syntax (js--looking-at-jsx-start-tag-p))))))
 
 (defun js--find-newline-backward ()
   "Move backward to the nearest newline that is not in a block comment."
@@ -1816,6 +1835,14 @@ js--find-newline-backward
         (setq result nil)))
     result))
 
+(defconst js--jsx-end-tag-re
+  (concat "</" sgml-name-re ">\\|/>")
+  "Regexp matching a JSXClosingElement.")
+
+(defun js--looking-back-at-jsx-end-tag-p ()
+  "Non-nil if a JSXClosingElement immediately precedes point."
+  (looking-back js--jsx-end-tag-re (point-at-bol)))
+
 (defun js--continued-expression-p ()
   "Return non-nil if the current line continues an expression."
   (save-excursion
@@ -1833,12 +1860,19 @@ js--continued-expression-p
       (and (js--find-newline-backward)
            (progn
              (skip-chars-backward " \t")
-             (or (bobp) (backward-char))
-             (and (> (point) (point-min))
-                  (save-excursion (backward-char) (not (looking-at "[/*]/\\|=>")))
-                  (js--looking-at-operator-p)
-                  (and (progn (backward-char)
-                              (not (looking-at "+\\+\\|--\\|/[/*]"))))))))))
+             (and
+              ;; The “>” at the end of any JSXBoundaryElement isn’t
+              ;; part of a continued expression.
+              (not (and js-jsx-syntax (js--looking-back-at-jsx-end-tag-p)))
+              (progn
+                (or (bobp) (backward-char))
+                (and (> (point) (point-min))
+                     (save-excursion
+                       (backward-char)
+                       (not (looking-at "[/*]/\\|=>")))
+                     (js--looking-at-operator-p)
+                     (and (progn (backward-char)
+                                 (not (looking-at "+\\+\\|--\\|/[/*]"))))))))))))
 
 (defun js--skip-term-backward ()
   "Skip a term before point; return t if a term was skipped."
@@ -2153,190 +2187,108 @@ js--proper-indentation
 
 ;;; JSX Indentation
 
-(defsubst js--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 js--jsx-end-tag-re
-  (concat "</" sgml-name-re ">\\|/>")
-  "Find the end of a JSX element.")
-
-(defconst js--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 js--jsx-indented-element-p ()
+(defmacro js--as-sgml (&rest body)
+  "Execute BODY as if in sgml-mode."
+  `(with-syntax-table sgml-mode-syntax-table
+     ,@body))
+
+(defun js--outermost-enclosing-jsx-tag-pos ()
+  (let (context tag-pos last-tag-pos parse-status parens paren-pos curly-pos)
+    (js--as-sgml
+      ;; Search until we reach the top or encounter the start of a
+      ;; JSXExpressionContainer (implying nested JSX).
+      (while (and (setq context (sgml-get-context))
+                  (progn
+                    (setq tag-pos (sgml-tag-start (car (last context))))
+                    (or (not curly-pos)
+                        ;; Stop before curly brackets (start of a
+                        ;; JSXExpressionContainer).
+                        (> tag-pos curly-pos))))
+        ;; Record this position so it can potentially be returned.
+        (setq last-tag-pos tag-pos)
+        ;; Always parse sexps / search for the next context from the
+        ;; immediately enclosing tag (sgml-get-context may not leave
+        ;; point there).
+        (goto-char tag-pos)
+        (unless parse-status ; Don’t needlessly reparse.
+          ;; Search upward for an enclosing starting curly bracket.
+          (setq parse-status (syntax-ppss))
+          (setq parens (reverse (nth 9 parse-status)))
+          (while (and (setq paren-pos (car parens))
+                      (not (when (= (char-after paren-pos) ?{)
+                             (setq curly-pos paren-pos))))
+            (setq parens (cdr parens)))
+          ;; Always search for the next context from the immediately
+          ;; enclosing tag (calling syntax-ppss in the above loop
+          ;; may move point from there).
+          (goto-char tag-pos))))
+    last-tag-pos))
+
+(defun js--jsx-indentation ()
   "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')
-  );"
+Return nil for first JSXElement line (indent like JS).
+Return `n+1th' for second+ JSXElement lines (indent like SGML).
+Return `expression' for lines within embedded JS expressions
+  (indent like JS inside SGML).
+Return nil for non-JSX lines."
   (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)
+        tag-start-pos 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 (js--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 js--jsx-end-tag-re nil t)))
-             (while (forward-comment 1))
-             (when (looking-at js--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
+      ;; Determine if inside a JSXElement.
+      (beginning-of-line) ; For exclusivity
+      (when (setq tag-start-pos (js--outermost-enclosing-jsx-tag-pos))
+        ;; Check if inside an embedded multi-line JS expression.
+        (goto-char current-pos)
+        (end-of-line) ; For exclusivity
+        (setq parens (nth 9 (syntax-ppss)))
+        (while
+            (and
+             (setq paren (car parens))
+             (if (and
+                  (>= paren tag-start-pos)
+                  ;; A curly bracket indicates the start of an
+                  ;; embedded expression.
+                  (= (char-after paren) ?{)
+                  ;; 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 js--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 js--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.
+                  ;; (exclusive, as the closing bracket is indented
+                  ;; like SGML).
+                  (if (progn
+                        (goto-char paren)
+                        (ignore-errors (let (forward-sexp-function)
+                                         (forward-sexp))))
+                      (< current-line (line-number-at-pos))
+                    ;; No matching bracket implies we’re inside!
+                    t))
+                 ;; Indicate this will be indented specially.  Return
+                 ;; nil to stop iterating too.
+                 (progn (setq type 'expression) nil)
+               ;; Stop iterating when parens = nil.
+               (setq parens (cdr parens)))))
+        (or type 'n+1th)))))
+
+(defun js--indent-line-in-jsx-expression ()
+  "Indent the current line as JavaScript within JSX."
+  (let ((parse-status (save-excursion (syntax-ppss (point-at-bol))))
+        offset indent-col)
     (unless (nth 3 parse-status)
-      (setq indent-col (save-excursion
-                         (back-to-indentation)
-                         (if (>= (point) savep) (setq savep nil))
-                         (js--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 (js--proper-indentation parse-status)
-                              (+ indent-col js-indent-level)))
-        (if savep
-            (save-excursion (indent-line-to indent-col))
-          (indent-line-to indent-col))))))
+      (save-excursion
+        (setq offset (- (point) (progn (back-to-indentation) (point)))
+              indent-col (js--as-sgml (sgml-calculate-indent))))
+      (if (null indent-col) 'noindent ; Like in sgml-mode
+        ;; Use whichever indentation column is greater, such that the
+        ;; SGML column is effectively a minimum.
+        (indent-line-to (max (js--proper-indentation parse-status)
+                             (+ indent-col js-indent-level)))
+        (when (> offset 0) (forward-char offset))))))
+
+(defun js--indent-n+1th-jsx-line ()
+  "Indent the current line as JSX within JavaScript."
+  (js--as-sgml (sgml-indent-line)))
 
 (defun js-indent-line ()
   "Indent the current line as JavaScript."
@@ -2353,19 +2305,11 @@ js-jsx-indent-line
 i.e., customize JSX element indentation with `sgml-basic-offset',
 `sgml-attribute-offset' et al."
   (interactive)
-  (let ((indentation-type (js--jsx-indented-element-p)))
-    (cond
-     ((eq indentation-type 'expression)
-      (js--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 #'js--continued-expression-p) 'ignore))
-        (js-indent-line)))
-     ((eq indentation-type 'nth)
-      (js--as-sgml (sgml-indent-line)))
-     (t (js-indent-line)))))
+  (let ((type (js--jsx-indentation)))
+    (if type
+        (if (eq type 'n+1th) (js--indent-n+1th-jsx-line)
+          (js--indent-line-in-jsx-expression))
+      (js-indent-line))))
 
 ;;; Filling
 
@@ -3944,6 +3888,7 @@ js-jsx-mode
     (setq-local sgml-basic-offset js-indent-level))
   (add-hook \\='js-jsx-mode-hook #\\='set-jsx-indentation)"
   :group 'js
+  (setq-local js-jsx-syntax t)
   (setq-local indent-line-function #'js-jsx-indent-line))
 
 ;;;###autoload (defalias 'javascript-mode 'js-mode)
-- 
2.11.0


[-- Attachment #4: 0003-Add-new-failing-unclosed-JSX-test-and-separate-such-.patch --]
[-- Type: text/x-patch, Size: 2585 bytes --]

From 54b8b1cb4ab0d4041b4e0613af0bc71e594aecbc Mon Sep 17 00:00:00 2001
From: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
Date: Sun, 10 Feb 2019 21:11:17 -0800
Subject: [PATCH] Add new (failing) unclosed JSX test and separate such tests

* test/manual/indent/js-jsx.js: Move test with intentional scan error to
its own file, js-jsx-unclosed-1.js.
* test/manual/indent/js-jsx-unclosed-1.js: New file.
* test/manual/indent/js-jsx-unclosed-2.js: New file with test for
regression caused by new ambiguous parsing of JS/JSX.
---
 test/manual/indent/js-jsx-unclosed-1.js | 15 +++++++++++++++
 test/manual/indent/js-jsx-unclosed-2.js | 17 +++++++++++++++++
 test/manual/indent/js-jsx.js            |  9 ---------
 3 files changed, 32 insertions(+), 9 deletions(-)
 create mode 100644 test/manual/indent/js-jsx-unclosed-1.js
 create mode 100644 test/manual/indent/js-jsx-unclosed-2.js

diff --git a/test/manual/indent/js-jsx-unclosed-1.js b/test/manual/indent/js-jsx-unclosed-1.js
new file mode 100644
index 0000000000..9418aed7a1
--- /dev/null
+++ b/test/manual/indent/js-jsx-unclosed-1.js
@@ -0,0 +1,15 @@
+// -*- mode: js-jsx; -*-
+
+// Local Variables:
+// indent-tabs-mode: nil
+// js-indent-level: 2
+// End:
+
+// The following test goes below any comments to avoid including
+// misindented comments among the erroring lines.
+
+return (
+  <div>
+    {array.map(function () {
+      return {
+        a: 1
diff --git a/test/manual/indent/js-jsx-unclosed-2.js b/test/manual/indent/js-jsx-unclosed-2.js
new file mode 100644
index 0000000000..2d42cf70f8
--- /dev/null
+++ b/test/manual/indent/js-jsx-unclosed-2.js
@@ -0,0 +1,17 @@
+// -*- mode: js-jsx; -*-
+
+// Local Variables:
+// indent-tabs-mode: nil
+// js-indent-level: 2
+// End:
+
+// The following tests go below any comments to avoid including
+// misindented comments among the erroring lines.
+
+// Don’t misinterpret equality operators as JSX.
+for (; i < length;) void 0
+if (foo > bar) void 0
+
+// Don’t even misinterpret unary operators as JSX.
+if (foo < await bar) void 0
+while (await foo > bar) void 0
diff --git a/test/manual/indent/js-jsx.js b/test/manual/indent/js-jsx.js
index 35ca4b275a..af3c340559 100644
--- a/test/manual/indent/js-jsx.js
+++ b/test/manual/indent/js-jsx.js
@@ -257,12 +257,3 @@ return (
 // indent-tabs-mode: nil
 // js-indent-level: 2
 // End:
-
-// The following test has intentionally unclosed elements and should
-// be placed below all other tests to prevent awkward indentation.
-
-return (
-  <div>
-    {array.map(function () {
-      return {
-        a: 1
-- 
2.11.0


[-- Attachment #5: 0004-js-syntax-propertize-Disambiguate-JS-from-JSX-fixing.patch --]
[-- Type: text/x-patch, Size: 7546 bytes --]

From 300a4b00af57f6e904f239ec24a85a78aa8989f5 Mon Sep 17 00:00:00 2001
From: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
Date: Mon, 11 Feb 2019 03:00:34 -0800
Subject: [PATCH] js-syntax-propertize: Disambiguate JS from JSX, fixing some
 indents
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix some JSX indentation bugs:

- Bug#24896 / https://github.com/mooz/js2-mode/issues/389
- Bug#30225
- https://github.com/mooz/js2-mode/issues/459

* lisp/progmodes/js.el (js--dotted-captured-name-re)
(js--unary-keyword-re, js--unary-keyword-p)
(js--disambiguate-beginning-of-jsx-tag)
(js--disambiguate-end-of-jsx-tag)
(js--disambiguate-js-from-jsx): New variables and functions.

(js-syntax-propertize): Additionally clarify when syntax is JS so that
‘(with-syntax-table sgml-mode-syntax-table …)’ does not mistake some
JS punctuation syntax for SGML parenthesis syntax, namely ‘<’ and ‘>’.

* test/manual/indent/js-jsx-unclosed-2.js: Add additional test for
unary operator parsing.
---
 lisp/progmodes/js.el                    | 100 +++++++++++++++++++++++++++++++-
 test/manual/indent/js-jsx-unclosed-2.js |  14 +++++
 2 files changed, 113 insertions(+), 1 deletion(-)

diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el
index e16ed98023..aad8e232cd 100644
--- a/lisp/progmodes/js.el
+++ b/lisp/progmodes/js.el
@@ -82,6 +82,10 @@ js--dotted-name-re
   (concat js--name-re "\\(?:\\." js--name-re "\\)*")
   "Regexp matching a dot-separated sequence of JavaScript names.")
 
+(defconst js--dotted-captured-name-re
+  (concat "\\(" js--name-re "\\)\\(?:\\." js--name-re "\\)*")
+  "Like `js--dotted-name-re', but capture the first name.")
+
 (defconst js--cpp-name-re js--name-re
   "Regexp matching a C preprocessor name.")
 
@@ -1731,6 +1735,99 @@ js-syntax-propertize-regexp
                            'syntax-table (string-to-syntax "\"/"))
         (goto-char end)))))
 
+(defconst js--unary-keyword-re
+  (js--regexp-opt-symbol '("await" "delete" "typeof" "void" "yield"))
+  "Regexp matching unary operator keywords.")
+
+(defun js--unary-keyword-p (string)
+  "Check if STRING is a unary operator keyword in JavaScript."
+  (string-match-p js--unary-keyword-re string))
+
+(defun js--disambiguate-beginning-of-jsx-tag ()
+  "Parse enough to determine if a JSX tag starts here.
+Disambiguate JSX from equality operators by testing for syntax
+only valid as JSX."
+  ;; “</…” - a JSXClosingElement.
+  ;; “<>” - a JSXOpeningFragment.
+  (if (memq (char-after) '(?\/ ?\>)) t
+    (save-excursion
+     (skip-chars-forward " \t\n")
+     (and
+      (looking-at js--dotted-captured-name-re)
+      ;; Don’t match code like “if (i < await foo)”
+      (not (js--unary-keyword-p (match-string 1)))
+      (progn
+        (goto-char (match-end 0))
+        (skip-chars-forward " \t\n")
+        (or
+         ;; “>”, “/>” - tag enders.
+         ;; “{” - a JSXExpressionContainer.
+         (memq (char-after) '(?\> ?\/ ?\{))
+         ;; Check if a JSXAttribute follows.
+         (looking-at js--name-start-re)))))))
+
+(defun js--disambiguate-end-of-jsx-tag ()
+  "Parse enough to determine if a JSX tag ends here.
+Disambiguate JSX from equality operators by testing for syntax
+only valid as JSX, or extremely unlikely except as JSX."
+  (save-excursion
+    (backward-char)
+    ;; “…/>” - a self-closing JSXOpeningElement.
+    ;; “</>” - a JSXClosingFragment.
+    (if (= (char-before) ?/) t
+      (let (last-tag-or-attr-name last-non-unary-p)
+        (catch 'match
+          (while t
+            (skip-chars-backward " \t\n")
+            ;; Check if the end of a JSXAttribute value or
+            ;; JSXExpressionContainer almost certainly precedes.
+            ;; The only valid JS this misses is
+            ;; - {} > foo
+            ;; - "bar" > foo
+            ;; which is no great loss, IMHO…
+            (if (memq (char-before) '(?\} ?\" ?\' ?\`)) (throw 'match t)
+              (if (and last-tag-or-attr-name last-non-unary-p
+                       ;; “<”, “</” - tag starters.
+                       (memq (char-before) '(?\< ?\/)))
+                  ;; Leftmost name parsed was the name of a
+                  ;; JSXOpeningElement.
+                  (throw 'match t))
+              ;; Technically the dotted name could span multiple
+              ;; lines, but dear God WHY?!  Also, match greedily to
+              ;; ensure the entire name is valid.
+              (if (looking-back js--dotted-captured-name-re (point-at-bol) t)
+                  (if (and (setq last-non-unary-p (not (js--unary-keyword-p (match-string 1))))
+                           last-tag-or-attr-name)
+                      ;; Valid (non-unary) name followed rightwards by
+                      ;; another name (any will do, including
+                      ;; keywords) is invalid JS, but valid JSX.
+                      (throw 'match t)
+                    ;; Remember match and skip backwards over it when
+                    ;; it is the first matched name or the N+1th
+                    ;; matched unary name (unary names on the left are
+                    ;; still ambiguously JS or JSX, so keep parsing to
+                    ;; disambiguate).
+                    (setq last-tag-or-attr-name (match-string 1))
+                    (goto-char (match-beginning 0)))
+                ;; Nothing else to look for; give up parsing.
+                (throw 'match nil)))))))))
+
+(defun js--disambiguate-js-from-jsx (start end)
+  "Figure out which ‘<’ and ‘>’ chars (from START to END) aren’t JSX.
+
+Later, this info prevents ‘sgml-’ functions from treating some
+‘<’ and ‘>’ chars as parts of tokens of SGML tags — a good thing,
+since they are serving their usual function as some JS equality
+operator or arrow function, instead."
+  (goto-char start)
+  (while (re-search-forward "[<>]" end t)
+    (unless (if (eq (char-before) ?<) (js--disambiguate-beginning-of-jsx-tag)
+              (js--disambiguate-end-of-jsx-tag))
+      ;; Inform sgml- functions that this >, >=, >>>, <, <=, <<<, or
+      ;; => token is punctuation (and not an open or close parenthesis
+      ;; as per usual in sgml-mode).
+      (put-text-property (1- (point)) (point) 'syntax-table '(1)))))
+
 (defun js-syntax-propertize (start end)
   ;; JavaScript allows immediate regular expression objects, written /.../.
   (goto-char start)
@@ -1758,7 +1855,8 @@ js-syntax-propertize
                               'syntax-table (string-to-syntax "\"/"))
            (js-syntax-propertize-regexp end)))))
     ("\\`\\(#\\)!" (1 "< b")))
-   (point) end))
+   (point) end)
+  (if js-jsx-syntax (js--disambiguate-js-from-jsx start end)))
 
 (defconst js--prettify-symbols-alist
   '(("=>" . ?⇒)
diff --git a/test/manual/indent/js-jsx-unclosed-2.js b/test/manual/indent/js-jsx-unclosed-2.js
index 2d42cf70f8..8b6f33325d 100644
--- a/test/manual/indent/js-jsx-unclosed-2.js
+++ b/test/manual/indent/js-jsx-unclosed-2.js
@@ -15,3 +15,17 @@ if (foo > bar) void 0
 // Don’t even misinterpret unary operators as JSX.
 if (foo < await bar) void 0
 while (await foo > bar) void 0
+
+// Allow unary keyword names as null-valued JSX attributes.
+// (As if this will EVER happen…)
+<Foo yield>
+  <Bar void>
+    <Baz
+      zorp
+      typeof>
+      <Please do_n0t delete this_stupidTest >
+        How would we ever live without unary support
+      </Please>
+    </Baz>
+  </Bar>
+</Foo>
-- 
2.11.0


^ permalink raw reply related	[flat|nested] 33+ messages in thread
* Re: Comprehensive JSX support in Emacs
@ 2019-02-15  6:38 Jackson Ray Hamilton
  2019-02-17  6:10 ` Marcin Borkowski
  0 siblings, 1 reply; 33+ messages in thread
From: Jackson Ray Hamilton @ 2019-02-15  6:38 UTC (permalink / raw)
  To: mbork; +Cc: emacs-devel

Hi Marcin,

I’m glad to hear that you’re interested.

Sorry for the TL;DR.  Indentation seems like a complex topic, so I 
wanted to extract from my jumbled thoughts a sufficient amount of 
context to help anyone else along.

I look forward to working with you to improve Emacs.

Jackson

(BTW, I’m currently figuring out the process for replying to a mailing 
list — replying to you, CCing emacs-devel, trying to set In-Reply-To in 
my mail client; hopefully that works.)




^ permalink raw reply	[flat|nested] 33+ messages in thread

end of thread, other threads:[~2019-10-20 16:57 UTC | newest]

Thread overview: 33+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2019-02-14  5:06 Comprehensive JSX support in Emacs Jackson Ray Hamilton
2019-02-14  8:03 ` Marcin Borkowski
2019-02-14 14:10 ` Stefan Monnier
2019-02-14 15:04   ` Clément Pit-Claudel
2019-02-15  8:21   ` Jackson Ray Hamilton
2019-02-15 13:25     ` Stefan Monnier
2019-02-15 13:54     ` Dmitry Gutov
2019-02-15 14:39 ` Dmitry Gutov
2019-02-16 20:50 ` Jostein Kjønigsen
2019-02-18  7:17   ` Jackson Ray Hamilton
2019-03-27  8:03 ` Jackson Ray Hamilton
2019-03-27  9:36   ` Marcin Borkowski
2019-03-30  2:18     ` Jackson Ray Hamilton
2019-04-02  1:12       ` Dmitry Gutov
2019-04-06 16:09         ` Jackson Ray Hamilton
2019-03-30  2:08   ` Jackson Ray Hamilton
2019-04-02  1:07   ` Dmitry Gutov
2019-04-02 11:23     ` Stefan Monnier
2019-04-06 16:02       ` Jackson Ray Hamilton
2019-04-07 23:19         ` Jackson Ray Hamilton
2019-04-09  6:06       ` Jackson Ray Hamilton
2019-04-09  8:12         ` Eli Zaretskii
2019-04-10  1:56           ` Jackson Ray Hamilton
2019-04-10 15:43             ` Eli Zaretskii
2019-04-06 15:55     ` Jackson Ray Hamilton
2019-04-07  0:31       ` Dmitry Gutov
2019-04-09  6:16         ` Jackson Ray Hamilton
2019-04-09  7:10           ` Marcin Borkowski
2019-04-09  8:00             ` Jackson Ray Hamilton
2019-04-11 19:51               ` Marcin Borkowski
2019-10-20 16:57                 ` Steinar Bang
  -- strict thread matches above, loose matches on Subject: below --
2019-02-15  6:38 Jackson Ray Hamilton
2019-02-17  6:10 ` Marcin Borkowski

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).