From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!.POSTED.blaine.gmane.org!not-for-mail From: Jackson Ray Hamilton Newsgroups: gmane.emacs.devel Subject: Comprehensive JSX support in Emacs Date: Wed, 27 Mar 2019 01:03:21 -0700 Message-ID: <8f533e76-302f-bd45-9eb5-f23b0bbd0ce8@jacksonrayhamilton.com> References: <1423022755.65233.1550120813763@privateemail.com> Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="------------D54DAF1A67FF0115BB5D3891" Injection-Info: blaine.gmane.org; posting-host="blaine.gmane.org:195.159.176.226"; logging-data="80506"; mail-complaints-to="usenet@blaine.gmane.org" User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.5.1 Cc: dgutov@yandex.ru, monnier@iro.umontreal.ca, emacs-devel@gnu.org To: jackson@jacksonrayhamilton.com Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Wed Mar 27 09:04:58 2019 Return-path: Envelope-to: ged-emacs-devel@m.gmane.org Original-Received: from lists.gnu.org ([209.51.188.17]) by blaine.gmane.org with esmtps (TLS1.0:RSA_AES_256_CBC_SHA1:256) (Exim 4.89) (envelope-from ) id 1h93YK-000KYl-Pi for ged-emacs-devel@m.gmane.org; Wed, 27 Mar 2019 09:04:57 +0100 Original-Received: from localhost ([127.0.0.1]:43476 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1h93YJ-0008PU-JQ for ged-emacs-devel@m.gmane.org; Wed, 27 Mar 2019 04:04:51 -0400 Original-Received: from eggs.gnu.org ([209.51.188.92]:43168) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1h93XF-0008P0-AZ for emacs-devel@gnu.org; Wed, 27 Mar 2019 04:03:58 -0400 Original-Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1h93Wz-0001Re-V2 for emacs-devel@gnu.org; Wed, 27 Mar 2019 04:03:43 -0400 Original-Received: from mta-06-3.privateemail.com ([198.54.127.59]:11655) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1h93Wx-0001GU-4n for emacs-devel@gnu.org; Wed, 27 Mar 2019 04:03:29 -0400 Original-Received: from MTA-06.privateemail.com (localhost [127.0.0.1]) by MTA-06.privateemail.com (Postfix) with ESMTP id 599E86004D; Wed, 27 Mar 2019 04:03:22 -0400 (EDT) Original-Received: from [192.168.88.227] (unknown [10.20.151.224]) by MTA-06.privateemail.com (Postfix) with ESMTPA id C11416003D; Wed, 27 Mar 2019 08:03:21 +0000 (UTC) In-Reply-To: <1423022755.65233.1550120813763@privateemail.com> Content-Language: en-US X-Virus-Scanned: ClamAV using ClamSMTP X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] X-Received-From: 198.54.127.59 X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Original-Sender: "Emacs-devel" Xref: news.gmane.org gmane.emacs.devel:234763 Archived-At: This is a multi-part message in MIME format. --------------D54DAF1A67FF0115BB5D3891 Content-Type: multipart/alternative; boundary="------------427AE7E08752745D28CA8434" --------------427AE7E08752745D28CA8434 Content-Type: text/plain; charset=utf-8; format=flowed Content-Transfer-Encoding: quoted-printable Hello Emacs maintainers, Following up on my long email from a month ago, wherein I announced my=20 plan to fully support editing JSX in js-mode=E2=80=A6 After many mornings and evenings of hacking (and many moonlight-melted=20 candlesticks, and many moments spent meandering and muttering), I=E2=80=99= ve=20 finally got the JavaScript+JSX implementation to a state where I feel=20 it=E2=80=99s worth sharing again. I=E2=80=99ve added pretty comprehensive indentation and font-locking supp= ort,=20 along with relatively simple automatic JSX detection support. The indentation and font-locking code complimented each other well.=C2=A0= In=20 order to fix issues like the one with an unterminated quote inside JSX=20 (https://github.com/mooz/js2-mode/issues/409) (which truly haunted me=20 for the past years) I had to thoroughly parse the JSX code.=C2=A0 The=20 information I gained from parsing eventually led to the resolution of=20 all the failing indentation test cases I compiled, and some new ones I=20 added along the way. js-mode no longer relies on sgml-mode for any parsing or indentation.=C2=A0= =20 It=E2=80=99s all performed precisely according to the semantics of JSX no= w,=20 borrowing just two blocks of code from sgml-mode as a basis for=20 overlapping indentation logic.=C2=A0 Also, the JSX code has pretty colors= =20 now.=C2=A0 And, a single unterminated quote doesn=E2=80=99t wreak havoc o= n the buffer=20 any more. I implemented pretty much all of the JSX detection logic I proposed, at=20 least in spirit.=C2=A0 I immediately found that using a list called=20 =E2=80=9Cjs-syntax-extensions=E2=80=9D would be inferior to the alternati= ve of using a=20 boolean called =E2=80=9Cjs-jsx-syntax=E2=80=9D.=C2=A0 Booleans are simple= r, and easier for=20 users to set given all the available methods, plus they=E2=80=99re fun to= =20 check-off in Customize. I mentioned that a =E2=80=9Cjs-syntax-extensions=E2=80=9D list might have= some esoteric=20 advantage by its ordering resolving theoretical conflicts between=20 multiple syntax extensions, but I figure 1) we can cross that bridge=20 when we get there, and 2) if we did add more syntax extensions for JS=20 /and/ those syntax extensions did partially conflict /and/ users wanted=20 to use conflicting extensions simultaneously, perhaps it=E2=80=99d be bet= ter to=20 add some booleans that would resolve such conflicts on a case-by-case=20 basis, anyway. Taking into consideration Dmitry=E2=80=99s ambivalence on deprecating=20 js-jsx-mode and js-jsx-indent-line, I figured we could strike a=20 compromise.=C2=A0 We can keep these APIs without marking them obsolete.=C2= =A0=20 However, I=E2=80=99ve tried to make the automatic detection of JSX=E2=80=94= and the=20 viable alternatives to manually enabling JSX support=E2=80=94/so effectiv= e/ and=20 numerous as to render these functions superfluous relics.=C2=A0 The=20 go-forward answer to =E2=80=9Chow to enable JSX support in Emacs?=E2=80=9D= should be=20 =E2=80=9Cupgrade to Emacs 27=E2=80=94now it just works=E2=84=A2=E2=80=9D. I also addressed the spelling / naming items that Stefan brought up. I had a chance to play with the code in a few projects of mine over the=20 past week, and generally it is working well for me=E2=80=94much better th= an=20 before, in all the previously painful cases.=C2=A0 However, there is a=20 noticeable delay when editing some lines in my 1000-line monolith.jsx=20 file, and in some rare cases I am seeing font-locking not working for JS=20 embedded in JSX, and font-locking could probably be more graceful when=20 typing new JSX code, too.=C2=A0 So there are still some improvements that= can=20 be made, but they may be entering the territory of =E2=80=9Cmaintenance.=E2= =80=9D=C2=A0=20 Therefore, we might be willing to unleash this beast sooner rather than=20 later, in spite of some imperfections. Time for the good part: a whole bunch of patches, freshly rebased on=20 master (see attached).=C2=A0 The first 4 patches should be the same as th= e=20 ones in my original =E2=80=9Cannouncement=E2=80=9D email (except for one = merge conflict=20 I fixed), and the following 15 patches add up to some pretty kick-butt=20 =E2=80=9CHTML-in-JavaScript=E2=80=9D splendiferousness (IMHO). (Note that the patch=20 =E2=80=9C0015-Indent-broken-arrow-function-bodies-as-an-N-1th-arg.patch=E2= =80=9D isn=E2=80=99t=20 really related to the JSX feature.=C2=A0 I was just editing some code in = a=20 project of mine, and found that this change provided more desirable=20 behavior with the code I was writing then.=C2=A0 A lot of my code using J= SX=20 also uses arrow functions, so this was a convenient patch for me to lump=20 in.) I invite people to provide feedback as I continue to battle-test and=20 optimize the code.=C2=A0 Feedback regarding improving the reliability and= =20 performance of parsing and font-locking would be especially appreciated. Thanks, Jackson --------------427AE7E08752745D28CA8434 Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable

Hello Emacs maintainers,

Following up on my long email from a month ago, wherein I announced my plan to fully support editing JSX in js-mode=E2=80=A6<= /p>

After many mornings and evenings of hacking (and many moonlight-melted candlesticks, and many moments spent meandering and muttering), I=E2=80=99ve finally got the JavaScript+JSX impleme= ntation to a state where I feel it=E2=80=99s worth sharing again.

I=E2=80=99ve added pretty comprehensive indentation and font-locki= ng support, along with relatively simple automatic JSX detection support.

The indentation and font-locking code complimented each other well.=C2=A0 In order to fix issues like the one with an unterminate= d quote inside JSX (https://github.com/mooz/js2-mode/i= ssues/409) (which truly haunted me for the past years) I had to thoroughly parse the JSX code.=C2=A0 The information I gained from parsing eventually led to the resolution of all the failing indentation test cases I compiled, and some new ones I added along the way.

js-mode no longer relies on sgml-mode for any parsing or indentation.=C2=A0 It=E2=80=99s all performed precisely according t= o the semantics of JSX now, borrowing just two blocks of code from sgml-mode as a basis for overlapping indentation logic.=C2=A0 Also,= the JSX code has pretty colors now.=C2=A0 And, a single unterminated qu= ote doesn=E2=80=99t wreak havoc on the buffer any more.

I implemented pretty much all of the JSX detection logic I proposed, at least in spirit.=C2=A0 I immediately found that using = a list called =E2=80=9Cjs-syntax-extensions=E2=80=9D would be inferio= r to the alternative of using a boolean called =E2=80=9Cjs-jsx-syntax=E2=80=9D= .=C2=A0 Booleans are simpler, and easier for users to set given all the available methods, plus they=E2=80=99re fun to check-off in Customize.

I mentioned that a =E2=80=9Cjs-syntax-extensions=E2=80=9D list mig= ht have some esoteric advantage by its ordering resolving theoretical conflicts between multiple syntax extensions, but I figure 1) we can cross that bridge when we get there, and 2) if we did add more syntax extensions for JS and those syntax extensions did partially conflict and users wanted to use conflicting extensions simultaneously, perhaps it=E2=80=99d be better to add so= me booleans that would resolve such conflicts on a case-by-case basis, anyway.

Taking into consideration Dmitry=E2=80=99s ambivalence on deprecat= ing js-jsx-mode and js-jsx-indent-line, I figured we could strike a compromise.=C2=A0 We can keep these APIs without marking them obsolete.=C2=A0 However, I=E2=80=99ve tried to make the automatic d= etection of JSX=E2=80=94and the viable alternatives to manually enabling JSX su= pport=E2=80=94so effective and numerous as to render these functions superfluous relics.=C2=A0 The go-forward answer to =E2=80=9Chow to = enable JSX support in Emacs?=E2=80=9D should be =E2=80=9Cupgrade to Emacs 27=E2= =80=94now it just works=E2=84=A2=E2=80=9D.

I also addressed the spelling / naming items that Stefan brought up.

I had a chance to play with the code in a few projects of mine over the past week, and generally it is working well for me=E2=80=94= much better than before, in all the previously painful cases.=C2=A0 Howe= ver, there is a noticeable delay when editing some lines in my 1000-line monolith.jsx file, and in some rare cases I am seeing font-locking not working for JS embedded in JSX, and font-locking could probably be more graceful when typing new JSX code, too.=C2=A0= So there are still some improvements that can be made, but they may be entering the territory of =E2=80=9Cmaintenance.=E2=80=9D=C2=A0 T= herefore, we might be willing to unleash this beast sooner rather than later, in spite of some imperfections.

Time for the good part: a whole bunch of patches, freshly rebased on master (see attached).=C2=A0 The first 4 patches should be the s= ame as the ones in my original =E2=80=9Cannouncement=E2=80=9D email (ex= cept for one merge conflict I fixed), and the following 15 patches add up to some pretty kick-butt =E2=80=9CHTML-in-JavaScript=E2=80=9D splendif= erousness (IMHO).

(Note that the patch =E2=80=9C0015-Indent-broken-arrow-function-bodies-as-an-N-1th-arg.p= atch=E2=80=9D isn=E2=80=99t really related to the JSX feature.=C2=A0 I was just e= diting some code in a project of mine, and found that this change provided more desirable behavior with the code I was writing then.=C2=A0 A l= ot of my code using JSX also uses arrow functions, so this was a convenient patch for me to lump in.)

I invite people to provide feedback as I continue to battle-test and optimize the code.=C2=A0 Feedback regarding improving the reliability and performance of parsing and font-locking would be especially appreciated.

Thanks,

Jackson

--------------427AE7E08752745D28CA8434-- --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0001-Add-failing-tests-for-JSX-indentation-bugs.patch" Content-Disposition: attachment; filename="0001-Add-failing-tests-for-JSX-indentation-bugs.patch" Content-Transfer-Encoding: quoted-printable >From 0ccfa924e21dd9db92fa9ecfff6f1cfb4821a73e Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sat, 9 Feb 2019 15:42:42 -0800 Subject: [PATCH 01/19] Add failing tests for JSX indentation bugs MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-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=E2=80=99ll 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-j= sx-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
C'est Montr=C3=A9al
; +} +function Test(foo =3D /'/, + bar =3D 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 ( ); =20 +// Indent void expressions (no need for contextual parens / commas) +// (https://github.com/mooz/js2-mode/issues/140#issuecomment-166250016). +
+

Title

+ {array.map(() =3D> { + return ; + })} + {message} +
+// Another example of above issue +// (https://github.com/mooz/js2-mode/issues/490). + +
+ {variable1} + +
+
+ +// Comments and arrows can break indentation (Bug#24896 / +// https://github.com/mooz/js2-mode/issues/389). +const Component =3D props =3D> ( + c} + b=3D{123}> + +); +const Component =3D props =3D> ( + + +); +const Component =3D props =3D> ( // Parse this comment, please. + c} + b=3D{123}> + +); +const Component =3D props =3D> ( // Parse this comment, please. + + +); +// Another example of above issue (Bug#30225). +class { + render() { + return ( + + ); + } +} + +// JSX attributes of an arrow function=E2=80=99s expression body=E2=80=99= 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 =3D this.state.messages.map( + message =3D> + ); return messages; + } + render() { + const messages =3D this.state.messages.map(message =3D> + + ); return messages; + } +} + +// Users expect tag closers to align with the tag=E2=80=99s 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 =3D (props) =3D> ( +
+ i} + /> + +
+); + +// Embedded JSX in parens breaks indentation +// (https://github.com/mooz/js2-mode/issues/411). +let a =3D ( +
+ {condition && } + {condition && } +
+
+) +let b =3D ( +
+ {condition && ()} +
+
+) +let c =3D ( +
+ {condition && ()} + {condition && "something"} +
+) +let d =3D ( +
+ {()} + {condition && "something"} +
+) +// Another example of the above issue (Bug#27000). +function testA() { + return ( +
+
{ (
) }
+
+ ); +} +function testB() { + return ( +
+
{
}
+
+ ); +} +// Another example of the above issue +// (https://github.com/mooz/js2-mode/issues/451). +class Classy extends React.Component { + render () { + return ( +
+
    + { this.state.list.map((item) =3D> { + return (
    ) + })} +
