ÿØÿà JFIF    ÿÛ „ !.%+&8&+/1555$;@;4?.451 4,$,44444444444414444444444444444444444444444444444444ÿÀ  á á" ÿÄ     ÿÄ ?    !1AQaq"2‘¡±ÁðBRbrÑá#‚’¢²3S CñÿÄ   ÿÄ !    !1QAa‘2ÿÚ   ? 5˜Z¯V¦cø)›t/? z¨±>Õ5€¶‹Á¤·¼z¼Ü¬+ñ®v¤¨_ˆR­BFn©—˜ý®ç̝P8gýt·ÉSTŦˆìät?þé¼íìN/Þa)ì–í6ô… Ï¿øÃj´¿KÇü]ÿ ªô¹-eKànëÕHTx}ýSÜ›ÿ ”7Ø×&µ<¦  ¥ÑO¶[Ù¯ä¨ÞÃÿ PZ-¬;#õ|•oaÿ ©CìÞz3˜öː/¤­ñTûIØ}š^ mÓ%ªxˆ¥ÉŸu=Z+ISe¿45™¼u;ú&WØ÷€æßQ™®{|íx*TC“#ZŠìZ§²‹ 6pv…³¿¡äª*áZÐ%ÒOáˆo"x«OHk w±æ+¬V(kMúŸ5Vö«$ ÁrÏbàb57/luR ¸ÑÛj Òµì`Мq­û žICÀÊ•©4€Âcà¨Ï€O´<èÐ:›ù(Ë^L8þ‘ÍÌ#¸Ð_Ì©ÙK(Öz 4¬û+¸;ü’V’84‘¬ÃŽ:[â‡ÔÌáõp¢~§ªlæ£ö{®G>J¼"°‡7¯ÆÉèßû ‹É‹§ÁòÃýâßî ^ƾÙõ‹×óH#«LP½ïX=xÑÍ$|W?•~• îëÔ©ª‹ {ÝT…Kÿ ”hûâá)J*ö˜–ÔU;iÇ€/ ÆþjóZ\ýwØ=Ìm ºèËL9 ýèÆð/¨’¥öo=nË.%Îì ŽÕ¯È|{Oj²ƒE6e/ßdÄõ²Ìâ1O®ò×TsəԸhOMýíMˆ¿¼H˜l²,7Â¥#MF/Úf°Ö½± ¸–dr‹NýÊ íjqx{œÉ ä-È ¦ øÄër¨q°ð †nцýÑÄÆ’mä…n<0È™;ÁÝá¯ÁZƒ7FÀmì­ É&9ˆîéi¶ùN§Y• ÃZãAâ?•‡©‰ , ó¾IŸŠc1 4â&y­&pŠ­6;M À 0¹qç»p.á …ŸÅáK@%6·y6ƒ‰3?”úºŽ‰éX5ªPT §µ!=Mž«Ú½‹ÅgÂSâÉaþÓoö–¯ÁÔìR>5éÿ üs¶ÆUcÌ kÇR ]ÿ ù¬¼«VŽ;Â|‡~¢¦”ÏŰæ {L™Õ°Óv¹ò¸írޡעCÃ!íVÕ {¶»sŒNPg/ "uÕbkm²“$ďå¿é¹§°½æz¯6 †s¿!s–wÚÝ“™Œ °.ûj>·+™Òa…©Œ&rÝÎtÛë긪Ît’LAVp%c Úý[ÄzJ¾ÇàXXç@˜ó<êL]·T˜¾¥1Ó©V‡g´æ½¦Ý@¹óø!_@´ÞâSÁ —S3™•& ]@JHÚý©ZŽ €×æÔr»Áf!‡yÞ4Mv*èÓã_{‘åóUuљØ«Oïé*®EvÑ Œ÷‡U \"㪒ÍK+À 4“M¡ï:0¥5í!'<@î´”>Ç»&Z–ïCCV˜Ì5Šo&îhè.žû |ÓK©h$s6KìŒëã)¹hI¦GïOåóI;ììü#É$Š0…Ææ¥TØ.5­¾gn´ “ÂÖ\:hœ89G)J@„}œ:’Ò{/Š"¦_Æ×7Æ3VÇŠÊa]ÚŒÙ€Ä–=®uÁßâACZƒ§§£ Qnâ:«,×{tyø¬iÛcœÜÄ€H½ÄÍCk´÷šß .W'b¤Íåh]÷€=,Žv×cÚEÚHXJX¶îo¨FÒtèöŸ>ªª6[J®Fµ£sGÁeqõfe\íjÒÐïÄÐGˆe1Ø‹.Ø”‘Ëuø Y­ˆÜ ŽG|zùªüMpDnQWÄ”%JŠ™)â*p@Örš«ÕT2Ð%ˆG#ª„ ·¤!°ŸOTÂT¸aÚ%4&h™LµšØüÐ.F¿²ÐÞ_Ç‚¾ÅÃaÜ÷09Æ q€öy˜v‡85õN÷]¬äѼóS{°_MެúÔ#°Ç¸0åÞè2ëôPcvÆw9®ií1Ä8F™˜à‰´+‰Ik1òÝ7“Ñ×ÒsÝ\x‚h`ÞÑ`ó"|µEcý£n˜h`}GÞ !±ù²Ápü²ß6 0ïi󜵩SÈÇ7˜-ÕURO˜¦´f$ªž-Í6(œ}<„ éc øs]ŽŽ„*—¾ ìdŽ„)méª\¿êÎIg¾ØÞ~I#C/¼¼´EÁÈŽi8“©õådô·>euä ƒ'Ê×लR1ÉJE1ÐAát`t;ÇР%Ý<‡¥„ÍÆ`×Oyó)õiI€ñQaŸ4Ûù\áàaÃÔ¹HÃu¹*k€¦<„e S‡&õÏ B!ŽhüÞ`yj}mªf×\¿ Ç~æ­9‡û\՞Ǖg²1Žû5V7 !àöšm° c`ܬøÇìµÒ'P"?…´Ö,"§^•õލsÔ)6˜sæéÍR¼ ò|Sl”‹7 nPW Gòú÷½§O¯‡„l¡kSÞŒr½PÊ@æ¢pŽ-mÿ #Ÿ˜Àº¶Áä¦;ïÔæ$1££`“Õ>„—·ž)ßð³ñ#Ï Ô$¶œ‰ÊE‹À;÷º ¯«P:Ñ”8–IÊtpÞ3ª“>ê“þës4ò2OÏÕ­±zô†Õ§‰.÷ä¸;¿˜“'œ›žª}«Œ{ª±Ì 9ÔóÞÕ‡0 $íWV3Üì¬ —@kÝ4@¿r¼±½¬™›?øØæ´'Áé®CË3-g$˜ö‡×auÚi´Žp/êÛ æF›Ú2v‹ã¿¿,nB1̨ƃqÞa5͝@&Æû“él÷ \C²½UÍc ¯k×¢U ÖéQå™—-r wô ÞÏ<Ò=&=ÿ Ôê Òêˈt,i—;LîÜ á¸*ÚÃ1$êL•LÍ <É)ýÐà’ ;F™{ƒ™˜€&'}‚ãÄK`¡ÞT@I;®žZóè‚s’7®°›+§O­Åq©é»²9<Ô J ¼9O’HL»Ùïì¸rk¼Ž_ý‘TŸu[²ßÚŒ·ü÷B%¯E ŸÔX5êO´ Ç•€’I0 ÉJX` ñ¹õ%;µŸD‘«´€àwÒ™U ûئžÖö\×®×´8 ½‡ºÐÆÓ§?Àkmœ=;d5*@-ì0F Rªýš[Ü6âö̃ڸr*KA9· u*µæ£?U¸Âêí†8@¦X4 e-ò„0s{ HâUpU?¼mñRa°®a%Ð'tÉ×’\¾ÊÉ]t›h>·(Ë@R¼¡Ãt h}’O÷au<+nT…Ö…MӐ??Óe95 q>í/;&JSû °¯ÊéÞ øƒ*Ã2½Ài&:nôUl=¾¿5eˆ3”ñc|Ú2V”>„»&eE;«ÚäC p¢Û úy 9š[ŒÌx¼擼A&DåÒ¯ˆ¤ÀÌ;"˜ ÏQä¸åhÊ}Ûq«Û0WžÒ|»€ø®öCm5•\ÇÀ§Pe3£]0ÃàLDÉ‰1øªxjgwT‚÷¿LΨK‹›ùs—xˆÜ±µ kæ¸f‰‰ÜGk/LÛØ6d9ò¶ùA{ƒA3š/¬D¬khÓk‰`˜"㯒r¿±Óã jx‡°e}<Ñø\3y:'À•/h½Í€Ç4~g ?Û(¼]v‘ªlKÎâ~?O‚W%{Ì:“'©úNq¾›úo(X’¥¯ˆ nFê{Ç€ü?º'ë ø‹ì Þ09ŒÌç9Æ —ËC`j@ÓÄ(+a‹un¸#ÂꟋ{K`‘ÑÍÍ'à´»/Û,KW;Þ4²þð ï Nm|~fGÏ(…³Ã)«1ö­Õ ¥‡¨©ƒÃ™ü-s=à=U66Ï«Ýc蓦W¹íž®›nÔ%êÇìŒ<#Ü×84ån®Ð ÒåOC` ñânÑs‡¢ç 1õ%Îhì½Ã½® e:ݼUZo™`  ÅZŸŒÊ«ê1ÏÄo$q¹Þ€©ˆhÐÉä¯ñ[!…Ú˜àJ:x2$Íß&PåT£6ç— ‡Í*4Ýšçjÿ ‰É nófÐ ó(L5C•åÆ\rMÒ@ò }y-W}™üýVù—ú¢=Ù”c®‘< M ž ´Phr ¦©TD ‘ù.$´÷O‡‘V2Æò.=IUŒ=ž‡â¬i™aþÓåÙ?òUø'ØÖ•.~* šTŒ!•-×áºTâ®ä#õü'´ eýlYÅÓeÕKÂrT"CÚ@u!Óxƒ{š3€}1¿(r}%«nËamjÑ%ÑNEò v ˜à  σöK³,*º.àzù¨™Ó ÚçâU¦*¿ 9{%Ö¹ njûdaXöb) kÛÆ±ûÓ\°M7ˆÂ=û›ç¿Ã‚­V»Cg–8ÙêE- j)k$º`Ã-ùEýeBÆÇ]c¡°ñty&Òd0nõ'¡W+ƒ*|–øµFa\GQªEAÔp5\Ǽ·¼Ç8·õ -â§Ú[ ‡ uZeÖ 3}×d'+¹:ð+K†Û®s!Ï$úe€<Û”x)1»a­¡LC]¸µík…ÚàA»AYº{†ªS[¦5HÒ7ù --,ísòDØ€èk ÞÀîÜ ò@â( ËNˆë›4ô½•/¦o‡€Û7 ê•ÆêòðÜy'Án½µ á˜ݦ ndeo…[ì¶Ê,¥R³Ä=À±—–ß;£™´ñSâ*g§”ïaið‘Jå~™ÓÞ ß³Õ¢»8x埒²52>AÊb&-÷\7´éÄù€T˜,w;3{ï˜k…à¹ÄqÀ«œ{€\ ˆ¾[´¨јr &Úé„Ívˆ±8†¿]|¬ņ4I×pÞS1ÈÖz‰#Ìv‡G!YNògñ:màTz¢Ý1ô©^O=~ë|5Bã™ç•¼µõ•bÆ@úÕS¬ÈŒ#¬zünrŸ û” Z²•èðV"ÁHÚý©wÝ €7¼Ìu1hÑa3Éä û f$o¿É ™Ú›ÝçnpÒ3äÌ3†Í§,Äï]$‰/pê †«À¼¸e9­Æê_C]žƒ·ý·frÁN«, E=›Çq -‰öŒ:aÏ¿±í&£Í:-} 84‘ÿ eƒQÑeëSsuiA ³g㟥ú£?ÿ ʼn*”“÷aühe:ÊWa@ÒÞk±eØ] F Ô—r.åä˜ @ö¥ªZoÐýYL·¥S²G/‡ñ <~*ZÆ´è>JlòàÛÆ½ÿ 窘ìGN¢:I®KšJp/`íIÁÀõ#Ä-€ö­šµŒoF4|ÆQØÆ@Ì|£Ô…¢À{9˜è½Üó›€ôYÒÎYsið;ís¤€à²ˆ‚4qÉVŒI$ ‰"° æµ8cXGjœˏ¡Aâý•ËÜ¢ûï e·çLx']á"oÅÎê3¯Ç—¹”ó0nå‚âg{Œñ> S´˜îè°g238‚ãköÝfÚd´6Ò€;ò÷±¢™¼›º ¢Æ'¥Ðx'e¬ç ]bÈÆV¢ó‹kýBO ðÊâ$Ÿ!×T 3Mýמ žìٍàÌü‘8÷€àæØ8æ©6‰©L´«…oãpð„~Çk‰!ñ;‹”ÛžÍ àž±z Ÿôû øŸÝužÏ;ÿ #|u6™Þ¬ÚˆÐõA4¶â|ôl|Ê2ŽÇ¤ÝÅÇY.<#Aí.k§hóF‚”Y; M½Ö4hŸ4&›­¿tès´%FìL¥£Ãk‰ÇT¤haÁ¤ÚxfÉ`ÑìË›>i 3t‚:,–+^÷´–{Û–Nxi"x‘Ûg î¨>¥Õ܁ùZH,2Û“:8xÊ¢Çí9.É-Ìâã-=çjwµS˜dütžçwýGòú®®ûº_ˆýx$–¡ãøO EÚÛÏ÷R„×w+3£Á£öUMyR²¹âŒ°š›¸Ñãò9§Ó_Dl+Ùßc›úšGÅÌc†Ž!Ko=¶.‘Îÿ c²(2®V mª.ÿ ¹B›¹å ù„öŸSV>™ü¯$y:G¢Z×àøúdî¹û­·ýÇ´:•c LÍõi_‹ö+ÎæGÊè>OŠ•äž´§Þ{X}¨1ÚTc›»Qþ•êô°t¿OP?eæ~É{5]•ÙR£r5†nZ\ã@ &îJõ ¾àC°þV>fé¥/ü5ñÊIº_é5 ;e­h<@ Ä&æÃëE%;X,ÒãÆÞ`Oò¦kŸm#˜!ÀyÄ¢| óLšò¥Ä` ¶R=|ÈCâh5ò3DˆïF†ðÒ#ÅìÛœ?¸yhBãœí ZxßÎÄhºRK„`Þödvײ™ÀÈÑÒgŒuY w³%†ƒÓzõ ÖÏp‚dH®¦A´ù§»ÓÇMæ~)ˆð‡û:ù&Ä •vGD´À n ݇¼Ö8Fö óáà£~Ë¥x`oK|Ä?fxiØü%pìR>éò+Û±éÎ>núlFŤ'tq8LZÏvÃ?„¡ß±È⽆¯³íü@x|PöUäèØã¡ð‚ŒAìÏ"vÍwóŸÍ{ ý0.z È•Ö{,N¡£¡ŸKÕÙž>Ýœþ ÍÀ°<×EA!Å‚D™IúOÍ¡>ôG}Â` ÍßkÜL™Ž Þð™ {IøF²¹òQ3&!ÃÂÞz.d&Ï-sH¸,Ôõ˜ŽP€ 77ˆÝ¼ÊëÜw =cÕ Ú,ØÐ5ÎYÐ)ì´öœgŒ[¤ßv㙑8心>h]§µháYš£²ºÑ.{Ï7Sð•?´~×SÃKýJÛ˜ ™Íäiúu<µX¶1õ^kâçIÑ£sZ4h>j*ÔšD:4­¿_ ÷¸ Õxæÿ ¸?Mù _•­ÊÐ ä ÷ý ÑwL œ­ïnTkÛUÍN©ë:¦fV ¶ÜÔÜMªÅâA½–¿R×TXš-%iTÊT•‡Ù‚JôϐZxWÑè‰f‰òG º ×Õû2aZ7OU3[“×AT–ÞŒ…-‘¤”Ì ì&(ˆ¿­•ƒkï’:ðY¦W‘ Å)“†‘˜³Åtcø˜ñTÂwÚÇ4|üLÇªí–v- qˆèU qPE.†â‘˜µ Æ,ÐÅs]8¾„oúÑ i>ÜxxÈó)ƒ ´æÁâØ$À‰vžŸf$Ž |ãw;ÀÁIJ»b` {¦Ó¤Ú$©YÀ‘n@Óïž«9J¼êG m¤ ܯ¹ÌW4€ÐÒÅÛ‡#褕Ÿn-?í|с¥÷Ú¹¬'´ÞÜ9ÓK `hê£SÄSà?7—Wí_´…óB›»:=Ãïq`<8ñÓŒÑlú2d¬ê³£hÖ[l|$vÝro~'R®‰§°ñmY ͧäP |PUª¹·:3Œ[Û{Xÿ ºâ@‚W–Äé u‚ ¯´*=íή.pûÒdt @G‰¬ s¸ ëÉücr ÞæÑ¨Ê@>¤¢Ö±. Þ'¯°ÌME[YéïĵÂCå½ Ué©Áû'Ê9%eÔðNU”ë‘ÌsD3/®+UI˜9h.WC”빓$#:pz:YÓ ¿xž* ³$Í +$kñAŠ‹†¢ Uê>¸)_š¬÷©ßAÂÔb9ÇU ¯¾á•9¯ÏÏ÷O÷¼¼Fähal1‰3Ì[Ïr•´UCksNÐ] R‘¸¥H+§Šé†c©vÖÞ0iÓ76s†î!§=ß ¼~Ô'°Ãmäoäš³ªøi1úÉ)³yV8 CLÄØÁ‘WYïi€H6ÖÑiámø^ÈY´°Ñ7¥Û*—Ñ©L«Qƒï—Ùrÿ ›£Ð*š¸ˆL©ˆ$ˆ ÷¾D§9È®«qbqC)–ˆïv´çñsÑVT­Ø, <àïºÀO«Jý·õ àfPìð .wFšir´þ’2_Y *Æ€x\« ì€9š@ Ž|F⇥ˆkZ@hÖÄ0t¿-<“‹qµ¾*ZL¤Ú)&BJpÓF5=$„at*Zš$’ÑtdûÝRI1 2މ$€$I$#‰SÞ’Hë¬ï;Á$¡t$’`<(ñÇt)$‡Ð.Êf¢X’Kt=Éé$‚ˆªè¢oÝëòI%Rgcª÷ŠyI%¡‰ÿ !ñ)´õ $¤ Ô’IIGÿÙvim9script # Language: Vim script # Maintainer: github user lacygoill # Last Change: 2023 Feb 01 # NOTE: Whenever you change the code, make sure the tests are still passing: # # $ cd runtime/indent/ # $ make clean; make test || vimdiff testdir/vim.{ok,fail} # Config {{{1 const TIMEOUT: number = get(g:, 'vim_indent', {}) ->get('searchpair_timeout', 100) def IndentMoreInBracketBlock(): number # {{{2 if get(g:, 'vim_indent', {}) ->get('more_in_bracket_block', false) return shiftwidth() else return 0 endif enddef def IndentMoreLineContinuation(): number # {{{2 var n: any = get(g:, 'vim_indent', {}) # We inspect `g:vim_indent_cont` to stay backward compatible. ->get('line_continuation', get(g:, 'vim_indent_cont', shiftwidth() * 3)) if n->typename() == 'string' return n->eval() else return n endif enddef # }}}2 # Init {{{1 var patterns: list # Tokens {{{2 # BAR_SEPARATION {{{3 const BAR_SEPARATION: string = '[^|\\]\@1<=|' # OPENING_BRACKET {{{3 const OPENING_BRACKET: string = '[[{(]' # CLOSING_BRACKET {{{3 const CLOSING_BRACKET: string = '[]})]' # NON_BRACKET {{{3 const NON_BRACKET: string = '[^[\]{}()]' # LIST_OR_DICT_CLOSING_BRACKET {{{3 const LIST_OR_DICT_CLOSING_BRACKET: string = '[]}]' # LIST_OR_DICT_OPENING_BRACKET {{{3 const LIST_OR_DICT_OPENING_BRACKET: string = '[[{]' # CHARACTER_UNDER_CURSOR {{{3 const CHARACTER_UNDER_CURSOR: string = '\%.c.' # INLINE_COMMENT {{{3 # TODO: It is not required for an inline comment to be surrounded by whitespace. # But it might help against false positives. # To be more reliable, we should inspect the syntax, and only require whitespace # before the `#` comment leader. But that might be too costly (because of # `synstack()`). const INLINE_COMMENT: string = '\s[#"]\%(\s\|[{}]\{3}\)' # INLINE_VIM9_COMMENT {{{3 const INLINE_VIM9_COMMENT: string = '\s#' # COMMENT {{{3 # TODO: Technically, `"\s` is wrong. # # First, whitespace is not required. # Second, in Vim9, a string might appear at the start of the line. # To be sure, we should also inspect the syntax. # We can't use `INLINE_COMMENT` here. {{{ # # const COMMENT: string = $'^\s*{INLINE_COMMENT}' # ^------------^ # ✘ # # Because `INLINE_COMMENT` asserts the presence of a whitespace before the # comment leader. This assertion is not satisfied for a comment starting at the # start of the line. #}}} const COMMENT: string = '^\s*\%(#\|"\\\=\s\).*$' # DICT_KEY {{{3 const DICT_KEY: string = '^\s*\%(' .. '\%(\w\|-\)\+' .. '\|' .. '"[^"]*"' .. '\|' .. "'[^']*'" .. '\|' .. '\[[^]]\+\]' .. '\)' .. ':\%(\s\|$\)' # NOT_A_DICT_KEY {{{3 const NOT_A_DICT_KEY: string = ':\@!' # END_OF_COMMAND {{{3 const END_OF_COMMAND: string = $'\s*\%($\|||\@!\|{INLINE_COMMENT}\)' # END_OF_LINE {{{3 const END_OF_LINE: string = $'\s*\%($\|{INLINE_COMMENT}\)' # END_OF_VIM9_LINE {{{3 const END_OF_VIM9_LINE: string = $'\s*\%($\|{INLINE_VIM9_COMMENT}\)' # OPERATOR {{{3 const OPERATOR: string = '\%(^\|\s\)\%([-+*/%]\|\.\.\|||\|&&\|??\|?\|<<\|>>\|\%([=!]=\|[<>]=\=\|[=!]\~\|is\|isnot\)[?#]\=\)\%(\s\|$\)\@=\%(\s*[|<]\)\@!' # assignment operators .. '\|' .. '\s\%([-+*/%]\|\.\.\)\==\%(\s\|$\)\@=' # support `:` when used inside conditional operator `?:` .. '\|' .. '\%(\s\|^\):\%(\s\|$\)' # HEREDOC_OPERATOR {{{3 const HEREDOC_OPERATOR: string = '\s=<<\s\@=\%(\s\+\%(trim\|eval\)\)\{,2}' # PATTERN_DELIMITER {{{3 # A better regex would be: # # [^-+*/%.:# \t[:alnum:]\"|]\@=.\|->\@!\%(=\s\)\@!\|[+*/%]\%(=\s\)\@! # # But sometimes, it can be too costly and cause `E363` to be given. const PATTERN_DELIMITER: string = '[-+*/%]\%(=\s\)\@!' # }}}2 # Syntaxes {{{2 # BLOCKS {{{3 const BLOCKS: list> = [ ['if', 'el\%[se]', 'elseif\=', 'en\%[dif]'], ['for', 'endfor\='], ['wh\%[ile]', 'endw\%[hile]'], ['try', 'cat\%[ch]', 'fina\|finally\=', 'endt\%[ry]'], ['def', 'enddef'], ['fu\%[nction](\@!', 'endf\%[unction]'], ['class', 'endclass'], ['interface', 'endinterface'], ['enum', 'endenum'], ['aug\%[roup]\%(\s\+[eE][nN][dD]\)\@!\s\+\S\+', 'aug\%[roup]\s\+[eE][nN][dD]'], ] # MODIFIERS {{{3 # some keywords can be prefixed by modifiers (e.g. `def` can be prefixed by `export`) const MODIFIERS: dict = { def: ['export', 'static'], class: ['export', 'abstract', 'export abstract'], interface: ['export'], } # ... # class: ['export', 'abstract', 'export abstract'], # ... # → # ... # class: '\%(export\|abstract\|export\s\+abstract\)\s\+', # ... ->map((_, mods: list): string => '\%(' .. mods ->join('\|') ->substitute('\s\+', '\\s\\+', 'g') .. '\)' .. '\s\+') # HIGHER_ORDER_COMMAND {{{3 patterns =<< trim eval END argdo\>!\= bufdo\>!\= cdo\>!\= folddoc\%[losed]\> foldd\%[oopen]\> ldo\=\>!\= tabdo\=\> windo\> au\%[tocmd]\>.* com\%[mand]\>.* g\%[lobal]!\={PATTERN_DELIMITER}.* v\%[global]!\={PATTERN_DELIMITER}.* END const HIGHER_ORDER_COMMAND: string = $'\%(^\|{BAR_SEPARATION}\)\s*\<\%({patterns->join('\|')}\){NOT_A_DICT_KEY}' # START_MIDDLE_END {{{3 # Let's derive this constant from `BLOCKS`: # # [['if', 'el\%[se]', 'elseif\=', 'en\%[dif]'], # ['for', 'endfor\='], # ..., # [...]] # → # { # 'for': ['for', '', 'endfor\='], # 'endfor': ['for', '', 'endfor\='], # 'if': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], # 'else': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], # 'elseif': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], # 'endif': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], # ... # } var START_MIDDLE_END: dict> def Unshorten(kwd: string): string return BlockStartKeyword(kwd) enddef def BlockStartKeyword(line: string): string var kwd: string = line->matchstr('\l\+') return fullcommand(kwd, false) enddef { for kwds: list in BLOCKS var [start: string, middle: string, end: string] = [kwds[0], '', kwds[-1]] if MODIFIERS->has_key(start->Unshorten()) start = $'\%({MODIFIERS[start]}\)\={start}' endif if kwds->len() > 2 middle = kwds[1 : -2]->join('\|') endif for kwd: string in kwds START_MIDDLE_END->extend({[kwd->Unshorten()]: [start, middle, end]}) endfor endfor } START_MIDDLE_END = START_MIDDLE_END ->map((_, kwds: list) => kwds->map((_, kwd: string) => kwd == '' ? '' : $'\%(^\|{BAR_SEPARATION}\|\\%(\s*{OPERATOR}\)\@!')) lockvar! START_MIDDLE_END # ENDS_BLOCK {{{3 const ENDS_BLOCK: string = '^\s*\%(' .. BLOCKS ->copy() ->map((_, kwds: list): string => kwds[-1]) ->join('\|') .. '\|' .. CLOSING_BRACKET .. $'\){END_OF_COMMAND}' # ENDS_BLOCK_OR_CLAUSE {{{3 patterns = BLOCKS ->copy() ->map((_, kwds: list) => kwds[1 :]) ->flattennew() # `catch` and `elseif` need to be handled as special cases ->filter((_, pat: string): bool => pat->Unshorten() !~ '^\%(catch\|elseif\)\>') const ENDS_BLOCK_OR_CLAUSE: string = '^\s*\%(' .. patterns->join('\|') .. $'\){END_OF_COMMAND}' .. $'\|^\s*cat\%[ch]\%(\s\+\({PATTERN_DELIMITER}\).*\1\)\={END_OF_COMMAND}' .. $'\|^\s*elseif\=\>\%({OPERATOR}\)\@!' # STARTS_NAMED_BLOCK {{{3 patterns = [] { for kwds: list in BLOCKS for kwd: string in kwds[0 : -2] if MODIFIERS->has_key(kwd->Unshorten()) patterns += [$'\%({MODIFIERS[kwd]}\)\={kwd}'] else patterns += [kwd] endif endfor endfor } const STARTS_NAMED_BLOCK: string = $'^\s*\%(sil\%[ent]\s\+\)\=\%({patterns->join('\|')}\)\>{NOT_A_DICT_KEY}' # STARTS_CURLY_BLOCK {{{3 # TODO: `{` alone on a line is not necessarily the start of a block. # It could be a dictionary if the previous line ends with a binary/ternary # operator. This can cause an issue whenever we use `STARTS_CURLY_BLOCK` or # `LINE_CONTINUATION_AT_EOL`. const STARTS_CURLY_BLOCK: string = '\%(' .. '^\s*{' .. '\|' .. '^.*\zs\s=>\s\+{' .. '\|' .. $'^\%(\s*\|.*{BAR_SEPARATION}\s*\)\%(com\%[mand]\|au\%[tocmd]\).*\zs\s{{' .. '\)' .. END_OF_COMMAND # STARTS_FUNCTION {{{3 const STARTS_FUNCTION: string = $'^\s*\%({MODIFIERS.def}\)\=def\>{NOT_A_DICT_KEY}' # ENDS_FUNCTION {{{3 const ENDS_FUNCTION: string = $'^\s*enddef\>{END_OF_COMMAND}' # ASSIGNS_HEREDOC {{{3 const ASSIGNS_HEREDOC: string = $'^\%({COMMENT}\)\@!.*\%({HEREDOC_OPERATOR}\)\s\+\zs[A-Z]\+{END_OF_LINE}' # PLUS_MINUS_COMMAND {{{3 # In legacy, the `:+` and `:-` commands are not required to be preceded by a colon. # As a result, when `+` or `-` is alone on a line, there is ambiguity. # It might be an operator or a command. # To not break the indentation in legacy scripts, we might need to consider such # lines as commands. const PLUS_MINUS_COMMAND: string = '^\s*[+-]\s*$' # TRICKY_COMMANDS {{{3 # Some commands are tricky because they accept an argument which can be # conflated with an operator. Examples: # # argdelete * # cd - # normal! == # nunmap ( # # TODO: Other commands might accept operators as argument. Handle them too. patterns =<< trim eval END {'\'}join('\|') # }}}2 # EOL {{{2 # OPENING_BRACKET_AT_EOL {{{3 const OPENING_BRACKET_AT_EOL: string = OPENING_BRACKET .. END_OF_VIM9_LINE # CLOSING_BRACKET_AT_EOL {{{3 const CLOSING_BRACKET_AT_EOL: string = CLOSING_BRACKET .. END_OF_VIM9_LINE # COMMA_AT_EOL {{{3 const COMMA_AT_EOL: string = $',{END_OF_VIM9_LINE}' # COMMA_OR_DICT_KEY_AT_EOL {{{3 const COMMA_OR_DICT_KEY_AT_EOL: string = $'\%(,\|{DICT_KEY}\){END_OF_VIM9_LINE}' # LAMBDA_ARROW_AT_EOL {{{3 const LAMBDA_ARROW_AT_EOL: string = $'\s=>{END_OF_VIM9_LINE}' # LINE_CONTINUATION_AT_EOL {{{3 const LINE_CONTINUATION_AT_EOL: string = '\%(' .. ',' .. '\|' .. OPERATOR .. '\|' .. '\s=>' .. '\|' .. '[^=]\zs[[(]' .. '\|' .. DICT_KEY # `{` is ambiguous. # It can be the start of a dictionary or a block. # We only want to match the former. .. '\|' .. $'^\%({STARTS_CURLY_BLOCK}\)\@!.*\zs{{' .. '\)\s*\%(\s#.*\)\=$' # }}}2 # SOL {{{2 # BACKSLASH_AT_SOL {{{3 const BACKSLASH_AT_SOL: string = '^\s*\%(\\\|[#"]\\ \)' # CLOSING_BRACKET_AT_SOL {{{3 const CLOSING_BRACKET_AT_SOL: string = $'^\s*{CLOSING_BRACKET}' # LINE_CONTINUATION_AT_SOL {{{3 const LINE_CONTINUATION_AT_SOL: string = '^\s*\%(' .. '\\' .. '\|' .. '[#"]\\ ' .. '\|' .. OPERATOR .. '\|' .. '->\s*\h' .. '\|' .. '\.\h' # dict member .. '\|' .. '|' # TODO: `}` at the start of a line is not necessarily a line continuation. # Could be the end of a block. .. '\|' .. CLOSING_BRACKET .. '\)' # RANGE_AT_SOL {{{3 const RANGE_AT_SOL: string = '^\s*:\S' # }}}1 # Interface {{{1 export def Expr(lnum = v:lnum): number # {{{2 # line which is indented var line_A: dict = {text: getline(lnum), lnum: lnum} # line above, on which we'll base the indent of line A var line_B: dict if line_A->AtStartOf('HereDoc') line_A->CacheHeredoc() elseif line_A.lnum->IsInside('HereDoc') return line_A.text->HereDocIndent() elseif line_A.lnum->IsRightBelow('HereDoc') var ind: number = b:vimindent.startindent unlet! b:vimindent return ind endif # Don't move this block after the function header one. # Otherwise, we might clear the cache too early if the line following the # header is a comment. if line_A.text =~ COMMENT return CommentIndent() endif line_B = PrevCodeLine(line_A.lnum) if line_A.text =~ BACKSLASH_AT_SOL if line_B.text =~ BACKSLASH_AT_SOL return Indent(line_B.lnum) else return Indent(line_B.lnum) + IndentMoreLineContinuation() endif endif if line_A->AtStartOf('FuncHeader') && !IsInInterface() line_A.lnum->CacheFuncHeader() elseif line_A.lnum->IsInside('FuncHeader') return b:vimindent.startindent + 2 * shiftwidth() elseif line_A.lnum->IsRightBelow('FuncHeader') var startindent: number = b:vimindent.startindent unlet! b:vimindent if line_A.text =~ ENDS_FUNCTION return startindent else return startindent + shiftwidth() endif endif var past_bracket_block: dict if exists('b:vimindent') && b:vimindent->has_key('is_BracketBlock') past_bracket_block = RemovePastBracketBlock(line_A) endif if line_A->AtStartOf('BracketBlock') line_A->CacheBracketBlock() endif if line_A.lnum->IsInside('BracketBlock') var is_in_curly_block: bool = IsInCurlyBlock() for block: dict in b:vimindent.block_stack if line_A.lnum <= block.startlnum continue endif if !block->has_key('startindent') block.startindent = Indent(block.startlnum) endif if !is_in_curly_block return BracketBlockIndent(line_A, block) endif endfor endif if line_A.text->ContinuesBelowBracketBlock(line_B, past_bracket_block) && line_A.text !~ CLOSING_BRACKET_AT_SOL return past_bracket_block.startindent + (past_bracket_block.startline =~ STARTS_NAMED_BLOCK ? 2 * shiftwidth() : 0) endif # Problem: If we press `==` on the line right below the start of a multiline # lambda (split after its arrow `=>`), the indent is not correct. # Solution: Indent relative to the line above. if line_B->EndsWithLambdaArrow() return Indent(line_B.lnum) + shiftwidth() + IndentMoreInBracketBlock() endif # FIXME: Similar issue here: # # var x = [] # ->filter((_, _) => # true) # ->items() # # Press `==` on last line. # Expected: The `->items()` line is indented like `->filter(...)`. # Actual: It's indented like `true)`. # Is it worth fixing? `=ip` gives the correct indentation, because then the # cache is used. # Don't move this block before the heredoc one.{{{ # # A heredoc might be assigned on the very first line. # And if it is, we need to cache some info. #}}} # Don't move it before the function header and bracket block ones either.{{{ # # You could, because these blocks of code deal with construct which can only # appear in a Vim9 script. And in a Vim9 script, the first line is # `vim9script`. Or maybe some legacy code/comment (see `:help vim9-mix`). # But you can't find a Vim9 function header or Vim9 bracket block on the # first line. # # Anyway, even if you could, don't. First, it would be inconsistent. # Second, it could give unexpected results while we're trying to fix some # failing test. #}}} if line_A.lnum == 1 return 0 endif # Don't do that: # if line_A.text !~ '\S' # return -1 # endif # It would prevent a line from being automatically indented when using the # normal command `o`. # TODO: Can we write a test for this? if line_B.text =~ STARTS_CURLY_BLOCK return Indent(line_B.lnum) + shiftwidth() + IndentMoreInBracketBlock() elseif line_A.text =~ CLOSING_BRACKET_AT_SOL var start: number = MatchingOpenBracket(line_A) if start <= 0 return -1 endif return Indent(start) + IndentMoreInBracketBlock() elseif line_A.text =~ ENDS_BLOCK_OR_CLAUSE && !line_B->EndsWithLineContinuation() var kwd: string = BlockStartKeyword(line_A.text) if !START_MIDDLE_END->has_key(kwd) return -1 endif # If the cursor is after the match for the end pattern, we won't find # the start of the block. Let's make sure that doesn't happen. cursor(line_A.lnum, 1) var [start: string, middle: string, end: string] = START_MIDDLE_END[kwd] var block_start: number = SearchPairStart(start, middle, end) if block_start > 0 return Indent(block_start) else return -1 endif endif var base_ind: number if line_A->IsFirstLineOfCommand(line_B) line_A.isfirst = true line_B = line_B->FirstLinePreviousCommand() base_ind = Indent(line_B.lnum) if line_B->EndsWithCurlyBlock() && !line_A->IsInThisBlock(line_B.lnum) return base_ind endif else line_A.isfirst = false base_ind = Indent(line_B.lnum) var line_C: dict = PrevCodeLine(line_B.lnum) if !line_B->IsFirstLineOfCommand(line_C) || line_C.lnum <= 0 return base_ind endif endif var ind: number = base_ind + Offset(line_A, line_B) return [ind, 0]->max() enddef def g:GetVimIndent(): number # {{{2 # for backward compatibility return Expr() enddef # }}}1 # Core {{{1 def Offset( # {{{2 # we indent this line ... line_A: dict, # ... relatively to this line line_B: dict, ): number if line_B->AtStartOf('FuncHeader') && IsInInterface() return 0 # increase indentation inside a block elseif line_B.text =~ STARTS_NAMED_BLOCK || line_B->EndsWithCurlyBlock() # But don't indent if the line starting the block also closes it. if line_B->AlsoClosesBlock() return 0 # Indent twice for a line continuation in the block header itself, so that # we can easily distinguish the end of the block header from the start of # the block body. elseif (line_B->EndsWithLineContinuation() && !line_A.isfirst) || (line_A.text =~ LINE_CONTINUATION_AT_SOL && line_A.text !~ PLUS_MINUS_COMMAND) || line_A.text->Is_IN_KeywordForLoop(line_B.text) return 2 * shiftwidth() else return shiftwidth() endif # increase indentation of a line if it's the continuation of a command which # started on a previous line elseif !line_A.isfirst && (line_B->EndsWithLineContinuation() || line_A.text =~ LINE_CONTINUATION_AT_SOL) return shiftwidth() endif return 0 enddef def HereDocIndent(line_A: string): number # {{{2 # at the end of a heredoc if line_A =~ $'^\s*{b:vimindent.endmarker}$' # `END` must be at the very start of the line if the heredoc is not trimmed if !b:vimindent.is_trimmed # We can't invalidate the cache just yet. # The indent of `END` is meaningless; it's always 0. The next line # will need to be indented relative to the start of the heredoc. It # must know where it starts; it needs the cache. return 0 else var ind: number = b:vimindent.startindent # invalidate the cache so that it's not used for the next heredoc unlet! b:vimindent return ind endif endif # In a non-trimmed heredoc, all of leading whitespace is semantic. # Leave it alone. if !b:vimindent.is_trimmed # But do save the indent of the assignment line. if !b:vimindent->has_key('startindent') b:vimindent.startindent = b:vimindent.startlnum->Indent() endif return -1 endif # In a trimmed heredoc, *some* of the leading whitespace is semantic. # We want to preserve it, so we can't just indent relative to the assignment # line. That's because we're dealing with data, not with code. # Instead, we need to compute by how much the indent of the assignment line # was increased or decreased. Then, we need to apply that same change to # every line inside the body. var offset: number if !b:vimindent->has_key('offset') var old_startindent: number = b:vimindent.startindent var new_startindent: number = b:vimindent.startlnum->Indent() offset = new_startindent - old_startindent # If all the non-empty lines in the body have a higher indentation relative # to the assignment, there is no need to indent them more. # But if at least one of them does have the same indentation level (or a # lower one), then we want to indent it further (and the whole block with it). # This way, we can clearly distinguish the heredoc block from the rest of # the code. var end: number = search($'^\s*{b:vimindent.endmarker}$', 'nW') var should_indent_more: bool = range(v:lnum, end - 1) ->indexof((_, lnum: number): bool => Indent(lnum) <= old_startindent && getline(lnum) != '') >= 0 if should_indent_more offset += shiftwidth() endif b:vimindent.offset = offset b:vimindent.startindent = new_startindent endif return [0, Indent(v:lnum) + b:vimindent.offset]->max() enddef def CommentIndent(): number # {{{2 var line_B: dict line_B.lnum = prevnonblank(v:lnum - 1) line_B.text = getline(line_B.lnum) if line_B.text =~ COMMENT return Indent(line_B.lnum) endif var next: number = NextCodeLine() if next == 0 return 0 endif var vimindent_save: dict = get(b:, 'vimindent', {})->deepcopy() var ind: number = next->Expr() # The previous `Expr()` might have set or deleted `b:vimindent`. # This could cause issues (e.g. when indenting 2 commented lines above a # heredoc). Let's make sure the state of the variable is not altered. if vimindent_save->empty() unlet! b:vimindent else b:vimindent = vimindent_save endif if getline(next) =~ ENDS_BLOCK return ind + shiftwidth() else return ind endif enddef def BracketBlockIndent(line_A: dict, block: dict): number # {{{2 var ind: number = block.startindent if line_A.text =~ CLOSING_BRACKET_AT_SOL if b:vimindent.is_on_named_block_line ind += 2 * shiftwidth() endif return ind + IndentMoreInBracketBlock() endif var startline: dict = { text: block.startline, lnum: block.startlnum } if startline->EndsWithComma() || startline->EndsWithLambdaArrow() || (startline->EndsWithOpeningBracket() # TODO: Is that reliable? && block.startline !~ $'^\s*{NON_BRACKET}\+{LIST_OR_DICT_CLOSING_BRACKET},\s\+{LIST_OR_DICT_OPENING_BRACKET}') ind += shiftwidth() + IndentMoreInBracketBlock() endif if b:vimindent.is_on_named_block_line ind += shiftwidth() endif if block.is_dict && line_A.text !~ DICT_KEY ind += shiftwidth() endif return ind enddef def CacheHeredoc(line_A: dict) # {{{2 var endmarker: string = line_A.text->matchstr(ASSIGNS_HEREDOC) var endlnum: number = search($'^\s*{endmarker}$', 'nW') var is_trimmed: bool = line_A.text =~ $'.*\s\%(trim\%(\s\+eval\)\=\)\s\+[A-Z]\+{END_OF_LINE}' b:vimindent = { is_HereDoc: true, startlnum: line_A.lnum, endlnum: endlnum, endmarker: endmarker, is_trimmed: is_trimmed, } if is_trimmed b:vimindent.startindent = Indent(line_A.lnum) endif RegisterCacheInvalidation() enddef def CacheFuncHeader(startlnum: number) # {{{2 var pos: list = getcurpos() cursor(startlnum, 1) if search('(', 'W', startlnum) <= 0 return endif var endlnum: number = SearchPair('(', '', ')', 'nW') setpos('.', pos) if endlnum == startlnum return endif b:vimindent = { is_FuncHeader: true, startindent: startlnum->Indent(), endlnum: endlnum, } RegisterCacheInvalidation() enddef def CacheBracketBlock(line_A: dict) # {{{2 var pos: list = getcurpos() var opening: string = line_A.text->matchstr(CHARACTER_UNDER_CURSOR) var closing: string = {'[': ']', '{': '}', '(': ')'}[opening] var endlnum: number = SearchPair(opening, '', closing, 'nW') setpos('.', pos) if endlnum <= line_A.lnum return endif if !exists('b:vimindent') b:vimindent = { is_BracketBlock: true, is_on_named_block_line: line_A.text =~ STARTS_NAMED_BLOCK, block_stack: [], } endif var is_dict: bool var is_curly_block: bool if opening == '{' if line_A.text =~ STARTS_CURLY_BLOCK [is_dict, is_curly_block] = [false, true] else [is_dict, is_curly_block] = [true, false] endif endif b:vimindent.block_stack->insert({ is_dict: is_dict, is_curly_block: is_curly_block, startline: line_A.text, startlnum: line_A.lnum, endlnum: endlnum, }) RegisterCacheInvalidation() enddef def RegisterCacheInvalidation() # {{{2 # invalidate the cache so that it's not used for the next `=` normal command autocmd_add([{ cmd: 'unlet! b:vimindent', event: 'ModeChanged', group: '__VimIndent__', once: true, pattern: '*:n', replace: true, }]) enddef def RemovePastBracketBlock(line_A: dict): dict # {{{2 var stack: list> = b:vimindent.block_stack var removed: dict if line_A.lnum > stack[0].endlnum removed = stack[0] endif stack->filter((_, block: dict): bool => line_A.lnum <= block.endlnum) if stack->empty() unlet! b:vimindent endif return removed enddef # }}}1 # Util {{{1 # Get {{{2 def Indent(lnum: number): number # {{{3 if lnum <= 0 # Don't return `-1`. It could cause `Expr()` to return a non-multiple of `'shiftwidth'`.{{{ # # It would be OK if we were always returning `Indent()` directly. But # we don't. Most of the time, we include it in some computation # like `Indent(...) + shiftwidth()`. If `'shiftwidth'` is `4`, and # `Indent()` returns `-1`, `Expr()` will end up returning `3`. #}}} return 0 endif return indent(lnum) enddef def MatchingOpenBracket(line: dict): number # {{{3 var end: string = line.text->matchstr(CLOSING_BRACKET) var start: string = {']': '[', '}': '{', ')': '('}[end] cursor(line.lnum, 1) return SearchPairStart(start, '', end) enddef def FirstLinePreviousCommand(line: dict): dict # {{{3 var line_B: dict = line while line_B.lnum > 1 var code_line_above: dict = PrevCodeLine(line_B.lnum) if line_B.text =~ CLOSING_BRACKET_AT_SOL var n: number = MatchingOpenBracket(line_B) if n <= 0 break endif line_B.lnum = n line_B.text = getline(line_B.lnum) continue elseif line_B->IsFirstLineOfCommand(code_line_above) break endif line_B = code_line_above endwhile return line_B enddef def PrevCodeLine(lnum: number): dict # {{{3 var line: string = getline(lnum) if line =~ '^\s*[A-Z]\+$' var endmarker: string = line->matchstr('[A-Z]\+') var pos: list = getcurpos() cursor(lnum, 1) var n: number = search(ASSIGNS_HEREDOC, 'bnW') setpos('.', pos) if n > 0 line = getline(n) if line =~ $'{HEREDOC_OPERATOR}\s\+{endmarker}' return {lnum: n, text: line} endif endif endif var n: number = prevnonblank(lnum - 1) line = getline(n) while line =~ COMMENT && n > 1 n = prevnonblank(n - 1) line = getline(n) endwhile # If we get back to the first line, we return 1 no matter what; even if it's a # commented line. That should not cause an issue though. We just want to # avoid a commented line above which there is a line of code which is more # relevant. There is nothing above the first line. return {lnum: n, text: line} enddef def NextCodeLine(): number # {{{3 var last: number = line('$') if v:lnum == last return 0 endif var lnum: number = v:lnum + 1 while lnum <= last var line: string = getline(lnum) if line != '' && line !~ COMMENT return lnum endif ++lnum endwhile return 0 enddef def SearchPair( # {{{3 start: string, middle: string, end: string, flags: string, stopline = 0, ): number var s: string = start var e: string = end if start == '[' || start == ']' s = s->escape('[]') endif if end == '[' || end == ']' e = e->escape('[]') endif return searchpair('\C' .. s, (middle == '' ? '' : '\C' .. middle), '\C' .. e, flags, (): bool => InCommentOrString(), stopline, TIMEOUT) enddef def SearchPairStart( # {{{3 start: string, middle: string, end: string, ): number return SearchPair(start, middle, end, 'bnW') enddef def SearchPairEnd( # {{{3 start: string, middle: string, end: string, stopline = 0, ): number return SearchPair(start, middle, end, 'nW', stopline) enddef # }}}2 # Test {{{2 def AtStartOf(line_A: dict, syntax: string): bool # {{{3 if syntax == 'BracketBlock' return AtStartOfBracketBlock(line_A) endif var pat: string = { HereDoc: ASSIGNS_HEREDOC, FuncHeader: STARTS_FUNCTION }[syntax] return line_A.text =~ pat && (!exists('b:vimindent') || !b:vimindent->has_key('is_HereDoc')) enddef def AtStartOfBracketBlock(line_A: dict): bool # {{{3 # We ignore bracket blocks while we're indenting a function header # because it makes the logic simpler. It might mean that we don't # indent correctly a multiline bracket block inside a function header, # but that's a corner case for which it doesn't seem worth making the # code more complex. if exists('b:vimindent') && !b:vimindent->has_key('is_BracketBlock') return false endif var pos: list = getcurpos() cursor(line_A.lnum, [line_A.lnum, '$']->col()) if SearchPair(OPENING_BRACKET, '', CLOSING_BRACKET, 'bcW', line_A.lnum) <= 0 setpos('.', pos) return false endif # Don't restore the cursor position. # It needs to be on a bracket for `CacheBracketBlock()` to work as intended. return line_A->EndsWithOpeningBracket() || line_A->EndsWithCommaOrDictKey() || line_A->EndsWithLambdaArrow() enddef def ContinuesBelowBracketBlock( # {{{3 line_A: string, line_B: dict, block: dict ): bool return !block->empty() && (line_A =~ LINE_CONTINUATION_AT_SOL || line_B->EndsWithLineContinuation()) enddef def IsInside(lnum: number, syntax: string): bool # {{{3 if !exists('b:vimindent') || !b:vimindent->has_key($'is_{syntax}') return false endif if syntax == 'BracketBlock' if !b:vimindent->has_key('block_stack') || b:vimindent.block_stack->empty() return false endif return lnum <= b:vimindent.block_stack[0].endlnum endif return lnum <= b:vimindent.endlnum enddef def IsRightBelow(lnum: number, syntax: string): bool # {{{3 return exists('b:vimindent') && b:vimindent->has_key($'is_{syntax}') && lnum > b:vimindent.endlnum enddef def IsInCurlyBlock(): bool # {{{3 return b:vimindent.block_stack ->indexof((_, block: dict): bool => block.is_curly_block) >= 0 enddef def IsInThisBlock(line_A: dict, lnum: number): bool # {{{3 var pos: list = getcurpos() cursor(lnum, [lnum, '$']->col()) var end: number = SearchPairEnd('{', '', '}') setpos('.', pos) return line_A.lnum <= end enddef def IsInInterface(): bool # {{{3 return SearchPair('interface', '', 'endinterface', 'nW') > 0 enddef def IsFirstLineOfCommand(line_1: dict, line_2: dict): bool # {{{3 if line_1.text->Is_IN_KeywordForLoop(line_2.text) return false endif if line_1.text =~ RANGE_AT_SOL || line_1.text =~ PLUS_MINUS_COMMAND return true endif if line_2.text =~ DICT_KEY && !line_1->IsInThisBlock(line_2.lnum) return true endif var line_1_is_good: bool = line_1.text !~ COMMENT && line_1.text !~ DICT_KEY && line_1.text !~ LINE_CONTINUATION_AT_SOL var line_2_is_good: bool = !line_2->EndsWithLineContinuation() return line_1_is_good && line_2_is_good enddef def Is_IN_KeywordForLoop(line_1: string, line_2: string): bool # {{{3 return line_2 =~ '^\s*for\s' && line_1 =~ '^\s*in\s' enddef def InCommentOrString(): bool # {{{3 return synstack('.', col('.')) ->indexof((_, id: number): bool => synIDattr(id, 'name') =~ '\ccomment\|string\|heredoc') >= 0 enddef def AlsoClosesBlock(line_B: dict): bool # {{{3 # We know that `line_B` opens a block. # Let's see if it also closes that block. var kwd: string = BlockStartKeyword(line_B.text) if !START_MIDDLE_END->has_key(kwd) return false endif var [start: string, middle: string, end: string] = START_MIDDLE_END[kwd] var pos: list = getcurpos() cursor(line_B.lnum, 1) var block_end: number = SearchPairEnd(start, middle, end, line_B.lnum) setpos('.', pos) return block_end > 0 enddef def EndsWithComma(line: dict): bool # {{{3 return NonCommentedMatch(line, COMMA_AT_EOL) enddef def EndsWithCommaOrDictKey(line_A: dict): bool # {{{3 return NonCommentedMatch(line_A, COMMA_OR_DICT_KEY_AT_EOL) enddef def EndsWithCurlyBlock(line_B: dict): bool # {{{3 return NonCommentedMatch(line_B, STARTS_CURLY_BLOCK) enddef def EndsWithLambdaArrow(line_A: dict): bool # {{{3 return NonCommentedMatch(line_A, LAMBDA_ARROW_AT_EOL) enddef def EndsWithLineContinuation(line_B: dict): bool # {{{3 return NonCommentedMatch(line_B, LINE_CONTINUATION_AT_EOL) enddef def EndsWithOpeningBracket(line: dict): bool # {{{3 return NonCommentedMatch(line, OPENING_BRACKET_AT_EOL) enddef def EndsWithClosingBracket(line: dict): bool # {{{3 return NonCommentedMatch(line, CLOSING_BRACKET_AT_EOL) enddef def NonCommentedMatch(line: dict, pat: string): bool # {{{3 # Could happen if there is no code above us, and we're not on the 1st line. # In that case, `PrevCodeLine()` returns `{lnum: 0, line: ''}`. if line.lnum == 0 return false endif # Technically, that's wrong. A line might start with a range and end with a # line continuation symbol. But it's unlikely. And it's useful to assume the # opposite because it prevents us from conflating a mark with an operator or # the start of a list: # # not a comparison operator # v # :'< mark < # :'< mark [ # ^ # not the start of a list if line.text =~ RANGE_AT_SOL return false endif # that's not an arithmetic operator # v # catch /pattern / # # When `/` is used as a pattern delimiter, it's always present twice. # And usually, the first occurrence is in the middle of a sequence of # non-whitespace characters. If we can find such a `/`, we assume that the # trailing `/` is not an operator. # Warning: Here, don't use a too complex pattern.{{{ # # In particular, avoid backreferences. # For example, this would be too costly: # # if line.text =~ $'\%(\S*\({PATTERN_DELIMITER}\)\S\+\|\S\+\({PATTERN_DELIMITER}\)\S*\)' # .. $'\s\+\1{END_OF_COMMAND}' # # Sometimes, it could even give `E363`. #}}} var delim: string = line.text ->matchstr($'\s\+\zs{PATTERN_DELIMITER}\ze{END_OF_COMMAND}') if !delim->empty() delim = $'\V{delim}\m' if line.text =~ $'\%(\S*{delim}\S\+\|\S\+{delim}\S*\)\s\+{delim}{END_OF_COMMAND}' return false endif endif # TODO: We might still miss some corner cases:{{{ # # conflated with arithmetic division # v # substitute/pat / rep / # echo # ^--^ # ✘ # # A better way to handle all these corner cases, would be to inspect the top # of the syntax stack: # # :echo synID('.', col('.'), v:false)->synIDattr('name') # # Unfortunately, the legacy syntax plugin is not accurate enough. # For example, it doesn't highlight a slash as an operator. # }}} # `%` at the end of a line is tricky. # It might be the modulo operator or the current file (e.g. `edit %`). # Let's assume it's the latter. if line.text =~ $'%{END_OF_COMMAND}' return false endif if line.text =~ TRICKY_COMMANDS return false endif var pos: list = getcurpos() cursor(line.lnum, 1) var match_lnum: number = search(pat, 'cnW', line.lnum, TIMEOUT, (): bool => InCommentOrString()) setpos('.', pos) return match_lnum > 0 enddef # }}}1 # vim:sw=4