+
+ ) + } +} + +// Self-closing tags should be indented properly +// (https://github.com/mooz/js2-mode/issues/459). +export default ({ stars }) =3D> ( +
+
+ Congratulations! +
+
+ 0)} size=3D'large' /> +
+ 1)} size=3D'small' /> + 2)} size=3D'small' /> +
+
+
+ You have created 1 reminder +
+
+) + +// JS expressions should not break indentation +// (https://github.com/mooz/js2-mode/issues/462). +return ( + + + ( +
nothing
+ )} /> + +
+
+) + // Local Variables: // indent-tabs-mode: nil // js-indent-level: 2 --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0002-Refactor-JSX-indentation-code-to-improve-enclosing-J.patch" Content-Disposition: attachment; filename*0="0002-Refactor-JSX-indentation-code-to-improve-enclosing-J.pa"; filename*1="tch" Content-Transfer-Encoding: quoted-printable >From 1d404e0c9256b6e18ca4ac9e8e663a611ab7e256 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sat, 9 Feb 2019 20:06:29 -0800 Subject: [PATCH 02/19] Refactor JSX indentation code to improve enclosing= JSX discovery MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-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 =E2=80=98= <=E2=80=99 and =E2=80=98>=E2=80=99 and SGML detection). Slow down indentation a fai= r 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=E2=80=99t mistake a JSXOpeningElement fo= r the =E2=80=98<=E2=80=99 operator. (js--continued-expression-p): Don=E2=80=99t mistake a JSXClosingElement a= s a fragment of a continued expression including the =E2=80=98>=E2=80=99 oper= ator. (js--as-sgml): Simplify. Probably needn=E2=80=99t bind forward-sexp-func= tion to nil (sgml-mode already does) and probably shouldn=E2=80=99t 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=E2=80=99s 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=E2=80=99= 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=E2=80=99t 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=E2=80=99s 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=E2=80=99t bind js--continued-express= ion-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 =E2=80=98JSX is enabled.=E2=80=99 (Maybe lat= er, 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 4d91da7334..5b992535a8 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -572,6 +572,15 @@ js-chain-indent :safe 'booleanp :group 'js) =20 +(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 =20 (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 expres= sions.") =20 +(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 com= ma." (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) '(?, ?} ?{))))) + ;; =E2=80=9C<=E2=80=9D isn=E2=80=99t necessarily an operator in= JSX. + (not (and js-jsx-syntax (js--looking-at-jsx-start-tag-p)))))) =20 (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)) =20 +(defconst js--jsx-end-tag-re + (concat "\\|/>") + "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 "[/*]= /\\|=3D>"))) - (js--looking-at-operator-p) - (and (progn (backward-char) - (not (looking-at "\\+\\+\\|--\\|/[/*]"))))= )))))) + (and + ;; The =E2=80=9C>=E2=80=9D at the end of any JSXBoundaryEl= ement isn=E2=80=99t + ;; 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 "[/*]/\\|=3D>"))) + (js--looking-at-operator-p) + (and (progn (backward-char) + (not (looking-at "\\+\\+\\|--\\|/[/*]")= ))))))))))) =20 (defun js--skip-term-backward () "Skip a term before point; return t if a term was skipped." @@ -2153,190 +2187,108 @@ js--proper-indentation =20 ;;; JSX Indentation =20 -(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 "\\|/>") - "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=E2=80=99t 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 (=3D (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. =20 -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 =3D
; - -- Multi-line elements (enclosed in parentheses): - - function () { - return ( -
-
-
- ); - } - -- Function arguments: - - React.render( -
, - 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 (=3D (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 - (>=3D 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 - (<=3D current-line tag-end-line) - ;; An "after" line which does not end an element begins wi= th - ;; js, so indent it like js - (<=3D 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 (>=3D paren tag-start-pos) - ;; Curly bracket indicates the start of an embedded ex= pression - (=3D (char-after paren) 123) ; { - ;; The first line of the expression is indented like s= gml + ;; 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 + (>=3D paren tag-start-pos) + ;; A curly bracket indicates the start of an + ;; embedded expression. + (=3D (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 js= x things - ;; like sgml relative to the first thing - ((=3D 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=E2=80=99re inside! + t)) + ;; Indicate this will be indented specially. Return + ;; nil to stop iterating too. + (progn (setq type 'expression) nil) + ;; Stop iterating when parens =3D 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 (>=3D (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 sg= ml - ;; 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))) =20 (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)))) =20 ;;; Filling =20 @@ -3944,6 +3888,7 @@ js-jsx-mode (setq-local sgml-basic-offset js-indent-level)) (add-hook \\=3D'js-jsx-mode-hook #\\=3D'set-jsx-indentation)" :group 'js + (setq-local js-jsx-syntax t) (setq-local indent-line-function #'js-jsx-indent-line)) =20 ;;;###autoload (defalias 'javascript-mode 'js-mode) --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0003-Add-new-failing-unclosed-JSX-test-and-separate-such-.patch" Content-Disposition: attachment; filename*0="0003-Add-new-failing-unclosed-JSX-test-and-separate-such-.pa"; filename*1="tch" Content-Transfer-Encoding: quoted-printable >From 2b062aef181ea2c5ed61c7e1e8a40d8a43925f2a Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sun, 10 Feb 2019 21:11:17 -0800 Subject: [PATCH 03/19] Add new (failing) unclosed JSX test and separate s= uch 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 ( +
+ {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=E2=80=99t misinterpret equality operators as JSX. +for (; i < length;) void 0 +if (foo > bar) void 0 + +// Don=E2=80=99t 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 ( -
- {array.map(function () { - return { - a: 1 --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0004-js-syntax-propertize-Disambiguate-JS-from-JSX-fixing.patch" Content-Disposition: attachment; filename*0="0004-js-syntax-propertize-Disambiguate-JS-from-JSX-fixing.pa"; filename*1="tch" Content-Transfer-Encoding: quoted-printable >From 1b5df3e4dccd60f76fa42d626a898eba09224088 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Mon, 11 Feb 2019 03:00:34 -0800 Subject: [PATCH 04/19] js-syntax-propertize: Disambiguate JS from JSX, fi= xing some indents MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-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 =E2=80=98(with-syntax-table sgml-mode-syntax-table =E2=80=A6)=E2=80=99 do= es not mistake some JS punctuation syntax for SGML parenthesis syntax, namely =E2=80=98<=E2=80= =99 and =E2=80=98>=E2=80=99. * 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 5b992535a8..d0556f3538 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.") =20 +(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.") =20 @@ -1731,6 +1735,99 @@ js-syntax-propertize-regexp 'syntax-table (string-to-syntax "\"/")) (goto-char end))))) =20 +(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." + ;; =E2=80=9C=E2=80=9D - a JSXOpeningFragment. + (if (memq (char-after) '(?\/ ?\>)) t + (save-excursion + (skip-chars-forward " \t\n") + (and + (looking-at js--dotted-captured-name-re) + ;; Don=E2=80=99t match code like =E2=80=9Cif (i < await foo)=E2=80= =9D + (not (js--unary-keyword-p (match-string 1))) + (progn + (goto-char (match-end 0)) + (skip-chars-forward " \t\n") + (or + ;; =E2=80=9C>=E2=80=9D, =E2=80=9C/>=E2=80=9D - tag enders. + ;; =E2=80=9C{=E2=80=9D - 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) + ;; =E2=80=9C=E2=80=A6/>=E2=80=9D - a self-closing JSXOpeningElement. + ;; =E2=80=9C=E2=80=9D - a JSXClosingFragment. + (if (=3D (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=E2=80=A6 + (if (memq (char-before) '(?\} ?\" ?\' ?\`)) (throw 'match t) + (if (and last-tag-or-attr-name last-non-unary-p + ;; =E2=80=9C<=E2=80=9D, =E2=80=9C=E2=80=99 chars (f= rom START to END) aren=E2=80=99t JSX. + +Later, this info prevents =E2=80=98sgml-=E2=80=99 functions from treatin= g some +=E2=80=98<=E2=80=99 and =E2=80=98>=E2=80=99 chars as parts of tokens of = SGML tags =E2=80=94 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 >, >=3D, >>>, <, <=3D, <<<, or + ;; =3D> 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))) =20 (defconst js--prettify-symbols-alist '(("=3D>" . ?=E2=87=92) 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=E2=80=99t 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=E2=80=A6) + + + + + How would we ever live without unary support + + + + --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0005-Use-js-jsx-prefix-for-functions-and-variables.patch" Content-Disposition: attachment; filename="0005-Use-js-jsx-prefix-for-functions-and-variables.patch" Content-Transfer-Encoding: quoted-printable >From 537ccf9bdfddd6e0e60f4eb55ce8b5f748f1cee8 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Fri, 15 Feb 2019 22:15:11 -0800 Subject: [PATCH 05/19] Use js-jsx- prefix for functions and variables * lisp/progmodes/js.el (js--disambiguate-beginning-of-jsx-tag): Rename to js-jsx--disambiguate-beginning-of-tag. (js--disambiguate-end-of-jsx-tag): Rename to js-jsx--disambiguate-end-of-tag. (js--disambiguate-js-from-jsx): Rename to js-jsx--disambiguate-syntax. (js--jsx-start-tag-re): Rename to js-jsx--start-tag-re. (js--looking-at-jsx-start-tag-p): Rename to js-jsx--looking-at-start-tag-p. (js--jsx-end-tag-re): Rename to js-jsx--end-tag-re. (js--looking-back-at-jsx-end-tag-p): Rename to js-jsx--looking-back-at-end-tag-p. (js--as-sgml): Rename to js-jsx--as-sgml. (js--outermost-enclosing-jsx-tag-pos): Rename to js-jsx--outermost-enclosing-tag-pos. (js--jsx-indentation): Rename to js-jsx--indentation-type. (js--indent-line-in-jsx-expression): Rename to js-jsx--indent-line-in-expression. (js--indent-n+1th-jsx-line): Rename to js-jsx--indent-n+1th-line. --- lisp/progmodes/js.el | 52 ++++++++++++++++++++++++++--------------------= ------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index d0556f3538..4404ea04a0 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -1743,7 +1743,7 @@ js--unary-keyword-p "Check if STRING is a unary operator keyword in JavaScript." (string-match-p js--unary-keyword-re string)) =20 -(defun js--disambiguate-beginning-of-jsx-tag () +(defun js-jsx--disambiguate-beginning-of-tag () "Parse enough to determine if a JSX tag starts here. Disambiguate JSX from equality operators by testing for syntax only valid as JSX." @@ -1766,7 +1766,7 @@ js--disambiguate-beginning-of-jsx-tag ;; Check if a JSXAttribute follows. (looking-at js--name-start-re))))))) =20 -(defun js--disambiguate-end-of-jsx-tag () +(defun js-jsx--disambiguate-end-of-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." @@ -1812,7 +1812,7 @@ js--disambiguate-end-of-jsx-tag ;; Nothing else to look for; give up parsing. (throw 'match nil))))))))) =20 -(defun js--disambiguate-js-from-jsx (start end) +(defun js-jsx--disambiguate-syntax (start end) "Figure out which =E2=80=98<=E2=80=99 and =E2=80=98>=E2=80=99 chars (f= rom START to END) aren=E2=80=99t JSX. =20 Later, this info prevents =E2=80=98sgml-=E2=80=99 functions from treatin= g some @@ -1821,8 +1821,8 @@ js--disambiguate-js-from-jsx 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)) + (unless (if (eq (char-before) ?<) (js-jsx--disambiguate-beginning-of= -tag) + (js-jsx--disambiguate-end-of-tag)) ;; Inform sgml- functions that this >, >=3D, >>>, <, <=3D, <<<, or ;; =3D> token is punctuation (and not an open or close parenthesis ;; as per usual in sgml-mode). @@ -1856,7 +1856,7 @@ js-syntax-propertize (js-syntax-propertize-regexp end))))) ("\\`\\(#\\)!" (1 "< b"))) (point) end) - (if js-jsx-syntax (js--disambiguate-js-from-jsx start end))) + (if js-jsx-syntax (js-jsx--disambiguate-syntax start end))) =20 (defconst js--prettify-symbols-alist '(("=3D>" . ?=E2=87=92) @@ -1881,13 +1881,13 @@ js--indent-operator-re (js--regexp-opt-symbol '("in" "instanceof"))) "Regexp matching operators that affect indentation of continued expres= sions.") =20 -(defconst js--jsx-start-tag-re +(defconst js-jsx--start-tag-re (concat "<" sgml-name-re) "Regexp matching code that looks like a JSXOpeningElement.") =20 -(defun js--looking-at-jsx-start-tag-p () +(defun js-jsx--looking-at-start-tag-p () "Non-nil if a JSXOpeningElement immediately follows point." - (looking-at js--jsx-start-tag-re)) + (looking-at js-jsx--start-tag-re)) =20 (defun js--looking-at-operator-p () "Return non-nil if point is on a JavaScript operator, other than a com= ma." @@ -1913,7 +1913,7 @@ js--looking-at-operator-p ;; return NaN anyway. Shouldn't be a problem. (memq (char-before) '(?, ?} ?{))))) ;; =E2=80=9C<=E2=80=9D isn=E2=80=99t necessarily an operator in= JSX. - (not (and js-jsx-syntax (js--looking-at-jsx-start-tag-p)))))) + (not (and js-jsx-syntax (js-jsx--looking-at-start-tag-p)))))) =20 (defun js--find-newline-backward () "Move backward to the nearest newline that is not in a block comment." @@ -1933,13 +1933,13 @@ js--find-newline-backward (setq result nil))) result)) =20 -(defconst js--jsx-end-tag-re +(defconst js-jsx--end-tag-re (concat "\\|/>") "Regexp matching a JSXClosingElement.") =20 -(defun js--looking-back-at-jsx-end-tag-p () +(defun js-jsx--looking-back-at-end-tag-p () "Non-nil if a JSXClosingElement immediately precedes point." - (looking-back js--jsx-end-tag-re (point-at-bol))) + (looking-back js-jsx--end-tag-re (point-at-bol))) =20 (defun js--continued-expression-p () "Return non-nil if the current line continues an expression." @@ -1961,7 +1961,7 @@ js--continued-expression-p (and ;; The =E2=80=9C>=E2=80=9D at the end of any JSXBoundaryEl= ement isn=E2=80=99t ;; part of a continued expression. - (not (and js-jsx-syntax (js--looking-back-at-jsx-end-tag-p= ))) + (not (and js-jsx-syntax (js-jsx--looking-back-at-end-tag-p= ))) (progn (or (bobp) (backward-char)) (and (> (point) (point-min)) @@ -2285,14 +2285,14 @@ js--proper-indentation =20 ;;; JSX Indentation =20 -(defmacro js--as-sgml (&rest body) +(defmacro js-jsx--as-sgml (&rest body) "Execute BODY as if in sgml-mode." `(with-syntax-table sgml-mode-syntax-table ,@body)) =20 -(defun js--outermost-enclosing-jsx-tag-pos () +(defun js-jsx--outermost-enclosing-tag-pos () (let (context tag-pos last-tag-pos parse-status parens paren-pos curly= -pos) - (js--as-sgml + (js-jsx--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)) @@ -2322,7 +2322,7 @@ js--outermost-enclosing-jsx-tag-pos (goto-char tag-pos)))) last-tag-pos)) =20 -(defun js--jsx-indentation () +(defun js-jsx--indentation-type () "Determine if/how the current line should be indented as JSX. =20 Return nil for first JSXElement line (indent like JS). @@ -2336,7 +2336,7 @@ js--jsx-indentation (save-excursion ;; Determine if inside a JSXElement. (beginning-of-line) ; For exclusivity - (when (setq tag-start-pos (js--outermost-enclosing-jsx-tag-pos)) + (when (setq tag-start-pos (js-jsx--outermost-enclosing-tag-pos)) ;; Check if inside an embedded multi-line JS expression. (goto-char current-pos) (end-of-line) ; For exclusivity @@ -2369,14 +2369,14 @@ js--jsx-indentation (setq parens (cdr parens))))) (or type 'n+1th))))) =20 -(defun js--indent-line-in-jsx-expression () +(defun js-jsx--indent-line-in-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) (save-excursion (setq offset (- (point) (progn (back-to-indentation) (point))) - indent-col (js--as-sgml (sgml-calculate-indent)))) + indent-col (js-jsx--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. @@ -2384,9 +2384,9 @@ js--indent-line-in-jsx-expression (+ indent-col js-indent-level))) (when (> offset 0) (forward-char offset)))))) =20 -(defun js--indent-n+1th-jsx-line () +(defun js-jsx--indent-n+1th-line () "Indent the current line as JSX within JavaScript." - (js--as-sgml (sgml-indent-line))) + (js-jsx--as-sgml (sgml-indent-line))) =20 (defun js-indent-line () "Indent the current line as JavaScript." @@ -2403,10 +2403,10 @@ js-jsx-indent-line i.e., customize JSX element indentation with `sgml-basic-offset', `sgml-attribute-offset' et al." (interactive) - (let ((type (js--jsx-indentation))) + (let ((type (js-jsx--indentation-type))) (if type - (if (eq type 'n+1th) (js--indent-n+1th-jsx-line) - (js--indent-line-in-jsx-expression)) + (if (eq type 'n+1th) (js-jsx--indent-n+1th-line) + (js-jsx--indent-line-in-expression)) (js-indent-line)))) =20 ;;; Filling --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0006-Add-basic-JSX-font-locking.patch" Content-Disposition: attachment; filename="0006-Add-basic-JSX-font-locking.patch" Content-Transfer-Encoding: quoted-printable >From 9c2271065aa5408544fb09fe386de3f357a1d3e6 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sun, 17 Feb 2019 00:38:01 -0800 Subject: [PATCH 06/19] Add basic JSX font-locking MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-8 Content-Transfer-Encoding: 8bit Font-lock JSX from the beginning of the buffer to the end. Tends to break temporarily when editing lines, because the parser doesn=E2=80=99t = yet look backwards to determine if the end of a tag in the current range starts before the range. This also re-breaks some tests fixed by previous commits, as we begin to take a different direction in our parsing code, looking for JSX, rather than for non-JSX. The parsing code will eventually provide information for indentation again. * lisp/progmodes/js.el (js--dotted-captured-name-re) (js-jsx--disambiguate-beginning-of-tag) (js-jsx--disambiguate-end-of-tag, js-jsx--disambiguate-syntax): Remove. (js-jsx--font-lock-keywords): New variable. (js--font-lock-keywords-3): Add JSX matchers. (js-jsx--match-tag-name, js-jsx--match-attribute-name): New functions. (js-jsx--syntax-propertize-tag): New function to aid in JSX font-locking and eventually indentation. (js-jsx--text-properties): New variable. (js-syntax-propertize): Propertize JSX properly using syntax-propertize-rules. --- lisp/progmodes/js.el | 216 +++++++++++++++++++++++++++++----------------= ------ 1 file changed, 124 insertions(+), 92 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 4404ea04a0..1319fa1939 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -82,10 +82,6 @@ js--dotted-name-re (concat js--name-re "\\(?:\\." js--name-re "\\)*") "Regexp matching a dot-separated sequence of JavaScript names.") =20 -(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.") =20 @@ -1498,6 +1494,33 @@ js--variable-decl-matcher ;; Matcher always "fails" nil) =20 +(defconst js-jsx--font-lock-keywords + `((js-jsx--match-tag-name 0 font-lock-function-name-face t) + (js-jsx--match-attribute-name 0 font-lock-variable-name-face t)) + "JSX font lock faces.") + +(defun js-jsx--match-tag-name (limit) + "Match JSXBoundaryElement names, until LIMIT." + (when js-jsx-syntax + (let ((pos (next-single-char-property-change (point) 'js-jsx-tag-nam= e nil limit)) + value) + (when (and pos (> pos (point))) + (goto-char pos) + (or (and (setq value (get-text-property pos 'js-jsx-tag-name)) + (progn (set-match-data value) t)) + (js-jsx--match-tag-name limit)))))) + +(defun js-jsx--match-attribute-name (limit) + "Match JSXAttribute names, until LIMIT." + (when js-jsx-syntax + (let ((pos (next-single-char-property-change (point) 'js-jsx-attribu= te-name nil limit)) + value) + (when (and pos (> pos (point))) + (goto-char pos) + (or (and (setq value (get-text-property pos 'js-jsx-attribute-na= me)) + (progn (set-match-data value) t)) + (js-jsx--match-attribute-name limit)))))) + (defconst js--font-lock-keywords-3 `( ;; This goes before keywords-2 so it gets used preferentially @@ -1609,7 +1632,10 @@ js--font-lock-keywords-3 (forward-symbol -1) (end-of-line)) '(end-of-line) - '(0 font-lock-variable-name-face)))) + '(0 font-lock-variable-name-face))) + + ;; jsx (when enabled) + ,@js-jsx--font-lock-keywords) "Level three font lock for `js-mode'.") =20 (defun js--inside-pitem-p (pitem) @@ -1743,94 +1769,100 @@ js--unary-keyword-p "Check if STRING is a unary operator keyword in JavaScript." (string-match-p js--unary-keyword-re string)) =20 -(defun js-jsx--disambiguate-beginning-of-tag () - "Parse enough to determine if a JSX tag starts here. -Disambiguate JSX from equality operators by testing for syntax -only valid as JSX." - ;; =E2=80=9C=E2=80=9D - a JSXOpeningFragment. - (if (memq (char-after) '(?\/ ?\>)) t - (save-excursion - (skip-chars-forward " \t\n") - (and - (looking-at js--dotted-captured-name-re) - ;; Don=E2=80=99t match code like =E2=80=9Cif (i < await foo)=E2=80= =9D - (not (js--unary-keyword-p (match-string 1))) - (progn - (goto-char (match-end 0)) - (skip-chars-forward " \t\n") - (or - ;; =E2=80=9C>=E2=80=9D, =E2=80=9C/>=E2=80=9D - tag enders. - ;; =E2=80=9C{=E2=80=9D - a JSXExpressionContainer. - (memq (char-after) '(?\> ?\/ ?\{)) - ;; Check if a JSXAttribute follows. - (looking-at js--name-start-re))))))) - -(defun js-jsx--disambiguate-end-of-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) - ;; =E2=80=9C=E2=80=A6/>=E2=80=9D - a self-closing JSXOpeningElement. - ;; =E2=80=9C=E2=80=9D - a JSXClosingFragment. - (if (=3D (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=E2=80=A6 - (if (memq (char-before) '(?\} ?\" ?\' ?\`)) (throw 'match t) - (if (and last-tag-or-attr-name last-non-unary-p - ;; =E2=80=9C<=E2=80=9D, =E2=80=9C=E2=80=99 chars (f= rom START to END) aren=E2=80=99t JSX. - -Later, this info prevents =E2=80=98sgml-=E2=80=99 functions from treatin= g some -=E2=80=98<=E2=80=99 and =E2=80=98>=E2=80=99 chars as parts of tokens of = SGML tags =E2=80=94 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-jsx--disambiguate-beginning-of= -tag) - (js-jsx--disambiguate-end-of-tag)) - ;; Inform sgml- functions that this >, >=3D, >>>, <, <=3D, <<<, or - ;; =3D> 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-jsx--syntax-propertize-tag (end) + "Determine if a JSXBoundaryElement is before END and propertize it. +Disambiguate JSX from inequality operators and arrow functions by +testing for syntax only valid as JSX." + (let ((tag-beg (1- (point))) tag-end (type 'open) + name-beg name-match-data unambiguous + forward-sexp-function) ; Use Lisp version. + (catch 'stop + (while (and (< (point) end) + (progn (skip-chars-forward " \t\n" end) + (< (point) end))) + (cond + ((=3D (char-after) ?>) + (forward-char) + (setq unambiguous t + tag-end (point)) + (throw 'stop nil)) + ;; Handle a JSXSpreadChild (=E2=80=9C=3D (point) end) (throw 'stop nil)) + (skip-chars-forward " \t\n" end) + (if (>=3D (point) end) (throw 'stop nil)) + (if (=3D (char-after) ?}) (forward-char) ; Shortcut to bail. + ;; Recursively propertize the JSXExpressionContainer=E2=80= =99s + ;; expression. + (js-syntax-propertize (point) (if expr-end (min (1- expr-e= nd) end) end)) + ;; Exit the JSXExpressionContainer if that=E2=80=99s possi= ble, + ;; else move to the end of the propertized area. + (goto-char (if expr-end (min expr-end end) end))))) + ((=3D (char-after) ?/) + ;; Assume a tag is an open tag until a slash is found, then + ;; figure out what type it actually is. + (if (eq type 'open) (setq type (if name-beg 'self-closing 'clo= se))) + (forward-char)) + ((looking-at js--dotted-name-re) + (if (not name-beg) + (progn + ;; Don=E2=80=99t match code like =E2=80=9Cif (i < await = foo)=E2=80=9D + (if (js--unary-keyword-p (match-string 0)) (throw 'stop = nil)) + ;; Save boundaries for later fontification after + ;; unambiguously determining the code is JSX. + (setq name-beg (match-beginning 0) + name-match-data (match-data)) + (goto-char (match-end 0))) + (setq unambiguous t) ; Non-unary name followed by 2nd name =E2= =87=92 JSX + ;; Save JSXAttribute=E2=80=99s name=E2=80=99s match data for= font-locking later. + (put-text-property (match-beginning 0) (1+ (match-beginning = 0)) + 'js-jsx-attribute-name (match-data)) + (goto-char (match-end 0)) + (if (>=3D (point) end) (throw 'stop nil)) + (skip-chars-forward " \t\n" end) + (if (>=3D (point) end) (throw 'stop nil)) + ;; =E2=80=9C=3D=E2=80=9D is optional for null-valued JSXAttr= ibutes. + (when (=3D (char-after) ?=3D) + (forward-char) + (if (>=3D (point) end) (throw 'stop nil)) + (skip-chars-forward " \t\n" end) + (if (>=3D (point) end) (throw 'stop nil)) + ;; Skip over strings (if possible). Any + ;; JSXExpressionContainer here will be parsed in the + ;; next iteration of the loop. + (when (memq (char-after) '(?\" ?\' ?\`)) + (condition-case nil + (forward-sexp) + (scan-error (throw 'stop nil))))))) + ;; There is nothing more to check; this either isn=E2=80=99t JS= X, or + ;; the tag is incomplete. + (t (throw 'stop nil))))) + (when unambiguous + ;; Save JSXBoundaryElement=E2=80=99s name=E2=80=99s match data for= font-locking. + (if name-beg (put-text-property name-beg (1+ name-beg) 'js-jsx-tag= -name name-match-data)) + ;; Mark beginning and end of tag for features like indentation. + (put-text-property tag-beg (1+ tag-beg) 'js-jsx-tag-beg type) + (if tag-end (put-text-property (1- tag-end) tag-end 'js-jsx-tag-en= d tag-beg))))) + +(defconst js-jsx--text-properties + '(js-jsx-tag-beg nil js-jsx-tag-end nil js-jsx-tag-name nil js-jsx-att= ribute-name nil) + "Plist of text properties added by `js-syntax-propertize'.") =20 (defun js-syntax-propertize (start end) ;; JavaScript allows immediate regular expression objects, written /..= ./. (goto-char start) + (if js-jsx-syntax (remove-text-properties start end js-jsx--text-prope= rties)) (js-syntax-propertize-regexp end) (funcall (syntax-propertize-rules @@ -1854,9 +1886,9 @@ js-syntax-propertize (put-text-property (match-beginning 1) (match-end 1) 'syntax-table (string-to-syntax "\"/")) (js-syntax-propertize-regexp end))))) - ("\\`\\(#\\)!" (1 "< b"))) - (point) end) - (if js-jsx-syntax (js-jsx--disambiguate-syntax start end))) + ("\\`\\(#\\)!" (1 "< b")) + ("<" (0 (ignore (if js-jsx-syntax (js-jsx--syntax-propertize-tag end= )))))) + (point) end)) =20 (defconst js--prettify-symbols-alist '(("=3D>" . ?=E2=87=92) --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0007-Font-lock-JSX-while-editing-it-by-extending-regions.patch" Content-Disposition: attachment; filename*0="0007-Font-lock-JSX-while-editing-it-by-extending-regions.pat"; filename*1="ch" Content-Transfer-Encoding: quoted-printable >From 08712ee97b2b4f37840a8d275234a53dd2df421e Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sun, 17 Feb 2019 21:16:13 -0800 Subject: [PATCH 07/19] Font-lock JSX while editing it by extending region= s * lisp/progmodes/js.el (js-jsx--font-lock-keywords): Call tag beginning and end matchers. (js-jsx--match-tag-beg, js-jsx--match-tag-end): New functions. (js-jsx--syntax-propertize-tag): Record buffer positions of JSXElement beginning and end for font-locking. (js--syntax-propertize-extend-region) (js-jsx--syntax-propertize-extend-region): New functions for extending the syntax-propertize region backwards to the start of a JSXElement so its JSXAttribute children on its n+1th lines can be parsed as such while editing those lines. (js-mode): Add js--syntax-propertize-extend-region to syntax-propertize-extend-region-functions. --- lisp/progmodes/js.el | 81 ++++++++++++++++++++++++++++++++++++++++++++++= +----- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 1319fa1939..7fb4bcc808 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -1496,8 +1496,10 @@ js--variable-decl-matcher =20 (defconst js-jsx--font-lock-keywords `((js-jsx--match-tag-name 0 font-lock-function-name-face t) - (js-jsx--match-attribute-name 0 font-lock-variable-name-face t)) - "JSX font lock faces.") + (js-jsx--match-attribute-name 0 font-lock-variable-name-face t) + (js-jsx--match-tag-beg) + (js-jsx--match-tag-end)) + "JSX font lock faces and multiline text properties.") =20 (defun js-jsx--match-tag-name (limit) "Match JSXBoundaryElement names, until LIMIT." @@ -1521,6 +1523,28 @@ js-jsx--match-attribute-name (progn (set-match-data value) t)) (js-jsx--match-attribute-name limit)))))) =20 +(defun js-jsx--match-tag-beg (limit) + "Match JSXBoundaryElements from start, until LIMIT." + (when js-jsx-syntax + (let ((pos (next-single-char-property-change (point) 'js-jsx-tag-beg= nil limit)) + value) + (when (and pos (> pos (point))) + (goto-char pos) + (or (and (setq value (get-text-property pos 'js-jsx-tag-beg)) + (progn (put-text-property pos (cdr value) 'font-lock-mu= ltiline t) t)) + (js-jsx--match-tag-beg limit)))))) + +(defun js-jsx--match-tag-end (limit) + "Match JSXBoundaryElements from end, until LIMIT." + (when js-jsx-syntax + (let ((pos (next-single-char-property-change (point) 'js-jsx-tag-end= nil limit)) + value) + (when (and pos (> pos (point))) + (goto-char pos) + (or (and (setq value (get-text-property pos 'js-jsx-tag-end)) + (progn (put-text-property value pos 'font-lock-multilin= e t) t)) + (js-jsx--match-tag-end limit)))))) + (defconst js--font-lock-keywords-3 `( ;; This goes before keywords-2 so it gets used preferentially @@ -1769,11 +1793,53 @@ js--unary-keyword-p "Check if STRING is a unary operator keyword in JavaScript." (string-match-p js--unary-keyword-re string)) =20 +(defun js--syntax-propertize-extend-region (start end) + "Extend the START-END region for propertization, if necessary. +For use by `syntax-propertize-extend-region-functions'." + (if js-jsx-syntax (js-jsx--syntax-propertize-extend-region start end))= ) + +(defun js-jsx--syntax-propertize-extend-region (start end) + "Extend the START-END region for propertization, if necessary. +If any =E2=80=9C>=E2=80=9D in the region appears to be the end of a tag = starting +before the start of the region, extend region backwards to the +start of that tag so parsing may proceed from that point. +For use by `syntax-propertize-extend-region-functions'." + (let (new-start + forward-sexp-function ; Use the Lisp version. + parse-sexp-lookup-properties) ; Fix backward-sexp error here. + (catch 'stop + (goto-char start) + (while (re-search-forward ">" end t) + (catch 'continue + ;; Check if this is really a right shift bitwise operator + ;; (=E2=80=9C>>=E2=80=9D or =E2=80=9C>>>=E2=80=9D). + (unless (or (eq (char-before (1- (point))) ?>) + (eq (char-after) ?>)) + (save-excursion + (backward-char) + (while (progn (if (=3D (point) (point-min)) (throw 'contin= ue nil)) + (/=3D (char-before) ?<)) + (skip-chars-backward " \t\n") + (if (=3D (point) (point-min)) (throw 'continue nil)) + (cond + ((memq (char-before) '(?\" ?\' ?\` ?\})) + (condition-case nil + (backward-sexp) + (scan-error (throw 'continue nil)))) + ((memq (char-before) '(?\/ ?\=3D)) (backward-char)) + ((looking-back js--dotted-name-re (line-beginning-posit= ion) t) + (goto-char (match-beginning 0))) + (t (throw 'continue nil)))) + (when (< (point) start) + (setq new-start (1- (point))) + (throw 'stop nil))))))) + (if new-start (cons new-start end)))) + (defun js-jsx--syntax-propertize-tag (end) "Determine if a JSXBoundaryElement is before END and propertize it. Disambiguate JSX from inequality operators and arrow functions by testing for syntax only valid as JSX." - (let ((tag-beg (1- (point))) tag-end (type 'open) + (let ((tag-beg (1- (point))) (type 'open) name-beg name-match-data unambiguous forward-sexp-function) ; Use Lisp version. (catch 'stop @@ -1783,8 +1849,7 @@ js-jsx--syntax-propertize-tag (cond ((=3D (char-after) ?>) (forward-char) - (setq unambiguous t - tag-end (point)) + (setq unambiguous t) (throw 'stop nil)) ;; Handle a JSXSpreadChild (=E2=80=9CFrom 2be948e6b608f8090dce58f8d33cde593ec84d9d Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Fri, 8 Mar 2019 16:29:02 -0800 Subject: [PATCH 08/19] Propertize and font-lock JSXText and JSXExpressionContainers This completes highlighting support for JSX, as requested in: - https://github.com/mooz/js2-mode/issues/140 - https://github.com/mooz/js2-mode/issues/330 - https://github.com/mooz/js2-mode/issues/409 * lisp/progmodes/js.el (js--name-start-chars): Extract part of js--name-start-re so it can be reused in another regexp. (js--name-start-re): Use js--name-start-chars. (js-jsx--font-lock-keywords): Use new matchers. (js-jsx--match-text, js-jsx--match-expr): New matchers to remove typical JS font-locking and extend the font-locked region, respectively. (js-jsx--tag-re, js-jsx--self-closing-re): New regexps matching JSX. (js-jsx--matched-tag-type, js-jsx--matching-close-tag-pos) (js-jsx--enclosing-curly-pos, js-jsx--enclosing-tag-pos) (js-jsx--at-enclosing-tag-child-p): New functions for parsing and analyzing JSX. (js-jsx--text-range, js-jsx--syntax-propertize-tag-text): New functions for propertizing JSXText. (js-jsx--syntax-propertize-tag): Propertize JSXText children of tags. (js-jsx--text-properties): Remove JSXText-related text properties when repropertizing. (js-mode): Extend the syntax-propertize region with syntax-propertize-multiline; we are now adding the syntax-multiline text property to buffer ranges that are JSXText to ensure the whole multiline JSX construct is reidentified. --- lisp/progmodes/js.el | 216 +++++++++++++++++++++++++++++++++++++++++++++= ++++-- 1 file changed, 211 insertions(+), 5 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 7fb4bcc808..220cf97fdc 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -66,7 +66,10 @@ electric-layout-rules =20 ;;; Constants =20 -(defconst js--name-start-re "[a-zA-Z_$]" +(defconst js--name-start-chars "a-zA-Z_$" + "Character class chars matching the start of a JavaScript identifier."= ) + +(defconst js--name-start-re (concat "[" js--name-start-chars "]") "Regexp matching the start of a JavaScript identifier, without groupin= g.") =20 (defconst js--stmt-delim-chars "^;{}?:") @@ -1497,8 +1500,10 @@ js--variable-decl-matcher (defconst js-jsx--font-lock-keywords `((js-jsx--match-tag-name 0 font-lock-function-name-face t) (js-jsx--match-attribute-name 0 font-lock-variable-name-face t) + (js-jsx--match-text 0 'default t) ; =E2=80=9CUndo=E2=80=9D keyword f= ontification. (js-jsx--match-tag-beg) - (js-jsx--match-tag-end)) + (js-jsx--match-tag-end) + (js-jsx--match-expr)) "JSX font lock faces and multiline text properties.") =20 (defun js-jsx--match-tag-name (limit) @@ -1523,6 +1528,19 @@ js-jsx--match-attribute-name (progn (set-match-data value) t)) (js-jsx--match-attribute-name limit)))))) =20 +(defun js-jsx--match-text (limit) + "Match JSXText, until LIMIT." + (when js-jsx-syntax + (let ((pos (next-single-char-property-change (point) 'js-jsx-text ni= l limit)) + value) + (when (and pos (> pos (point))) + (goto-char pos) + (or (and (setq value (get-text-property pos 'js-jsx-text)) + (progn (set-match-data value) + (put-text-property (car value) (cadr value) 'fon= t-lock-multiline t) + t)) + (js-jsx--match-text limit)))))) + (defun js-jsx--match-tag-beg (limit) "Match JSXBoundaryElements from start, until LIMIT." (when js-jsx-syntax @@ -1545,6 +1563,17 @@ js-jsx--match-tag-end (progn (put-text-property value pos 'font-lock-multilin= e t) t)) (js-jsx--match-tag-end limit)))))) =20 +(defun js-jsx--match-expr (limit) + "Match JSXExpressionContainers, until LIMIT." + (when js-jsx-syntax + (let ((pos (next-single-char-property-change (point) 'js-jsx-expr ni= l limit)) + value) + (when (and pos (> pos (point))) + (goto-char pos) + (or (and (setq value (get-text-property pos 'js-jsx-expr)) + (progn (put-text-property pos value 'font-lock-multilin= e t) t)) + (js-jsx--match-expr limit)))))) + (defconst js--font-lock-keywords-3 `( ;; This goes before keywords-2 so it gets used preferentially @@ -1835,6 +1864,177 @@ js-jsx--syntax-propertize-extend-region (throw 'stop nil))))))) (if new-start (cons new-start end)))) =20 +(defconst js-jsx--tag-re + (concat "<\\s-*\\(" + "[/>]" ; JSXClosingElement, or JSXOpeningFragment, or JSXClosi= ngFragment + "\\|" + js--dotted-name-re "\\s-*[" js--name-start-chars "{/>]" ; JSXO= peningElement + "\\)") + "Regexp unambiguously matching a JSXBoundaryElement.") + +(defun js-jsx--matched-tag-type () + "Determine the tag type of the last match to `js-jsx--tag-re'. +Return `close' for a JSXClosingElement/JSXClosingFragment match, +return `self-closing' for some self-closing JSXOpeningElements, +else return `other'." + (let ((chars (vconcat (match-string 1)))) + (cond + ((=3D (aref chars 0) ?/) 'close) + ((=3D (aref chars (1- (length chars))) ?/) 'self-closing) + (t 'other)))) + +(defconst js-jsx--self-closing-re "/\\s-*>" + "Regexp matching the end of a self-closing JSXOpeningElement.") + +(defun js-jsx--matching-close-tag-pos () + "Return position of the closer of the opener before point. +Assuming a JSXOpeningElement or a JSXOpeningFragment is +immediately before point, find a matching JSXClosingElement or +JSXClosingFragment, skipping over any nested JSXElements to find +the match. Return nil if a match can=E2=80=99t be found." + (let ((tag-stack 1) self-closing-pos type) + (catch 'stop + (while (re-search-forward js-jsx--tag-re nil t) + (setq type (js-jsx--matched-tag-type)) + ;; Balance the total of self-closing tags that we subtract + ;; from the stack, ignoring those tags which are never added + ;; to the stack (see below). + (unless (eq type 'self-closing) + (when (and self-closing-pos (> (point) self-closing-pos)) + (setq tag-stack (1- tag-stack)))) + (if (eq type 'close) + (progn + (setq tag-stack (1- tag-stack)) + (when (=3D tag-stack 0) + (throw 'stop (match-beginning 0)))) + ;; Tags that we know are self-closing aren=E2=80=99t added to = the + ;; stack at all, because we only close the ones that we have + ;; anticipated after moving past those anticipated tags=E2=80=99 + ;; ends, and if a self-closing tag is the first tag we + ;; encounter in this loop, then it will never be anticipated + ;; (due to an optimization where we sometimes can avoid + ;; looking for self-closing tags). + (unless (eq type 'self-closing) + (setq tag-stack (1+ tag-stack)))) + ;; Don=E2=80=99t needlessly recalculate. + (unless (and self-closing-pos (<=3D (point) self-closing-pos)) + (setq self-closing-pos nil) ; Reset if recalculating. + (save-excursion + ;; Anticipate a self-closing tag that we should make sure + ;; to subtract from the tag stack once we move past its + ;; end; we might might miss the end otherwise, due to the + ;; regexp-matching method we use to detect tags. + (when (re-search-forward js-jsx--self-closing-re nil t) + (setq self-closing-pos (match-beginning 0))))))))) + +(defun js-jsx--enclosing-curly-pos () + "Return position of enclosing =E2=80=9C{=E2=80=9D in a =E2=80=9C{/}=E2= =80=9D pair about point." + (let ((parens (reverse (nth 9 (syntax-ppss)))) paren-pos curly-pos) + (while + (and + (setq paren-pos (car parens)) + (not (when (=3D (char-after paren-pos) ?{) + (setq curly-pos paren-pos))) + (setq parens (cdr parens)))) + curly-pos)) + +(defun js-jsx--enclosing-tag-pos () + "Return beginning and end of a JSXElement about point. +Look backward for a JSXElement that both starts before point and +also ends after point. That may be either a self-closing +JSXElement or a JSXOpeningElement/JSXClosingElement pair." + (let ((start (point)) + (curly-pos (save-excursion (js-jsx--enclosing-curly-pos))) + tag-beg tag-beg-pos tag-end-pos close-tag-pos) + (while + (and + (setq tag-beg (js--backward-text-property 'js-jsx-tag-beg)) + (progn + (setq tag-beg-pos (point) + tag-end-pos (cdr tag-beg)) + (not + (or + (and (eq (car tag-beg) 'self-closing) + (< start tag-end-pos)) + (and (eq (car tag-beg) 'open) + (save-excursion + (goto-char tag-end-pos) + (setq close-tag-pos (js-jsx--matching-close-tag-pos)= ) + ;; The JSXOpeningElement may either be unclosed, + ;; else the closure must occur after the start + ;; point (otherwise, a miscellaneous previous + ;; JSXOpeningElement has been found, and we should + ;; keep looking back for an enclosing one). + (or (not close-tag-pos) (< start close-tag-pos))))))= )) + ;; Don=E2=80=99t return the last tag pos (if any; it wasn=E2=80=99= t enclosing). + (setq tag-beg nil)) + (and tag-beg + (or (not curly-pos) (> tag-beg-pos curly-pos)) + (cons tag-beg-pos tag-end-pos)))) + +(defun js-jsx--at-enclosing-tag-child-p () + "Return t if point is at an enclosing tag=E2=80=99s child." + (let ((pos (save-excursion (js-jsx--enclosing-tag-pos)))) + (and pos (>=3D (point) (cdr pos))))) + +(defun js-jsx--text-range (beg end) + "Identify JSXText within a =E2=80=9C>/{/}/<=E2=80=9D pair." + (when (> (- end beg) 0) + (save-excursion + (goto-char beg) + (while (and (skip-chars-forward " \t\n" end) (< (point) end)) + ;; Comments and string quotes don=E2=80=99t serve their usual + ;; syntactic roles in JSXText; make them plain punctuation to + ;; negate those roles. + (when (or (=3D (char-after) ?/) ; comment + (=3D (syntax-class (syntax-after (point))) 7)) ; strin= g quote + (put-text-property (point) (1+ (point)) 'syntax-table '(1))) + (forward-char))) + ;; Mark JSXText so it can be font-locked as non-keywords. + (put-text-property beg (1+ beg) 'js-jsx-text (list beg end (current-= buffer))) + ;; Ensure future propertization beginning from within the + ;; JSXText determines JSXText context from earlier lines. + (put-text-property beg end 'syntax-multiline t))) + +(defun js-jsx--syntax-propertize-tag-text (end) + "Determine if JSXText is before END and propertize it. +Text within an open/close tag pair may be JSXText. Temporarily +interrupt JSXText by JSXExpressionContainers, and terminate +JSXText when another JSXBoundaryElement is encountered. Despite +terminations, all JSXText will be identified once all the +JSXBoundaryElements within an outermost JSXElement=E2=80=99s tree have +been propertized." + (let ((text-beg (point)) + forward-sexp-function) ; Use Lisp version. + (catch 'stop + (while (re-search-forward "[{<]" end t) + (js-jsx--text-range text-beg (1- (point))) + (cond + ((=3D (char-before) ?{) + (let (expr-beg expr-end) + (condition-case nil + (save-excursion + (backward-char) + (setq expr-beg (point)) + (forward-sexp) + (setq expr-end (point))) + (scan-error nil)) + ;; Recursively propertize the JSXExpressionContainer=E2=80=99= s + ;; (possibly-incomplete) expression. + (js-syntax-propertize (1+ expr-beg) (if expr-end (min (1- ex= pr-end) end) end)) + ;; Ensure future propertization beginning from within the + ;; (possibly-incomplete) expression can determine JSXText + ;; context from earlier lines. + (put-text-property expr-beg (1+ expr-beg) 'js-jsx-expr (or e= xpr-end end)) ; font-lock + (put-text-property expr-beg (if expr-end (min expr-end end) = end) 'syntax-multiline t) ; syntax-propertize + ;; Exit the JSXExpressionContainer if that=E2=80=99s possibl= e, + ;; else move to the end of the propertized area. + (goto-char (if expr-end (min expr-end end) end)))) + ((=3D (char-before) ?<) + (backward-char) ; Ensure the next tag can be propertized. + (throw 'stop nil))) + (setq text-beg (point)))))) + (defun js-jsx--syntax-propertize-tag (end) "Determine if a JSXBoundaryElement is before END and propertize it. Disambiguate JSX from inequality operators and arrow functions by @@ -1916,12 +2116,16 @@ js-jsx--syntax-propertize-tag (when unambiguous ;; Save JSXBoundaryElement=E2=80=99s name=E2=80=99s match data for= font-locking. (if name-beg (put-text-property name-beg (1+ name-beg) 'js-jsx-tag= -name name-match-data)) - ;; Mark beginning and end of tag for features like indentation. + ;; Mark beginning and end of tag for font-locking. (put-text-property tag-beg (1+ tag-beg) 'js-jsx-tag-beg (cons type= (point))) - (put-text-property (point) (1+ (point)) 'js-jsx-tag-end tag-beg)))= ) + (put-text-property (point) (1+ (point)) 'js-jsx-tag-end tag-beg)) + (if (js-jsx--at-enclosing-tag-child-p) (js-jsx--syntax-propertize-ta= g-text end)))) =20 (defconst js-jsx--text-properties - '(js-jsx-tag-beg nil js-jsx-tag-end nil js-jsx-tag-name nil js-jsx-att= ribute-name nil) + (list + 'js-jsx-tag-beg nil 'js-jsx-tag-end nil + 'js-jsx-tag-name nil 'js-jsx-attribute-name nil + 'js-jsx-text nil 'js-jsx-expr nil) "Plist of text properties added by `js-syntax-propertize'.") =20 (defun js-syntax-propertize (start end) @@ -4011,6 +4215,8 @@ js-mode . js-font-lock-syntactic-face-function))) (setq-local syntax-propertize-function #'js-syntax-propertize) (add-hook 'syntax-propertize-extend-region-functions + #'syntax-propertize-multiline 'append 'local) + (add-hook 'syntax-propertize-extend-region-functions #'js--syntax-propertize-extend-region 'append 'local) (setq-local prettify-symbols-alist js--prettify-symbols-alist) =20 --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0009-Update-expectations-for-JSX-indentation-in-JSXAttrib.patch" Content-Disposition: attachment; filename*0="0009-Update-expectations-for-JSX-indentation-in-JSXAttrib.pa"; filename*1="tch" Content-Transfer-Encoding: quoted-printable >From edae9ccb0acd84cdd62566d0f96c167b29138965 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sat, 23 Mar 2019 12:33:20 -0700 Subject: [PATCH 09/19] Update expectations for JSX indentation in JSXAttr= ibute space * test/manual/indent/js-jsx.js: Align expectations for dangling closing constructs with other places in the tests. --- test/manual/indent/js-jsx.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/manual/indent/js-jsx.js b/test/manual/indent/js-jsx.js index af3c340559..2ec00c63bb 100644 --- a/test/manual/indent/js-jsx.js +++ b/test/manual/indent/js-jsx.js @@ -37,7 +37,7 @@ return ( =20 React.render( , + />, { a: 1 } @@ -242,12 +242,18 @@ export default ({ stars }) =3D> ( =20 // JS expressions should not break indentation // (https://github.com/mooz/js2-mode/issues/462). +// +// In the referenced issue, the user actually wanted indentation which +// was simply different than Emacs=E2=80=99 SGML attribute indentation. +// Nevertheless, his issue highlighted our inability to properly +// indent code with JSX inside JSXExpressionContainers inside JSX. return ( - ( -
nothing
- )} /> + ( +
nothing
+ )} />
--=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0010-Indent-JSX-as-parsed-in-a-JS-context.patch" Content-Disposition: attachment; filename="0010-Indent-JSX-as-parsed-in-a-JS-context.patch" Content-Transfer-Encoding: quoted-printable >From b9d31378f8e4ef0e2dc35a2c50862d6d56bcbebc Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sat, 23 Mar 2019 14:22:35 -0700 Subject: [PATCH 10/19] Indent JSX as parsed in a JS context MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-8 Content-Transfer-Encoding: 8bit Fixes the following issues (and re-fixes indentation issues initially fixed but later re-broken by previous commits in the process of adding comprehensive JSX support): - https://github.com/mooz/js2-mode/issues/389#issuecomment-390766873 - https://github.com/mooz/js2-mode/issues/482 - Bug#32158 - https://github.com/mooz/js2-mode/issues/462 Previously, we delegated to sgml-mode functions for JSX indentation. However, there were some problems with this approach: - sgml-mode does not anticipate tags inside attributes when indenting, which compromises JSX indentation inside JSXExpressionContainers inside JSXAttributes. - In previous iterations to provide comprehensive JSX support, it proved tedious to disambiguate =E2=80=9C<=E2=80=9D and =E2=80=9C>=E2=80= =9D as JS inequality operators and arrow functions from opening and closing angle brackets as part of SGML tags. That code evolved into a more complete JSX parsing implementation for syntax-propertize rules for font-locking, discarding the superfluous =E2=80=9C<=E2=80=9D/=E2=80=9C>= =E2=80=9D disambiguation in anticipation of using the improved JSX analysis for indentation. - Using sgml-mode functions, we controlled JSX indentation using SGML variables. However, JSX is a different thing than SGML; referencing SGML in JS was a leaky abstraction. To resolve these issues, use the text properties added by the JSX syntax-propertize code to determine the boundaries of various aspects of JSX syntax, and reimplement the sgml-mode indentation code in js-mode with better respect to JSX indentation conventions. * lisp/progmodes/js.el (js-jsx-attribute-offset): New variable to provide a way for users to still control JSX attribute offsets as they could with sgml-attribute-offset before. The value of this feature is dubious IMO, but it=E2=80=99s trivial to keep it, so let=E2=80=99s do it = just in case. (js-jsx--goto-outermost-enclosing-curly): New function. (js-jsx--enclosing-tag-pos): Refactor to be unbounded by curlies, so this function can be used to find JSXExpressionContainers within JSX. Fix bug where an enclosing JSXElement couldn=E2=80=99t be found when poin= t was at the start of its JSXClosingElement. Return the JSXClosingElement=E2=80= =99s position as well, so the JSXClosingElement can be indentified when indenting and be indented like the matching JSXOpeningElement. (js-jsx--at-enclosing-tag-child-p): js-jsx--enclosing-tag-pos now returns a list rather than a cons, so retrieve the JSXOpeningElement=E2=80= =99s end position from a list. (js-jsx--context, js-jsx--indenting): New function and variable. (js-jsx--indentation): New function replacing the prior js-jsx--indent* functions and js-jsx-indent-line=E2=80=99s implementation= . Use the JSX parsing performed in a JS context to more accurately calculate JSX indentation than by delegating to sgml-mode functions. (js--proper-indentation): Use js-jsx--indentation as yet another type of indentation. (js-jsx--as-sgml, js-jsx--outermost-enclosing-tag-pos) (js-jsx--indentation-type, js-jsx--indent-line-in-expression) (js-jsx--indent-n+1th-line): Remove obsolete functions. (js-jsx-indent-line): Refactor nearly-obsolete function to behave the same as it usually would before these changes, without respect to the binding of js-jsx-syntax. (js-jsx-mode): Remove obsolete documentation about the use of SGML variables to control indentation, and don=E2=80=99t bind indent-line-func= tion any more, because it is no longer necessary given the new implementation of js-jsx-indent-line. --- lisp/progmodes/js.el | 307 +++++++++++++++++++++++++++------------------= ------ 1 file changed, 165 insertions(+), 142 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 220cf97fdc..af83e04df4 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -584,6 +584,29 @@ js-jsx-syntax :safe 'booleanp :group 'js) =20 +(defcustom js-jsx-attribute-offset 0 + "Specifies a delta for JSXAttribute indentation. + +Let `js-indent-level' be 2. When this variable is also set to 0, +JSXAttribute indentation looks like this: + + + + +Alternatively, when this variable is also set to 2, JSXAttribute +indentation looks like this: + + + + +This variable is like `sgml-attribute-offset'." + :version "27.1" + :type 'integer + :safe 'integerp + :group 'js) + ;;; KeyMap =20 (defvar js-mode-map @@ -1938,14 +1961,21 @@ js-jsx--enclosing-curly-pos (setq parens (cdr parens)))) curly-pos)) =20 +(defun js-jsx--goto-outermost-enclosing-curly (limit) + "Set point to enclosing =E2=80=9C{=E2=80=9D at or closest after LIMIT.= " + (let (pos) + (while + (and + (setq pos (js-jsx--enclosing-curly-pos)) + (if (>=3D pos limit) (goto-char pos)) + (> pos limit))))) + (defun js-jsx--enclosing-tag-pos () "Return beginning and end of a JSXElement about point. Look backward for a JSXElement that both starts before point and also ends after point. That may be either a self-closing JSXElement or a JSXOpeningElement/JSXClosingElement pair." - (let ((start (point)) - (curly-pos (save-excursion (js-jsx--enclosing-curly-pos))) - tag-beg tag-beg-pos tag-end-pos close-tag-pos) + (let ((start (point)) tag-beg tag-beg-pos tag-end-pos close-tag-pos) (while (and (setq tag-beg (js--backward-text-property 'js-jsx-tag-beg)) @@ -1957,25 +1987,24 @@ js-jsx--enclosing-tag-pos (and (eq (car tag-beg) 'self-closing) (< start tag-end-pos)) (and (eq (car tag-beg) 'open) - (save-excursion - (goto-char tag-end-pos) - (setq close-tag-pos (js-jsx--matching-close-tag-pos)= ) - ;; The JSXOpeningElement may either be unclosed, - ;; else the closure must occur after the start - ;; point (otherwise, a miscellaneous previous - ;; JSXOpeningElement has been found, and we should - ;; keep looking back for an enclosing one). - (or (not close-tag-pos) (< start close-tag-pos))))))= )) - ;; Don=E2=80=99t return the last tag pos (if any; it wasn=E2=80=99= t enclosing). - (setq tag-beg nil)) - (and tag-beg - (or (not curly-pos) (> tag-beg-pos curly-pos)) - (cons tag-beg-pos tag-end-pos)))) + (or (< start tag-end-pos) + (save-excursion + (goto-char tag-end-pos) + (setq close-tag-pos (js-jsx--matching-close-tag-= pos)) + ;; The JSXOpeningElement may be unclosed, else + ;; the closure must occur at/after the start + ;; point (otherwise, a miscellaneous previous + ;; JSXOpeningElement has been found, so keep + ;; looking backwards for an enclosing one). + (or (not close-tag-pos) (<=3D start close-tag-po= s))))))))) + ;; Don=E2=80=99t return the last tag pos, as it wasn=E2=80=99t enc= losing. + (setq tag-beg nil close-tag-pos nil)) + (and tag-beg (list tag-beg-pos tag-end-pos close-tag-pos)))) =20 (defun js-jsx--at-enclosing-tag-child-p () "Return t if point is at an enclosing tag=E2=80=99s child." (let ((pos (save-excursion (js-jsx--enclosing-tag-pos)))) - (and pos (>=3D (point) (cdr pos))))) + (and pos (>=3D (point) (nth 1 pos))))) =20 (defun js-jsx--text-range (beg end) "Identify JSXText within a =E2=80=9C>/{/}/<=E2=80=9D pair." @@ -2515,6 +2544,118 @@ js--looking-at-broken-arrow-function-p (t (looking-at-p (concat js--name-re js--line-terminating-arrow-re))))) =20 +(defun js-jsx--context () + "Determine JSX context and move to enclosing JSX." + (let ((pos (point)) + (parse-status (syntax-ppss)) + (enclosing-tag-pos (js-jsx--enclosing-tag-pos))) + (when enclosing-tag-pos + (if (< pos (nth 1 enclosing-tag-pos)) + (if (nth 3 parse-status) + (list 'string (nth 8 parse-status)) + (list 'tag (nth 0 enclosing-tag-pos) (nth 1 enclosing-tag-po= s))) + (list 'text (nth 0 enclosing-tag-pos) (nth 2 enclosing-tag-pos))= )))) + +(defvar js-jsx--indenting nil + "Flag to prevent infinite recursion while indenting JSX.") + +(defun js-jsx--indentation (parse-status) + "Helper function for `js--proper-indentation'. +Return the proper indentation of the current line if it is part +of a JSXElement expression spanning multiple lines; otherwise, +return nil." + (let ((current-line (line-number-at-pos)) + (curly-pos (js-jsx--enclosing-curly-pos)) + nth-context context expr-p beg-line col + forward-sexp-function) ; Use the Lisp version. + ;; Find the immediate context for indentation information, but + ;; keep going to determine that point is at the N+1th line of + ;; multiline JSX. + (save-excursion + (while + (and + (setq nth-context (js-jsx--context)) + (progn + (unless context + (setq context nth-context) + (setq expr-p (and curly-pos (< (point) curly-pos)))) + (setq beg-line (line-number-at-pos)) + (and + (=3D beg-line current-line) + (or (not curly-pos) (> (point) curly-pos))))))) + (when (and context (> current-line beg-line)) + (save-excursion + ;; The column calculation is based on `sgml-calculate-indent'. + (setq col (pcase (nth 0 context) + + ('string + ;; Go back to previous non-empty line. + (while (and (> (point) (nth 1 context)) + (zerop (forward-line -1)) + (looking-at "[ \t]*$"))) + (if (> (point) (nth 1 context)) + ;; Previous line is inside the string. + (current-indentation) + (goto-char (nth 1 context)) + (1+ (current-column)))) + + ('tag + ;; Special JSX indentation rule: a =E2=80=9Cdanglin= g=E2=80=9D + ;; closing angle bracket on its own line is + ;; indented at the same level as the opening + ;; angle bracket of the JSXElement. Otherwise, + ;; indent JSXAttribute space like SGML. + (if (progn + (goto-char (nth 2 context)) + (and (=3D current-line (line-number-at-pos)) + (looking-back "^\\s-*/?>" (line-beginnin= g-position)))) + (progn + (goto-char (nth 1 context)) + (current-column)) + ;; Indent JSXAttribute space like SGML. + (goto-char (nth 1 context)) + ;; Skip tag name: + (skip-chars-forward " \t") + (skip-chars-forward "^ \t\n") + (skip-chars-forward " \t") + (if (not (eolp)) + (current-column) + ;; This is the first attribute: indent. + (goto-char (+ (nth 1 context) js-jsx-attribute-= offset)) + (+ (current-column) js-indent-level)))) + + ('text + ;; Indent to reflect nesting. + (goto-char (nth 1 context)) + (+ (current-column) + ;; The last line isn=E2=80=99t nested, but the r= est are. + (if (or (not (nth 2 context)) ; Unclosed. + (< current-line (line-number-at-pos (nth= 2 context)))) + js-indent-level + 0))) + + ))) + ;; When indenting a JSXExpressionContainer expression, use JSX + ;; indentation as a minimum, and use regular JS indentation if + ;; it=E2=80=99s deeper. + (if expr-p + (max (+ col + ;; An expression in a JSXExpressionContainer in a + ;; JSXAttribute should be indented more, except on + ;; the ending line of the JSXExpressionContainer. + (if (and (eq (nth 0 context) 'tag) + (< current-line + (save-excursion + (js-jsx--goto-outermost-enclosing-curly + (nth 1 context)) + (forward-sexp) + (line-number-at-pos)))) + js-indent-level + 0)) + (let ((js-jsx--indenting t)) ; Prevent recursion. + (js--proper-indentation parse-status))) + col)))) + (defun js--proper-indentation (parse-status) "Return the proper indentation for the current line." (save-excursion @@ -2522,6 +2663,8 @@ js--proper-indentation (cond ((nth 4 parse-status) ; inside comment (js--get-c-offset 'c (nth 8 parse-status))) ((nth 3 parse-status) 0) ; inside string + ((when (and js-jsx-syntax (not js-jsx--indenting)) + (save-excursion (js-jsx--indentation parse-status)))) ((eq (char-after) ?#) 0) ((save-excursion (js--beginning-of-macro)) 4) ;; Indent array comprehension continuation lines specially. @@ -2584,111 +2727,6 @@ js--proper-indentation (+ js-indent-level js-expr-indent-offset)) (t (prog-first-column))))) =20 -;;; JSX Indentation - -(defmacro js-jsx--as-sgml (&rest body) - "Execute BODY as if in sgml-mode." - `(with-syntax-table sgml-mode-syntax-table - ,@body)) - -(defun js-jsx--outermost-enclosing-tag-pos () - (let (context tag-pos last-tag-pos parse-status parens paren-pos curly= -pos) - (js-jsx--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=E2=80=99t 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 (=3D (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-type () - "Determine if/how the current line should be indented as JSX. - -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)) - tag-start-pos parens paren type) - (save-excursion - ;; Determine if inside a JSXElement. - (beginning-of-line) ; For exclusivity - (when (setq tag-start-pos (js-jsx--outermost-enclosing-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 - (>=3D paren tag-start-pos) - ;; A curly bracket indicates the start of an - ;; embedded expression. - (=3D (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). - (if (progn - (goto-char paren) - (ignore-errors (let (forward-sexp-function) - (forward-sexp)))) - (< current-line (line-number-at-pos)) - ;; No matching bracket implies we=E2=80=99re inside! - t)) - ;; Indicate this will be indented specially. Return - ;; nil to stop iterating too. - (progn (setq type 'expression) nil) - ;; Stop iterating when parens =3D nil. - (setq parens (cdr parens))))) - (or type 'n+1th))))) - -(defun js-jsx--indent-line-in-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) - (save-excursion - (setq offset (- (point) (progn (back-to-indentation) (point))) - indent-col (js-jsx--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-jsx--indent-n+1th-line () - "Indent the current line as JSX within JavaScript." - (js-jsx--as-sgml (sgml-indent-line))) - (defun js-indent-line () "Indent the current line as JavaScript." (interactive) @@ -2700,15 +2738,9 @@ js-indent-line (when (> offset 0) (forward-char offset))))) =20 (defun js-jsx-indent-line () - "Indent the current line as JSX (with SGML offsets). -i.e., customize JSX element indentation with `sgml-basic-offset', -`sgml-attribute-offset' et al." + "Indent the current line as JavaScript+JSX." (interactive) - (let ((type (js-jsx--indentation-type))) - (if type - (if (eq type 'n+1th) (js-jsx--indent-n+1th-line) - (js-jsx--indent-line-in-expression)) - (js-indent-line)))) + (let ((js-jsx-syntax t)) (js-indent-line))) =20 ;;; Filling =20 @@ -4281,18 +4313,9 @@ js-mode =20 ;;;###autoload (define-derived-mode js-jsx-mode js-mode "JSX" - "Major mode for editing JSX. - -To customize the indentation for this mode, set the SGML offset -variables (`sgml-basic-offset', `sgml-attribute-offset' et al.) -locally, like so: - - (defun set-jsx-indentation () - (setq-local sgml-basic-offset js-indent-level)) - (add-hook \\=3D'js-jsx-mode-hook #\\=3D'set-jsx-indentation)" + "Major mode for editing JSX." :group 'js - (setq-local js-jsx-syntax t) - (setq-local indent-line-function #'js-jsx-indent-line)) + (setq-local js-jsx-syntax t)) =20 ;;;###autoload (defalias 'javascript-mode 'js-mode) =20 --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0011-Finish-replacing-SGML-based-JSX-detection-with-js-mo.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="0011-Finish-replacing-SGML-based-JSX-detection-with-js-mo.pa"; filename*1="tch" >From c3a559b9b317a68f9bc7a35cc866c51d4ba27122 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sat, 23 Mar 2019 15:01:55 -0700 Subject: [PATCH 11/19] =?UTF-8?q?Finish=20replacing=20SGML-based=20JSX=20d?= =?UTF-8?q?etection=20with=20js-mode=E2=80=99s=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This removes the last dependency on sgml-mode for JSX-related logic. * lisp/progmodes/js.el (js-jsx--start-tag-re) (js-jsx--end-tag-re): Remove. (js-jsx--looking-at-start-tag-p) (js-jsx--looking-back-at-end-tag-p): Reimplement using text properties, using syntax information which ought to be slightly more accurate than regexps since it was found by complete parsing. --- lisp/progmodes/js.el | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index af83e04df4..df2c41332e 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -50,7 +50,6 @@ (require 'imenu) (require 'moz nil t) (require 'json) -(require 'sgml-mode) (require 'prog-mode) (eval-when-compile @@ -2211,13 +2210,10 @@ 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-jsx--looking-at-start-tag-p () "Non-nil if a JSXOpeningElement immediately follows point." - (looking-at js-jsx--start-tag-re)) + (let ((tag-beg (get-text-property (point) 'js-jsx-tag-beg))) + (and tag-beg (memq (car tag-beg) '(open self-closing))))) (defun js--looking-at-operator-p () "Return non-nil if point is on a JavaScript operator, other than a comma." @@ -2263,13 +2259,9 @@ js--find-newline-backward (setq result nil))) result)) -(defconst js-jsx--end-tag-re - (concat "\\|/>") - "Regexp matching a JSXClosingElement.") - (defun js-jsx--looking-back-at-end-tag-p () "Non-nil if a JSXClosingElement immediately precedes point." - (looking-back js-jsx--end-tag-re (point-at-bol))) + (get-text-property (point) 'js-jsx-tag-end)) (defun js--continued-expression-p () "Return non-nil if the current line continues an expression." -- 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0012-Automatically-detect-JSX-in-JavaScript-files.patch" Content-Disposition: attachment; filename="0012-Automatically-detect-JSX-in-JavaScript-files.patch" Content-Transfer-Encoding: quoted-printable >From 2d1b5259825e70f8b95e0fc42213a3d141ee4e75 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sat, 23 Mar 2019 20:14:29 -0700 Subject: [PATCH 12/19] Automatically detect JSX in JavaScript files MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-8 Content-Transfer-Encoding: 8bit * lisp/files.el (auto-mode-alist): Simply enable javascript-mode (js-mode) when opening =E2=80=9C.jsx=E2=80=9D files, sinc= e the =E2=80=9C.jsx=E2=80=9D file extension will be used as an indicator of JSX syntax by js-mode, and more code is likely to work in js-mode than js-jsx-mode, and we probably want to guide users to use js-mode (with js-jsx-syntax) instead. Code that used to work exclusively in js-jsx-mode (if anyone ever wrote any) ought to be updated to work in js-mode too when js-jsx-syntax is set to t. * lisp/progmodes/js.el (js-jsx-detect-syntax, js-jsx-regexps) (js-jsx--detect-and-enable, js-jsx--detect-after-change): New variables and functions for detecting and enabling JSX. (js-jsx-syntax): Update docstring with respect to the widened scope of the effects and use of this variable. (js-syntactic-mode-name, js--update-mode-name) (js--idly-update-mode-name, js-jsx-enable): New variable and functions for indicating when JSX is enabled. (js-mode): Detect and enable JSX. Print all enabled syntaxes after the mode name whenever Emacs goes idle; this ensures lately-enabled syntaxes are evident. (js-jsx-mode): Update mode name for consistency with the state in which JSX is enabled in js-mode. Update docstring to suggest alternative means of using JSX without this mode. Going forward, it may be best to gently guide users away from js-jsx-mode, since a =E2=80=9C= one mode per syntax extension=E2=80=9D model would not scale well if more syn= tax extensions were to be simultaneously supported (e.g. Facebook=E2=80=99s =E2=80=9CFlow=E2=80=9D). --- lisp/files.el | 3 +- lisp/progmodes/js.el | 119 +++++++++++++++++++++++++++++++++++++++++++++= +++--- 2 files changed, 115 insertions(+), 7 deletions(-) diff --git a/lisp/files.el b/lisp/files.el index 77a194b085..6ef63fd4de 100644 --- a/lisp/files.el +++ b/lisp/files.el @@ -2705,9 +2705,8 @@ auto-mode-alist ("\\.dbk\\'" . xml-mode) ("\\.dtd\\'" . sgml-mode) ("\\.ds\\(ss\\)?l\\'" . dsssl-mode) - ("\\.jsm?\\'" . javascript-mode) + ("\\.js[mx]?\\'" . javascript-mode) ("\\.json\\'" . javascript-mode) - ("\\.jsx\\'" . js-jsx-mode) ("\\.[ds]?vh?\\'" . verilog-mode) ("\\.by\\'" . bovine-grammar-mode) ("\\.wy\\'" . wisent-grammar-mode) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index df2c41332e..0bba8159c1 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -574,10 +574,30 @@ js-chain-indent :safe 'booleanp :group 'js) =20 +(defcustom js-jsx-detect-syntax t + "When non-nil, automatically detect whether JavaScript uses JSX. +`js-jsx-syntax' (which see) may be made buffer-local and set to +t. The detection strategy can be customized by adding elements +to `js-jsx-regexps', which see." + :version "27.1" + :type 'boolean + :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'." + +This enables proper font-locking and indentation of code using +Facebook=E2=80=99s =E2=80=9CJSX=E2=80=9D syntax extension for JavaScript= , for use with +Facebook=E2=80=99s =E2=80=9CReact=E2=80=9D library. Font-locking is lik= e sgml-mode. +Indentation is also like sgml-mode, although some indentation +behavior may differ slightly to align more closely with the +conventions of the React developer community. + +When `js-mode' is already enabled, you should call +`js-jsx-enable' to set this variable. + +It is set to be buffer-local (and t) when in `js-jsx-mode'." :version "27.1" :type 'boolean :safe 'booleanp @@ -4223,6 +4243,79 @@ js--js-add-resource-alias (when temp-name (delete-file temp-name)))))) =20 +;;; Syntax extensions + +(defvar js-syntactic-mode-name t + "If non-nil, print enabled syntaxes in the mode name.") + +(defun js--update-mode-name () + "Print enabled syntaxes if `js-syntactic-mode-name' is t." + (when js-syntactic-mode-name + (setq mode-name (concat "JavaScript" + (if js-jsx-syntax "+JSX" ""))))) + +(defun js--idly-update-mode-name () + "Update `mode-name' whenever Emacs goes idle. +In case `js-jsx-syntax' is updated, especially by features of +Emacs like .dir-locals.el or file variables, this ensures the +modeline eventually reflects which syntaxes are enabled." + (let (timer) + (setq timer + (run-with-idle-timer + 0 t + (lambda (buffer) + (if (buffer-live-p buffer) + (with-current-buffer buffer + (js--update-mode-name)) + (cancel-timer timer))) + (current-buffer))))) + +(defun js-jsx-enable () + "Enable JSX in the current buffer." + (interactive) + (setq-local js-jsx-syntax t) + (js--update-mode-name)) + +(defvar js-jsx-regexps + (list "\\_<\\(?:var\\|let\\|const\\|import\\)\\_>.*?React") + "Regexps for detecting JSX in JavaScript buffers. +When `js-jsx-detect-syntax' is non-nil and any of these regexps +match text near the beginning of a JavaScript buffer, +`js-jsx-syntax' (which see) will be made buffer-local and set to +t.") + +(defun js-jsx--detect-and-enable (&optional arbitrarily) + "Detect if JSX is likely to be used, and enable it if so. +Might make `js-jsx-syntax' buffer-local and set it to t. Matches +from the beginning of the buffer, unless optional arg ARBITRARILY +is non-nil. Return t after enabling, nil otherwise." + (when (or (and (buffer-file-name) + (string-match-p "\\.jsx\\'" (buffer-file-name))) + (and js-jsx-detect-syntax + (save-excursion + (unless arbitrarily + (goto-char (point-min))) + (catch 'match + (mapc + (lambda (regexp) + (if (re-search-forward regexp 4000 t) (throw 'ma= tch t))) + js-jsx-regexps) + nil)))) + (js-jsx-enable) + t)) + +(defun js-jsx--detect-after-change (beg end _len) + "Detect if JSX is likely to be used after a change. +This function is intended for use in `after-change-functions'." + (when (<=3D end 4000) + (save-excursion + (goto-char beg) + (beginning-of-line) + (save-restriction + (narrow-to-region (point) end) + (when (js-jsx--detect-and-enable 'arbitrarily) + (remove-hook 'after-change-functions #'js-jsx--detect-after-ch= ange t)))))) + ;;; Main Function =20 ;;;###autoload @@ -4259,6 +4352,12 @@ js-mode ;; Frameworks (js--update-quick-match-re) =20 + ;; Syntax extensions + (unless (js-jsx--detect-and-enable) + (add-hook 'after-change-functions #'js-jsx--detect-after-change nil = t)) + (js--update-mode-name) ; If `js-jsx-syntax' was set from outside. + (js--idly-update-mode-name) + ;; Imenu (setq imenu-case-fold-search nil) (setq imenu-create-index-function #'js--imenu-create-index) @@ -4304,10 +4403,20 @@ js-mode ) =20 ;;;###autoload -(define-derived-mode js-jsx-mode js-mode "JSX" - "Major mode for editing JSX." +(define-derived-mode js-jsx-mode js-mode "JavaScript+JSX" + "Major mode for editing JavaScript+JSX. + +Simply makes `js-jsx-syntax' buffer-local and sets it to t. + +`js-mode' may detect and enable support for JSX automatically if +it appears to be used in a JavaScript file. You could also +customize `js-jsx-regexps' to improve that detection; or, you +could set `js-jsx-syntax' to t in your init file, or in a +.dir-locals.el file, or using file variables; or, you could call +`js-jsx-enable' in `js-mode-hook'. You may be better served by +one of the aforementioned options instead of using this mode." :group 'js - (setq-local js-jsx-syntax t)) + (js-jsx-enable)) =20 ;;;###autoload (defalias 'javascript-mode 'js-mode) =20 --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0013-Improve-JSX-syntax-propertization.patch" Content-Disposition: attachment; filename="0013-Improve-JSX-syntax-propertization.patch" Content-Transfer-Encoding: quoted-printable >From d3710261f53aa72d018c873761592ad4d2185bc5 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sun, 24 Mar 2019 09:55:14 -0700 Subject: [PATCH 13/19] Improve JSX syntax propertization MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-8 Content-Transfer-Encoding: 8bit * lisp/progmodes/js.el (js-jsx--attribute-name-re): New variable. (js-jsx--syntax-propertize-tag): Allow =E2=80=9C-=E2=80=9D in JSXAttribut= e names. Fix =E2=80=9Cout of range=E2=80=9D error when typing at the end of a buffer. = Fix/improve future propertization of unfinished JSXBoundaryElements. * test/manual/indent/js-jsx-unclosed-2.js: Add tests for allowed characters in JSX. --- lisp/progmodes/js.el | 74 +++++++++++++++++++--------= ------ test/manual/indent/js-jsx-unclosed-2.js | 8 ++++ 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 0bba8159c1..5d87489b52 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -2083,11 +2083,15 @@ js-jsx--syntax-propertize-tag-text (throw 'stop nil))) (setq text-beg (point)))))) =20 +(defconst js-jsx--attribute-name-re (concat js--name-start-re + "\\(?:\\s_\\|\\sw\\|-\\)*") + "Like `js--name-re', but matches =E2=80=9C-=E2=80=9D as well.") + (defun js-jsx--syntax-propertize-tag (end) "Determine if a JSXBoundaryElement is before END and propertize it. Disambiguate JSX from inequality operators and arrow functions by testing for syntax only valid as JSX." - (let ((tag-beg (1- (point))) (type 'open) + (let ((tag-beg (1- (point))) tag-end (type 'open) name-beg name-match-data unambiguous forward-sexp-function) ; Use Lisp version. (catch 'stop @@ -2127,46 +2131,54 @@ js-jsx--syntax-propertize-tag ;; figure out what type it actually is. (if (eq type 'open) (setq type (if name-beg 'self-closing 'clo= se))) (forward-char)) - ((looking-at js--dotted-name-re) - (if (not name-beg) - (progn - ;; Don=E2=80=99t match code like =E2=80=9Cif (i < await = foo)=E2=80=9D - (if (js--unary-keyword-p (match-string 0)) (throw 'stop = nil)) - ;; Save boundaries for later fontification after - ;; unambiguously determining the code is JSX. - (setq name-beg (match-beginning 0) - name-match-data (match-data)) - (goto-char (match-end 0))) - (setq unambiguous t) ; Non-unary name followed by 2nd name =E2= =87=92 JSX - ;; Save JSXAttribute=E2=80=99s name=E2=80=99s match data for= font-locking later. - (put-text-property (match-beginning 0) (1+ (match-beginning = 0)) - 'js-jsx-attribute-name (match-data)) - (goto-char (match-end 0)) + ((and (not name-beg) (looking-at js--dotted-name-re)) + ;; Don=E2=80=99t match code like =E2=80=9Cif (i < await foo)=E2= =80=9D + (if (js--unary-keyword-p (match-string 0)) (throw 'stop nil)) + ;; Save boundaries for later fontification after + ;; unambiguously determining the code is JSX. + (setq name-beg (match-beginning 0) + name-match-data (match-data)) + (goto-char (match-end 0))) + ((and name-beg (looking-at js-jsx--attribute-name-re)) + (setq unambiguous t) ; Non-unary name followed by 2nd name =E2= =87=92 JSX + ;; Save JSXAttribute=E2=80=99s name=E2=80=99s match data for f= ont-locking later. + (put-text-property (match-beginning 0) (1+ (match-beginning 0)= ) + 'js-jsx-attribute-name (match-data)) + (goto-char (match-end 0)) + (if (>=3D (point) end) (throw 'stop nil)) + (skip-chars-forward " \t\n" end) + (if (>=3D (point) end) (throw 'stop nil)) + ;; =E2=80=9C=3D=E2=80=9D is optional for null-valued JSXAttrib= utes. + (when (=3D (char-after) ?=3D) + (forward-char) (if (>=3D (point) end) (throw 'stop nil)) (skip-chars-forward " \t\n" end) (if (>=3D (point) end) (throw 'stop nil)) - ;; =E2=80=9C=3D=E2=80=9D is optional for null-valued JSXAttr= ibutes. - (when (=3D (char-after) ?=3D) - (forward-char) - (if (>=3D (point) end) (throw 'stop nil)) - (skip-chars-forward " \t\n" end) - (if (>=3D (point) end) (throw 'stop nil)) - ;; Skip over strings (if possible). Any - ;; JSXExpressionContainer here will be parsed in the - ;; next iteration of the loop. - (when (memq (char-after) '(?\" ?\' ?\`)) - (condition-case nil - (forward-sexp) - (scan-error (throw 'stop nil))))))) + ;; Skip over strings (if possible). Any + ;; JSXExpressionContainer here will be parsed in the + ;; next iteration of the loop. + (when (memq (char-after) '(?\" ?\' ?\`)) + (condition-case nil + (forward-sexp) + (scan-error (throw 'stop nil)))))) ;; There is nothing more to check; this either isn=E2=80=99t JS= X, or ;; the tag is incomplete. (t (throw 'stop nil))))) (when unambiguous ;; Save JSXBoundaryElement=E2=80=99s name=E2=80=99s match data for= font-locking. (if name-beg (put-text-property name-beg (1+ name-beg) 'js-jsx-tag= -name name-match-data)) + ;; Prevent =E2=80=9Cout of range=E2=80=9D errors when typing at th= e end of a buffer. + (setq tag-end (if (eobp) (1- (point)) (point))) ;; Mark beginning and end of tag for font-locking. - (put-text-property tag-beg (1+ tag-beg) 'js-jsx-tag-beg (cons type= (point))) - (put-text-property (point) (1+ (point)) 'js-jsx-tag-end tag-beg)) + (put-text-property tag-beg (1+ tag-beg) 'js-jsx-tag-beg (cons type= tag-end)) + (put-text-property tag-end (1+ tag-end) 'js-jsx-tag-end tag-beg) + ;; Use text properties to extend the syntax-propertize region + ;; backward to the beginning of the JSXBoundaryElement in the + ;; future. Typically the closing angle bracket could suggest + ;; extending backward, but that would also involve more rigorous + ;; parsing, and the closing angle bracket may not even exist yet + ;; if the JSXBoundaryElement is still being typed. + (put-text-property tag-beg (1+ tag-end) 'syntax-multiline t)) (if (js-jsx--at-enclosing-tag-child-p) (js-jsx--syntax-propertize-ta= g-text end)))) =20 (defconst js-jsx--text-properties diff --git a/test/manual/indent/js-jsx-unclosed-2.js b/test/manual/indent= /js-jsx-unclosed-2.js index 8b6f33325d..843ef9b6a8 100644 --- a/test/manual/indent/js-jsx-unclosed-2.js +++ b/test/manual/indent/js-jsx-unclosed-2.js @@ -29,3 +29,11 @@ while (await foo > bar) void 0
+ +// =E2=80=9C-=E2=80=9D is not allowed in a JSXBoundaryElement=E2=80=99s = name. + + // Weirdly-indented =E2=80=9Ccontinued expression.=E2=80=9D + +// =E2=80=9C-=E2=80=9D may be used in a JSXAttribute=E2=80=99s name. + --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0014-Rename-tests-to-use-the-.jsx-file-extension.patch" Content-Disposition: attachment; filename="0014-Rename-tests-to-use-the-.jsx-file-extension.patch" Content-Transfer-Encoding: quoted-printable >From f1685dc9eb44fcc6371374ba7dd89bd24213d234 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sun, 24 Mar 2019 10:05:28 -0700 Subject: [PATCH 14/19] =3D?UTF-8?q?Rename=3D20tests=3D20to=3D20use=3D20th= e=3D20?=3D =3D?UTF-8?q?=3DE2=3D80=3D9C.jsx=3DE2=3D80=3D9D=3D20file=3D20extension?=3D MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-8 Content-Transfer-Encoding: 8bit * test/manual/indent/js-jsx-quote.js: Renamed to =E2=80=9Cjsx-quote.jsx=E2= =80=9D. * test/manual/indent/js-jsx-unclosed-1.js: Renamed to =E2=80=9Cjsx-unclosed-1.jsx=E2=80=9D. * test/manual/indent/js-jsx-unclosed-2.js: Renamed to =E2=80=9Cjsx-unclosed-2.jsx=E2=80=9D. * test/manual/indent/js-jsx.js: Renamed to =E2=80=9Cjsx.jsx=E2=80=9D. * test/manual/indent/jsx-quote.jsx: Renamed from =E2=80=9Cjs-jsx-quote.js= =E2=80=9D. * test/manual/indent/jsx-unclosed-1.jsx: Renamed from =E2=80=9Cjs-jsx-unclosed-1.js=E2=80=9D. * test/manual/indent/jsx-unclosed-2.jsx: Renamed from =E2=80=9Cjs-jsx-unclosed-2.js=E2=80=9D. * test/manual/indent/jsx.jsx: Renamed from =E2=80=9Cjs-jsx.js=E2=80=9D. --- test/manual/indent/{js-jsx-quote.js =3D> jsx-quote.jsx} | 2 -- test/manual/indent/{js-jsx-unclosed-1.js =3D> jsx-unclosed-1.jsx} | 2 -- test/manual/indent/{js-jsx-unclosed-2.js =3D> jsx-unclosed-2.jsx} | 2 -- test/manual/indent/{js-jsx.js =3D> jsx.jsx} | 2 -- 4 files changed, 8 deletions(-) rename test/manual/indent/{js-jsx-quote.js =3D> jsx-quote.jsx} (95%) rename test/manual/indent/{js-jsx-unclosed-1.js =3D> jsx-unclosed-1.jsx}= (91%) rename test/manual/indent/{js-jsx-unclosed-2.js =3D> jsx-unclosed-2.jsx}= (97%) rename test/manual/indent/{js-jsx.js =3D> jsx.jsx} (99%) diff --git a/test/manual/indent/js-jsx-quote.js b/test/manual/indent/jsx-= quote.jsx similarity index 95% rename from test/manual/indent/js-jsx-quote.js rename to test/manual/indent/jsx-quote.jsx index 4b71a65674..1b2c652873 100644 --- a/test/manual/indent/js-jsx-quote.js +++ b/test/manual/indent/jsx-quote.jsx @@ -1,5 +1,3 @@ -// -*- 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). diff --git a/test/manual/indent/js-jsx-unclosed-1.js b/test/manual/indent= /jsx-unclosed-1.jsx similarity index 91% rename from test/manual/indent/js-jsx-unclosed-1.js rename to test/manual/indent/jsx-unclosed-1.jsx index 9418aed7a1..1f5c3fba8d 100644 --- a/test/manual/indent/js-jsx-unclosed-1.js +++ b/test/manual/indent/jsx-unclosed-1.jsx @@ -1,5 +1,3 @@ -// -*- mode: js-jsx; -*- - // Local Variables: // indent-tabs-mode: nil // js-indent-level: 2 diff --git a/test/manual/indent/js-jsx-unclosed-2.js b/test/manual/indent= /jsx-unclosed-2.jsx similarity index 97% rename from test/manual/indent/js-jsx-unclosed-2.js rename to test/manual/indent/jsx-unclosed-2.jsx index 843ef9b6a8..8db25aa67f 100644 --- a/test/manual/indent/js-jsx-unclosed-2.js +++ b/test/manual/indent/jsx-unclosed-2.jsx @@ -1,5 +1,3 @@ -// -*- mode: js-jsx; -*- - // Local Variables: // indent-tabs-mode: nil // js-indent-level: 2 diff --git a/test/manual/indent/js-jsx.js b/test/manual/indent/jsx.jsx similarity index 99% rename from test/manual/indent/js-jsx.js rename to test/manual/indent/jsx.jsx index 2ec00c63bb..c2351a8cf1 100644 --- a/test/manual/indent/js-jsx.js +++ b/test/manual/indent/jsx.jsx @@ -1,5 +1,3 @@ -// -*- mode: js-jsx; -*- - var foo =3D
; =20 return ( --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0015-Indent-broken-arrow-function-bodies-as-an-N-1th-arg.patch" Content-Disposition: attachment; filename*0="0015-Indent-broken-arrow-function-bodies-as-an-N-1th-arg.pat"; filename*1="ch" Content-Transfer-Encoding: quoted-printable >From 126ac966a632fb4cb67a2033e9adc8528e6b269c Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Sun, 24 Mar 2019 13:17:12 -0700 Subject: [PATCH 15/19] Indent broken arrow function bodies as an N+1th ar= g MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-8 Content-Transfer-Encoding: 8bit * lisp/progmodes/js.el (js--line-terminating-arrow-re): Revise regexp for use with re-search-backward. (js--looking-at-broken-arrow-function-p): Remove. (js--broken-arrow-terminates-line-p): Replacement for js--looking-at-broken-arrow-function-p. Don=E2=80=99t consider whether a= n arrow appears at point (in an arglist); instead, just look for an arrow that terminates the line. (js--proper-indentation): Use js--broken-arrow-terminates-line-p. * test/manual/indent/js.js: Add test for a broken arrow as an N+1th arg. --- lisp/progmodes/js.el | 22 ++++++++-------------- test/manual/indent/js.js | 5 +++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 5d87489b52..f8dd72c22b 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -2550,23 +2550,17 @@ js--maybe-goto-declaration-keyword-end (when comma-p (goto-char (1+ declaration-keyword-end)))))))) =20 -(defconst js--line-terminating-arrow-re "\\s-*=3D>\\s-*\\(/[/*]\\|$\\)" +(defconst js--line-terminating-arrow-re "=3D>\\s-*\\(/[/*]\\|$\\)" "Regexp matching the last \"=3D>\" (arrow) token on a line. Whitespace and comments around the arrow are ignored.") =20 -(defun js--looking-at-broken-arrow-function-p () +(defun js--broken-arrow-terminates-line-p () "Helper function for `js--proper-indentation'. -Return t if point is at the start of a (possibly async) arrow -function and the last non-comment, non-whitespace token of the -current line is the \"=3D>\" token." - (when (looking-at "\\s-*async\\s-*") - (goto-char (match-end 0))) - (cond - ((eq (char-after) ?\() - (forward-list) - (looking-at-p js--line-terminating-arrow-re)) - (t (looking-at-p - (concat js--name-re js--line-terminating-arrow-re))))) +Return t if the last non-comment, non-whitespace token of the +current line is the \"=3D>\" token (of an arrow function)." + (let ((from (point))) + (end-of-line) + (re-search-backward js--line-terminating-arrow-re from t))) =20 (defun js-jsx--context () "Determine JSX context and move to enclosing JSX." @@ -2713,7 +2707,7 @@ js--proper-indentation (goto-char (nth 1 parse-status)) ; go to the opening char (if (or (not js-indent-align-list-continuation) (looking-at "[({[]\\s-*\\(/[/*]\\|$\\)") - (save-excursion (forward-char) (js--looking-at-brok= en-arrow-function-p))) + (save-excursion (forward-char) (js--broken-arrow-te= rminates-line-p))) (progn ; nothing following the opening paren/bracket (skip-syntax-backward " ") (when (eq (char-before) ?\)) (backward-list)) diff --git a/test/manual/indent/js.js b/test/manual/indent/js.js index 647d7438f4..9658c95701 100644 --- a/test/manual/indent/js.js +++ b/test/manual/indent/js.js @@ -160,6 +160,11 @@ foo.bar.baz(very =3D> // A comment snorf ); =20 +// Continuation of bug#25904; support broken arrow as N+1th arg +map(arr, (val) =3D> + val +) + // Local Variables: // indent-tabs-mode: nil // js-indent-level: 2 --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0016-Fix-counting-of-nested-self-closing-JSXOpeningElemen.patch" Content-Disposition: attachment; filename*0="0016-Fix-counting-of-nested-self-closing-JSXOpeningElemen.pa"; filename*1="tch" Content-Transfer-Encoding: quoted-printable >From 9f73db7929cfe2501e21a1f268240a5227435342 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Mon, 25 Mar 2019 20:39:48 -0700 Subject: [PATCH 16/19] Fix counting of nested self-closing JSXOpeningElem= ents * lisp/progmodes/js.el (js-jsx--matching-close-tag-pos): Fix bug where self-closing JSXOpeningElements might be missed if one was nested within another. * test/manual/indent/jsx-self-closing.jsx: Add test for bug concerning self-closing JSXOpeningElement counting. --- lisp/progmodes/js.el | 39 ++++++++++++---------------= ------ test/manual/indent/jsx-self-closing.jsx | 13 +++++++++++ 2 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 test/manual/indent/jsx-self-closing.jsx diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index f8dd72c22b..f22c68cff9 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -1934,40 +1934,29 @@ js-jsx--matching-close-tag-pos immediately before point, find a matching JSXClosingElement or JSXClosingFragment, skipping over any nested JSXElements to find the match. Return nil if a match can=E2=80=99t be found." - (let ((tag-stack 1) self-closing-pos type) + (let ((tag-stack 1) type tag-pos last-pos pos) (catch 'stop (while (re-search-forward js-jsx--tag-re nil t) - (setq type (js-jsx--matched-tag-type)) - ;; Balance the total of self-closing tags that we subtract - ;; from the stack, ignoring those tags which are never added - ;; to the stack (see below). - (unless (eq type 'self-closing) - (when (and self-closing-pos (> (point) self-closing-pos)) + (setq type (js-jsx--matched-tag-type) + tag-pos (match-beginning 0)) + ;; Clear the stack of any JSXOpeningElements which turned out + ;; to be self-closing. + (when last-pos + (setq pos (point)) + (goto-char last-pos) + (while (re-search-forward js-jsx--self-closing-re pos 'move) (setq tag-stack (1- tag-stack)))) (if (eq type 'close) (progn (setq tag-stack (1- tag-stack)) (when (=3D tag-stack 0) - (throw 'stop (match-beginning 0)))) - ;; Tags that we know are self-closing aren=E2=80=99t added to = the - ;; stack at all, because we only close the ones that we have - ;; anticipated after moving past those anticipated tags=E2=80=99 - ;; ends, and if a self-closing tag is the first tag we - ;; encounter in this loop, then it will never be anticipated - ;; (due to an optimization where we sometimes can avoid - ;; looking for self-closing tags). + (throw 'stop tag-pos))) + ;; JSXOpeningElements that we know are self-closing aren=E2=80= =99t + ;; added to the stack at all (since re-search-forward moves + ;; point after their self-closing syntax). (unless (eq type 'self-closing) (setq tag-stack (1+ tag-stack)))) - ;; Don=E2=80=99t needlessly recalculate. - (unless (and self-closing-pos (<=3D (point) self-closing-pos)) - (setq self-closing-pos nil) ; Reset if recalculating. - (save-excursion - ;; Anticipate a self-closing tag that we should make sure - ;; to subtract from the tag stack once we move past its - ;; end; we might might miss the end otherwise, due to the - ;; regexp-matching method we use to detect tags. - (when (re-search-forward js-jsx--self-closing-re nil t) - (setq self-closing-pos (match-beginning 0))))))))) + (setq last-pos (point)))))) =20 (defun js-jsx--enclosing-curly-pos () "Return position of enclosing =E2=80=9C{=E2=80=9D in a =E2=80=9C{/}=E2= =80=9D pair about point." diff --git a/test/manual/indent/jsx-self-closing.jsx b/test/manual/indent= /jsx-self-closing.jsx new file mode 100644 index 0000000000..f8ea7a138a --- /dev/null +++ b/test/manual/indent/jsx-self-closing.jsx @@ -0,0 +1,13 @@ +// 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. + +// Properly parse/indent code with a self-closing tag inside the +// attribute of another self-closing tag. +
+
} /> +
--=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0017-Indent-expressions-in-JSXAttributes-relative-to-the-.patch" Content-Disposition: attachment; filename*0="0017-Indent-expressions-in-JSXAttributes-relative-to-the-.pa"; filename*1="tch" Content-Transfer-Encoding: quoted-printable >From ee40eeb24ceb89e9bddebb94483c50f36d7facc0 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Tue, 26 Mar 2019 18:18:39 -0700 Subject: [PATCH 17/19] =3D?UTF-8?q?Indent=3D20expressions=3D20in=3D20JSXA= ttributes?=3D =3D?UTF-8?q?=3D20relative=3D20to=3D20the=3D20attribute=3DE2=3D80=3D99s=3D= 20name?=3D MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-8 Content-Transfer-Encoding: 8bit * lisp/progmodes/js.el (js-jsx--syntax-propertize-tag): Refer to the beginning of a JSXExpressionContainer=E2=80=99s associated JSXAttribute (= so line numbers can be calculated later). (js-jsx--text-properties): Also clear the new text property js-jsx-expr-attribute. (js-jsx--indenting): Remove. (js-jsx--indent-col, js-jsx--indent-attribute-line): New variables. (js-jsx--indentation): Instead of alternating between two separate column calculations, neither necessarily correct, bind the JSX column such that the second call to js--proper-indentation can use it as a base column. (js--proper-indentation): Use JSX as the base column for some indents while indenting JSX. * test/manual/indent/jsx.jsx: Add more tests for expression indents. --- lisp/progmodes/js.el | 97 +++++++++++++++++++++++++++-------------= ------ test/manual/indent/jsx.jsx | 25 ++++++++++++ 2 files changed, 83 insertions(+), 39 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index f22c68cff9..679633fc83 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -2081,7 +2081,7 @@ js-jsx--syntax-propertize-tag Disambiguate JSX from inequality operators and arrow functions by testing for syntax only valid as JSX." (let ((tag-beg (1- (point))) tag-end (type 'open) - name-beg name-match-data unambiguous + name-beg name-match-data expr-attribute-beg unambiguous forward-sexp-function) ; Use Lisp version. (catch 'stop (while (and (< (point) end) @@ -2096,8 +2096,16 @@ js-jsx--syntax-propertize-tag ;; JSXExpressionContainer as a JSXAttribute value ;; (=E2=80=9C ); =20 +return ( +
// Also dedent. +); + +return ( +
+) + // Indent void expressions (no need for contextual parens / commas) // (https://github.com/mooz/js2-mode/issues/140#issuecomment-166250016).
--=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0018-Split-JSX-indentation-calculation-into-several-funct.patch" Content-Disposition: attachment; filename*0="0018-Split-JSX-indentation-calculation-into-several-funct.pa"; filename*1="tch" Content-Transfer-Encoding: quoted-printable >From 0aea38556e7c132dd9b0b15f534f60845e15d465 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Tue, 26 Mar 2019 20:14:46 -0700 Subject: [PATCH 18/19] Split JSX indentation calculation into several functions MIME-Version: 1.0 Content-Type: text/plain; charset=3DUTF-8 Content-Transfer-Encoding: 8bit * lisp/progmodes/js.el (js-jsx--contextual-indentation) (js-jsx--expr-attribute-pos, js-jsx--expr-indentation): Extract logic from js-jsx--indentation, and improve the logic=E2=80=99s documentation. (js-jsx--indentation): Simplify by splitting into several functions (see above) and improve the logic=E2=80=99s documentation. --- lisp/progmodes/js.el | 146 ++++++++++++++++++++++++++++-----------------= ------ 1 file changed, 81 insertions(+), 65 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 679633fc83..2d29d4e443 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -2575,12 +2575,86 @@ js-jsx--context (list 'tag (nth 0 enclosing-tag-pos) (nth 1 enclosing-tag-po= s))) (list 'text (nth 0 enclosing-tag-pos) (nth 2 enclosing-tag-pos))= )))) =20 +(defun js-jsx--contextual-indentation (line context) + "Calculate indentation column for LINE from CONTEXT. +The column calculation is based off of `sgml-calculate-indent'." + (pcase (nth 0 context) + + ('string + ;; Go back to previous non-empty line. + (while (and (> (point) (nth 1 context)) + (zerop (forward-line -1)) + (looking-at "[ \t]*$"))) + (if (> (point) (nth 1 context)) + ;; Previous line is inside the string. + (current-indentation) + (goto-char (nth 1 context)) + (1+ (current-column)))) + + ('tag + ;; Special JSX indentation rule: a =E2=80=9Cdangling=E2=80=9D closi= ng angle + ;; bracket on its own line is indented at the same level as the + ;; opening angle bracket of the JSXElement. Otherwise, indent + ;; JSXAttribute space like SGML. + (if (progn + (goto-char (nth 2 context)) + (and (=3D line (line-number-at-pos)) + (looking-back "^\\s-*/?>" (line-beginning-position)))) + (progn + (goto-char (nth 1 context)) + (current-column)) + ;; Indent JSXAttribute space like SGML. + (goto-char (nth 1 context)) + ;; Skip tag name: + (skip-chars-forward " \t") + (skip-chars-forward "^ \t\n") + (skip-chars-forward " \t") + (if (not (eolp)) + (current-column) + ;; This is the first attribute: indent. + (goto-char (+ (nth 1 context) js-jsx-attribute-offset)) + (+ (current-column) js-indent-level)))) + + ('text + ;; Indent to reflect nesting. + (goto-char (nth 1 context)) + (+ (current-column) + ;; The last line isn=E2=80=99t nested, but the rest are. + (if (or (not (nth 2 context)) ; Unclosed. + (< line (line-number-at-pos (nth 2 context)))) + js-indent-level + 0))) + + )) + +(defun js-jsx--expr-attribute-pos (start limit) + "Look back from START to LIMIT for a JSXAttribute." + (save-excursion + (goto-char start) ; Skip the first curly. + ;; Skip any remaining enclosing curlies until the JSXElement=E2=80=99= s + ;; beginning position; the last curly ought to be one of a + ;; JSXExpressionContainer, which may refer to its JSXAttribute=E2=80= =99s + ;; beginning position (if it has one). + (js-jsx--goto-outermost-enclosing-curly limit) + (get-text-property (point) 'js-jsx-expr-attribute))) + (defvar js-jsx--indent-col nil "Baseline column for JS indentation within JSX.") =20 (defvar js-jsx--indent-attribute-line nil "Line relative to which indentation uses JSX as a baseline.") =20 +(defun js-jsx--expr-indentation (parse-status pos col) + "Indent using PARSE-STATUS; relative to POS, use base COL. +To indent a JSXExpressionContainer=E2=80=99s expression, calculate the J= S +indentation, using JSX indentation as the base column when +indenting relative to the beginning line of the +JSXExpressionContainer=E2=80=99s JSXAttribute (if any)." + (let* ((js-jsx--indent-col col) + (js-jsx--indent-attribute-line + (if pos (line-number-at-pos pos)))) + (js--proper-indentation parse-status))) + (defun js-jsx--indentation (parse-status) "Helper function for `js--proper-indentation'. Return the proper indentation of the current line if it is part @@ -2605,74 +2679,16 @@ js-jsx--indentation (and (=3D beg-line current-line) (or (not curly-pos) (> (point) curly-pos))))))) + ;; When on the second or later line of JSX, indent as JSX, + ;; possibly switching back to JS indentation within + ;; JSXExpressionContainers, possibly using the JSX as a base + ;; column while switching back to JS indentation. (when (and context (> current-line beg-line)) (save-excursion - ;; The column calculation is based on `sgml-calculate-indent'. - (setq col (pcase (nth 0 context) - - ('string - ;; Go back to previous non-empty line. - (while (and (> (point) (nth 1 context)) - (zerop (forward-line -1)) - (looking-at "[ \t]*$"))) - (if (> (point) (nth 1 context)) - ;; Previous line is inside the string. - (current-indentation) - (goto-char (nth 1 context)) - (1+ (current-column)))) - - ('tag - ;; Special JSX indentation rule: a =E2=80=9Cdanglin= g=E2=80=9D - ;; closing angle bracket on its own line is - ;; indented at the same level as the opening - ;; angle bracket of the JSXElement. Otherwise, - ;; indent JSXAttribute space like SGML. - (if (progn - (goto-char (nth 2 context)) - (and (=3D current-line (line-number-at-pos)) - (looking-back "^\\s-*/?>" (line-beginnin= g-position)))) - (progn - (goto-char (nth 1 context)) - (current-column)) - ;; Indent JSXAttribute space like SGML. - (goto-char (nth 1 context)) - ;; Skip tag name: - (skip-chars-forward " \t") - (skip-chars-forward "^ \t\n") - (skip-chars-forward " \t") - (if (not (eolp)) - (current-column) - ;; This is the first attribute: indent. - (goto-char (+ (nth 1 context) js-jsx-attribute-= offset)) - (+ (current-column) js-indent-level)))) - - ('text - ;; Indent to reflect nesting. - (goto-char (nth 1 context)) - (+ (current-column) - ;; The last line isn=E2=80=99t nested, but the r= est are. - (if (or (not (nth 2 context)) ; Unclosed. - (< current-line (line-number-at-pos (nth= 2 context)))) - js-indent-level - 0))) - - ))) - ;; To indent a JSXExpressionContainer=E2=80=99s expression, calcul= ate - ;; the JS indentation, possibly using JSX indentation as the - ;; base column. + (setq col (js-jsx--contextual-indentation current-line context))= ) (if expr-p - (let* ((js-jsx--indent-col col) - (expr-attribute-pos - (save-excursion - (goto-char curly-pos) ; Skip first curly. - ;; Skip any remaining enclosing curlies up until - ;; the contextual JSXElement=E2=80=99s beginning pos= ition. - (js-jsx--goto-outermost-enclosing-curly (nth 1 conte= xt)) - (get-text-property (point) 'js-jsx-expr-attribute))) - (js-jsx--indent-attribute-line - (when expr-attribute-pos - (line-number-at-pos expr-attribute-pos)))) - (js--proper-indentation parse-status)) + (js-jsx--expr-indentation + parse-status (js-jsx--expr-attribute-pos curly-pos (nth 1 con= text)) col) col)))) =20 (defun js--proper-indentation (parse-status) --=20 2.11.0 --------------D54DAF1A67FF0115BB5D3891 Content-Type: text/x-patch; name="0019-Add-tests-for-miscellaneous-JSX-parsing-feats.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="0019-Add-tests-for-miscellaneous-JSX-parsing-feats.patch" >From d986e2ef8cf6077bce60ffc06259e73ad75fb6de Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton Date: Tue, 26 Mar 2019 21:47:34 -0700 Subject: [PATCH 19/19] Add tests for miscellaneous JSX parsing feats * test/manual/indent/jsx.jsx: Add tests for JSXMemberExpression names and JSXOpeningFragment/JSXClosingFragment support (already supported). --- test/manual/indent/jsx.jsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/manual/indent/jsx.jsx b/test/manual/indent/jsx.jsx index 5004d57a0b..c200979df8 100644 --- a/test/manual/indent/jsx.jsx +++ b/test/manual/indent/jsx.jsx @@ -93,6 +93,32 @@ return ( } /> ) +// JSXMemberExpression names are parsed/indented: + +
+ + Hello World! + + +
+
+
+
+
+ +// JSXOpeningFragment and JSXClosingFragment are parsed/indented: +<> +
+ <> + Hello World! + + <> +
+
+ +
+ + // Indent void expressions (no need for contextual parens / commas) // (https://github.com/mooz/js2-mode/issues/140#issuecomment-166250016).
-- 2.11.0 --------------D54DAF1A67FF0115BB5D3891--