123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336 |
- const { conditionPromise } = require('./async-spec-helpers');
- const Random = require('random-seed');
- const { getRandomBufferRange, buildRandomLines } = require('./helpers/random');
- const TextEditorComponent = require('../src/text-editor-component');
- const TextEditorElement = require('../src/text-editor-element');
- const TextEditor = require('../src/text-editor');
- const TextBuffer = require('text-buffer');
- const { Point } = TextBuffer;
- const fs = require('fs');
- const path = require('path');
- const Grim = require('grim');
- const electron = require('electron');
- const clipboard = electron.clipboard;
- const SAMPLE_TEXT = fs.readFileSync(
- path.join(__dirname, 'fixtures', 'sample.js'),
- 'utf8'
- );
- class DummyElement extends HTMLElement {
- connectedCallback() {
- this.didAttach();
- }
- }
- window.customElements.define(
- 'text-editor-component-test-element',
- DummyElement
- );
- document.createElement('text-editor-component-test-element');
- const editors = [];
- let verticalScrollbarWidth, horizontalScrollbarHeight;
- describe('TextEditorComponent', () => {
- beforeEach(() => {
- jasmine.useRealClock();
- // Force scrollbars to be visible regardless of local system configuration
- const scrollbarStyle = document.createElement('style');
- scrollbarStyle.textContent =
- 'atom-text-editor ::-webkit-scrollbar { -webkit-appearance: none }';
- jasmine.attachToDOM(scrollbarStyle);
- if (verticalScrollbarWidth == null) {
- const { component, element } = buildComponent({
- text: 'abcdefgh\n'.repeat(10),
- width: 30,
- height: 30
- });
- verticalScrollbarWidth = getVerticalScrollbarWidth(component);
- horizontalScrollbarHeight = getHorizontalScrollbarHeight(component);
- element.remove();
- }
- });
- afterEach(() => {
- for (const editor of editors) {
- editor.destroy();
- }
- editors.length = 0;
- });
- describe('rendering', () => {
- it('renders lines and line numbers for the visible region', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 3,
- autoHeight: false
- });
- expect(queryOnScreenLineNumberElements(element).length).toBe(13);
- expect(queryOnScreenLineElements(element).length).toBe(13);
- element.style.height = 4 * component.measurements.lineHeight + 'px';
- await component.getNextUpdatePromise();
- expect(queryOnScreenLineNumberElements(element).length).toBe(9);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- await setScrollTop(component, 5 * component.getLineHeight());
- // After scrolling down beyond > 3 rows, the order of line numbers and lines
- // in the DOM is a bit weird because the first tile is recycled to the bottom
- // when it is scrolled out of view
- expect(
- queryOnScreenLineNumberElements(element).map(element =>
- element.textContent.trim()
- )
- ).toEqual(['10', '11', '12', '4', '5', '6', '7', '8', '9']);
- expect(
- queryOnScreenLineElements(element).map(
- element => element.dataset.screenRow
- )
- ).toEqual(['9', '10', '11', '3', '4', '5', '6', '7', '8']);
- expect(
- queryOnScreenLineElements(element).map(element => element.textContent)
- ).toEqual([
- editor.lineTextForScreenRow(9),
- ' ', // this line is blank in the model, but we render a space to prevent the line from collapsing vertically
- editor.lineTextForScreenRow(11),
- editor.lineTextForScreenRow(3),
- editor.lineTextForScreenRow(4),
- editor.lineTextForScreenRow(5),
- editor.lineTextForScreenRow(6),
- editor.lineTextForScreenRow(7),
- editor.lineTextForScreenRow(8)
- ]);
- await setScrollTop(component, 2.5 * component.getLineHeight());
- expect(
- queryOnScreenLineNumberElements(element).map(element =>
- element.textContent.trim()
- )
- ).toEqual(['1', '2', '3', '4', '5', '6', '7', '8', '9']);
- expect(
- queryOnScreenLineElements(element).map(
- element => element.dataset.screenRow
- )
- ).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8']);
- expect(
- queryOnScreenLineElements(element).map(element => element.textContent)
- ).toEqual([
- editor.lineTextForScreenRow(0),
- editor.lineTextForScreenRow(1),
- editor.lineTextForScreenRow(2),
- editor.lineTextForScreenRow(3),
- editor.lineTextForScreenRow(4),
- editor.lineTextForScreenRow(5),
- editor.lineTextForScreenRow(6),
- editor.lineTextForScreenRow(7),
- editor.lineTextForScreenRow(8)
- ]);
- });
- it('bases the width of the lines div on the width of the longest initially-visible screen line', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 2,
- height: 20,
- width: 100
- });
- {
- expect(editor.getApproximateLongestScreenRow()).toBe(3);
- const expectedWidth = Math.ceil(
- component.pixelPositionForScreenPosition(Point(3, Infinity)).left +
- component.getBaseCharacterWidth()
- );
- expect(element.querySelector('.lines').style.width).toBe(
- expectedWidth + 'px'
- );
- }
- {
- // Get the next update promise synchronously here to ensure we don't
- // miss the update while polling the condition.
- const nextUpdatePromise = component.getNextUpdatePromise();
- await conditionPromise(
- () => editor.getApproximateLongestScreenRow() === 6
- );
- await nextUpdatePromise;
- // Capture the width of the lines before requesting the width of
- // longest line, because making that request forces a DOM update
- const actualWidth = element.querySelector('.lines').style.width;
- const expectedWidth = Math.ceil(
- component.pixelPositionForScreenPosition(Point(6, Infinity)).left +
- component.getBaseCharacterWidth()
- );
- expect(actualWidth).toBe(expectedWidth + 'px');
- }
- // eslint-disable-next-line no-lone-blocks
- {
- // Make sure we do not throw an error if a synchronous update is
- // triggered before measuring the longest line from a
- // previously-scheduled update.
- editor.getBuffer().insert(Point(12, Infinity), 'x'.repeat(100));
- expect(editor.getLongestScreenRow()).toBe(12);
- TextEditorComponent.getScheduler().readDocument(() => {
- // This will happen before the measurement phase of the update
- // triggered above.
- component.pixelPositionForScreenPosition(Point(11, Infinity));
- });
- await component.getNextUpdatePromise();
- }
- });
- it('re-renders lines when their height changes', async () => {
- const { component, element } = buildComponent({
- rowsPerTile: 3,
- autoHeight: false
- });
- element.style.height = 4 * component.measurements.lineHeight + 'px';
- await component.getNextUpdatePromise();
- expect(queryOnScreenLineNumberElements(element).length).toBe(9);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- element.style.lineHeight = '2.0';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(queryOnScreenLineNumberElements(element).length).toBe(6);
- expect(queryOnScreenLineElements(element).length).toBe(6);
- element.style.lineHeight = '0.7';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(queryOnScreenLineNumberElements(element).length).toBe(12);
- expect(queryOnScreenLineElements(element).length).toBe(12);
- element.style.lineHeight = '0.05';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(queryOnScreenLineNumberElements(element).length).toBe(13);
- expect(queryOnScreenLineElements(element).length).toBe(13);
- element.style.lineHeight = '0';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(queryOnScreenLineNumberElements(element).length).toBe(13);
- expect(queryOnScreenLineElements(element).length).toBe(13);
- element.style.lineHeight = '1';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(queryOnScreenLineNumberElements(element).length).toBe(9);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- });
- it('makes the content at least as tall as the scroll container client height', async () => {
- const { component, editor } = buildComponent({
- text: 'a'.repeat(100),
- width: 50,
- height: 100
- });
- expect(component.refs.content.offsetHeight).toBe(
- 100 - getHorizontalScrollbarHeight(component)
- );
- editor.setText('a\n'.repeat(30));
- await component.getNextUpdatePromise();
- expect(component.refs.content.offsetHeight).toBeGreaterThan(100);
- expect(component.refs.content.offsetHeight).toBeNear(
- component.getContentHeight(),
- 2
- );
- });
- it('honors the scrollPastEnd option by adding empty space equivalent to the clientHeight to the end of the content area', async () => {
- const { component, editor } = buildComponent({
- autoHeight: false,
- autoWidth: false
- });
- await editor.update({ scrollPastEnd: true });
- await setEditorHeightInLines(component, 6);
- // scroll to end
- await setScrollTop(component, Infinity);
- expect(component.getFirstVisibleRow()).toBe(
- editor.getScreenLineCount() - 3
- );
- editor.update({ scrollPastEnd: false });
- await component.getNextUpdatePromise(); // wait for scrollable content resize
- expect(component.getFirstVisibleRow()).toBe(
- editor.getScreenLineCount() - 6
- );
- // Always allows at least 3 lines worth of overscroll if the editor is short
- await setEditorHeightInLines(component, 2);
- await editor.update({ scrollPastEnd: true });
- await setScrollTop(component, Infinity);
- expect(component.getFirstVisibleRow()).toBe(
- editor.getScreenLineCount() + 1
- );
- });
- it('does not fire onDidChangeScrollTop listeners when assigning the same maximal value and the content height has fractional pixels (regression)', async () => {
- const { component, element, editor } = buildComponent({
- autoHeight: false,
- autoWidth: false
- });
- await setEditorHeightInLines(component, 3);
- // Force a fractional content height with a block decoration
- const item = document.createElement('div');
- item.style.height = '10.6px';
- editor.decorateMarker(editor.markBufferPosition([0, 0]), {
- type: 'block',
- item
- });
- await component.getNextUpdatePromise();
- component.setScrollTop(Infinity);
- element.onDidChangeScrollTop(newScrollTop => {
- throw new Error('Scroll top should not have changed');
- });
- component.setScrollTop(component.getScrollTop());
- });
- it('gives the line number tiles an explicit width and height so their layout can be strictly contained', async () => {
- const { component, editor } = buildComponent({ rowsPerTile: 3 });
- const lineNumberGutterElement =
- component.refs.gutterContainer.refs.lineNumberGutter.element;
- expect(lineNumberGutterElement.offsetHeight).toBeNear(
- component.getScrollHeight()
- );
- for (const child of lineNumberGutterElement.children) {
- expect(child.offsetWidth).toBe(lineNumberGutterElement.offsetWidth);
- if (!child.classList.contains('line-number')) {
- for (const lineNumberElement of child.children) {
- expect(lineNumberElement.offsetWidth).toBe(
- lineNumberGutterElement.offsetWidth
- );
- }
- }
- }
- editor.setText('x\n'.repeat(99));
- await component.getNextUpdatePromise();
- expect(lineNumberGutterElement.offsetHeight).toBeNear(
- component.getScrollHeight()
- );
- for (const child of lineNumberGutterElement.children) {
- expect(child.offsetWidth).toBe(lineNumberGutterElement.offsetWidth);
- if (!child.classList.contains('line-number')) {
- for (const lineNumberElement of child.children) {
- expect(lineNumberElement.offsetWidth).toBe(
- lineNumberGutterElement.offsetWidth
- );
- }
- }
- }
- });
- it('keeps the number of tiles stable when the visible line count changes during vertical scrolling', async () => {
- const { component } = buildComponent({
- rowsPerTile: 3,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 5.5);
- expect(component.refs.lineTiles.children.length).toBe(3 + 2); // account for cursors and highlights containers
- await setScrollTop(component, 0.5 * component.getLineHeight());
- expect(component.refs.lineTiles.children.length).toBe(3 + 2); // account for cursors and highlights containers
- await setScrollTop(component, 1 * component.getLineHeight());
- expect(component.refs.lineTiles.children.length).toBe(3 + 2); // account for cursors and highlights containers
- });
- it('recycles tiles on resize', async () => {
- const { component } = buildComponent({
- rowsPerTile: 2,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 7);
- await setScrollTop(component, 3.5 * component.getLineHeight());
- const lineNode = lineNodeForScreenRow(component, 7);
- await setEditorHeightInLines(component, 4);
- expect(lineNodeForScreenRow(component, 7)).toBe(lineNode);
- });
- it("updates lines numbers when a row's foldability changes (regression)", async () => {
- const { component, editor } = buildComponent({ text: 'abc\n' });
- editor.setCursorBufferPosition([1, 0]);
- await component.getNextUpdatePromise();
- expect(
- lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')
- ).toBeNull();
- editor.insertText(' def');
- await component.getNextUpdatePromise();
- expect(
- lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')
- ).toBeDefined();
- editor.undo();
- await component.getNextUpdatePromise();
- expect(
- lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')
- ).toBeNull();
- });
- it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => {
- const { component } = buildComponent({
- text: 'abc\n de\nfghijklm\n no',
- softWrapped: true
- });
- await setEditorWidthInCharacters(component, 5);
- expect(
- lineNumberNodeForScreenRow(component, 0).classList.contains('foldable')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('foldable')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 2).classList.contains('foldable')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 3).classList.contains('foldable')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 4).classList.contains('foldable')
- ).toBe(false);
- });
- it('renders dummy vertical and horizontal scrollbars when content overflows', async () => {
- const { component, editor } = buildComponent({
- height: 100,
- width: 100
- });
- const verticalScrollbar = component.refs.verticalScrollbar.element;
- const horizontalScrollbar = component.refs.horizontalScrollbar.element;
- expect(verticalScrollbar.scrollHeight).toBeNear(
- component.getContentHeight()
- );
- expect(horizontalScrollbar.scrollWidth).toBeNear(
- component.getContentWidth()
- );
- expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0);
- expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0);
- expect(verticalScrollbar.style.bottom).toBe(
- getVerticalScrollbarWidth(component) + 'px'
- );
- expect(verticalScrollbar.style.visibility).toBe('');
- expect(horizontalScrollbar.style.right).toBe(
- getHorizontalScrollbarHeight(component) + 'px'
- );
- expect(horizontalScrollbar.style.visibility).toBe('');
- expect(component.refs.scrollbarCorner).toBeDefined();
- setScrollTop(component, 100);
- await setScrollLeft(component, 100);
- expect(verticalScrollbar.scrollTop).toBe(100);
- expect(horizontalScrollbar.scrollLeft).toBe(100);
- verticalScrollbar.scrollTop = 120;
- horizontalScrollbar.scrollLeft = 120;
- await component.getNextUpdatePromise();
- expect(component.getScrollTop()).toBe(120);
- expect(component.getScrollLeft()).toBe(120);
- editor.setText('a\n'.repeat(15));
- await component.getNextUpdatePromise();
- expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0);
- expect(getHorizontalScrollbarHeight(component)).toBe(0);
- expect(verticalScrollbar.style.visibility).toBe('');
- expect(horizontalScrollbar.style.visibility).toBe('hidden');
- editor.setText('a'.repeat(100));
- await component.getNextUpdatePromise();
- expect(getVerticalScrollbarWidth(component)).toBe(0);
- expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0);
- expect(verticalScrollbar.style.visibility).toBe('hidden');
- expect(horizontalScrollbar.style.visibility).toBe('');
- editor.setText('');
- await component.getNextUpdatePromise();
- expect(getVerticalScrollbarWidth(component)).toBe(0);
- expect(getHorizontalScrollbarHeight(component)).toBe(0);
- expect(verticalScrollbar.style.visibility).toBe('hidden');
- expect(horizontalScrollbar.style.visibility).toBe('hidden');
- });
- describe('when scrollbar styles change or the editor element is detached and then reattached', () => {
- it('updates the bottom/right of dummy scrollbars and client height/width measurements', async () => {
- const { component, element, editor } = buildComponent({
- height: 100,
- width: 100
- });
- expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10);
- expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10);
- setScrollTop(component, 20);
- setScrollLeft(component, 10);
- await component.getNextUpdatePromise();
- // Updating scrollbar styles.
- const style = document.createElement('style');
- style.textContent =
- '::-webkit-scrollbar { height: 10px; width: 10px; }';
- jasmine.attachToDOM(style);
- TextEditor.didUpdateScrollbarStyles();
- await component.getNextUpdatePromise();
- expect(getHorizontalScrollbarHeight(component)).toBeNear(10);
- expect(getVerticalScrollbarWidth(component)).toBeNear(10);
- expect(
- component.refs.horizontalScrollbar.element.style.right
- ).toHaveNearPixels('10px');
- expect(
- component.refs.verticalScrollbar.element.style.bottom
- ).toHaveNearPixels('10px');
- expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeNear(
- 10
- );
- expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear(20);
- expect(component.getScrollContainerClientHeight()).toBeNear(100 - 10);
- expect(component.getScrollContainerClientWidth()).toBeNear(
- 100 - component.getGutterContainerWidth() - 10
- );
- // Detaching and re-attaching the editor element.
- element.remove();
- jasmine.attachToDOM(element);
- expect(getHorizontalScrollbarHeight(component)).toBeNear(10);
- expect(getVerticalScrollbarWidth(component)).toBeNear(10);
- expect(
- component.refs.horizontalScrollbar.element.style.right
- ).toHaveNearPixels('10px');
- expect(
- component.refs.verticalScrollbar.element.style.bottom
- ).toHaveNearPixels('10px');
- expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeNear(
- 10
- );
- expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear(20);
- expect(component.getScrollContainerClientHeight()).toBeNear(100 - 10);
- expect(component.getScrollContainerClientWidth()).toBeNear(
- 100 - component.getGutterContainerWidth() - 10
- );
- // Ensure we don't throw an error trying to remeasure non-existent scrollbars for mini editors.
- await editor.update({ mini: true });
- TextEditor.didUpdateScrollbarStyles();
- component.scheduleUpdate();
- await component.getNextUpdatePromise();
- });
- });
- it('renders cursors within the visible row range', async () => {
- const { component, element, editor } = buildComponent({
- height: 40,
- rowsPerTile: 2
- });
- await setScrollTop(component, 100);
- expect(component.getRenderedStartRow()).toBe(4);
- expect(component.getRenderedEndRow()).toBe(10);
- editor.setCursorScreenPosition([0, 0], { autoscroll: false }); // out of view
- editor.addCursorAtScreenPosition([2, 2], { autoscroll: false }); // out of view
- editor.addCursorAtScreenPosition([4, 0], { autoscroll: false }); // line start
- editor.addCursorAtScreenPosition([4, 4], { autoscroll: false }); // at token boundary
- editor.addCursorAtScreenPosition([4, 6], { autoscroll: false }); // within token
- editor.addCursorAtScreenPosition([5, Infinity], { autoscroll: false }); // line end
- editor.addCursorAtScreenPosition([10, 2], { autoscroll: false }); // out of view
- await component.getNextUpdatePromise();
- let cursorNodes = Array.from(element.querySelectorAll('.cursor'));
- expect(cursorNodes.length).toBe(4);
- verifyCursorPosition(component, cursorNodes[0], 4, 0);
- verifyCursorPosition(component, cursorNodes[1], 4, 4);
- verifyCursorPosition(component, cursorNodes[2], 4, 6);
- verifyCursorPosition(component, cursorNodes[3], 5, 30);
- editor.setCursorScreenPosition([8, 11], { autoscroll: false });
- await component.getNextUpdatePromise();
- cursorNodes = Array.from(element.querySelectorAll('.cursor'));
- expect(cursorNodes.length).toBe(1);
- verifyCursorPosition(component, cursorNodes[0], 8, 11);
- editor.setCursorScreenPosition([0, 0], { autoscroll: false });
- await component.getNextUpdatePromise();
- cursorNodes = Array.from(element.querySelectorAll('.cursor'));
- expect(cursorNodes.length).toBe(0);
- editor.setSelectedScreenRange([[8, 0], [12, 0]], { autoscroll: false });
- await component.getNextUpdatePromise();
- cursorNodes = Array.from(element.querySelectorAll('.cursor'));
- expect(cursorNodes.length).toBe(0);
- });
- it('hides cursors with non-empty selections when showCursorOnSelection is false', async () => {
- const { component, element, editor } = buildComponent();
- editor.setSelectedScreenRanges([[[0, 0], [0, 3]], [[1, 0], [1, 0]]]);
- await component.getNextUpdatePromise();
- {
- const cursorNodes = Array.from(element.querySelectorAll('.cursor'));
- expect(cursorNodes.length).toBe(2);
- verifyCursorPosition(component, cursorNodes[0], 0, 3);
- verifyCursorPosition(component, cursorNodes[1], 1, 0);
- }
- editor.update({ showCursorOnSelection: false });
- await component.getNextUpdatePromise();
- {
- const cursorNodes = Array.from(element.querySelectorAll('.cursor'));
- expect(cursorNodes.length).toBe(1);
- verifyCursorPosition(component, cursorNodes[0], 1, 0);
- }
- editor.setSelectedScreenRanges([[[0, 0], [0, 3]], [[1, 0], [1, 4]]]);
- await component.getNextUpdatePromise();
- {
- const cursorNodes = Array.from(element.querySelectorAll('.cursor'));
- expect(cursorNodes.length).toBe(0);
- }
- });
- /**
- * TODO: FAILING TEST - This test fails with the following output:
- * Error: Timed out waiting on anonymous condition at
- * conditionPromise (/home/runner/work/pulsar/pulsar/spec/async-spec-helpers.js:20:13)
- */
- xit('blinks cursors when the editor is focused and the cursors are not moving', async () => {
- assertDocumentFocused();
- const { component, element, editor } = buildComponent();
- component.props.cursorBlinkPeriod = 30;
- component.props.cursorBlinkResumeDelay = 30;
- editor.addCursorAtScreenPosition([1, 0]);
- element.focus();
- await component.getNextUpdatePromise();
- const [cursor1, cursor2] = element.querySelectorAll('.cursor');
- await conditionPromise(
- () =>
- getComputedStyle(cursor1).opacity === '1' &&
- getComputedStyle(cursor2).opacity === '1'
- );
- await conditionPromise(
- () =>
- getComputedStyle(cursor1).opacity === '0' &&
- getComputedStyle(cursor2).opacity === '0'
- );
- await conditionPromise(
- () =>
- getComputedStyle(cursor1).opacity === '1' &&
- getComputedStyle(cursor2).opacity === '1'
- );
- editor.moveRight();
- await component.getNextUpdatePromise();
- expect(getComputedStyle(cursor1).opacity).toBe('1');
- expect(getComputedStyle(cursor2).opacity).toBe('1');
- });
- it('gives cursors at the end of lines the width of an "x" character', async () => {
- const { component, element, editor } = buildComponent();
- editor.setText('abcde');
- await setEditorWidthInCharacters(component, 5.5);
- editor.setCursorScreenPosition([0, Infinity]);
- await component.getNextUpdatePromise();
- expect(element.querySelector('.cursor').offsetWidth).toBe(
- Math.round(component.getBaseCharacterWidth())
- );
- // Clip cursor width when soft-wrap is on and the cursor is at the end of
- // the line. This prevents the parent tile from disabling sub-pixel
- // anti-aliasing. For some reason, adding overflow: hidden to the cursor
- // container doesn't solve this issue so we're adding this workaround instead.
- editor.setSoftWrapped(true);
- await component.getNextUpdatePromise();
- expect(element.querySelector('.cursor').offsetWidth).toBeLessThan(
- Math.round(component.getBaseCharacterWidth())
- );
- });
- it('positions and sizes cursors correctly when they are located next to a fold marker', async () => {
- const { component, element, editor } = buildComponent();
- editor.foldBufferRange([[0, 3], [0, 6]]);
- editor.setCursorScreenPosition([0, 3]);
- await component.getNextUpdatePromise();
- verifyCursorPosition(component, element.querySelector('.cursor'), 0, 3);
- editor.setCursorScreenPosition([0, 4]);
- await component.getNextUpdatePromise();
- verifyCursorPosition(component, element.querySelector('.cursor'), 0, 4);
- });
- it('positions cursors and placeholder text correctly when the lines container has a margin and/or is padded', async () => {
- const { component, element, editor } = buildComponent({
- placeholderText: 'testing'
- });
- component.refs.lineTiles.style.marginLeft = '10px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- editor.setCursorBufferPosition([0, 3]);
- await component.getNextUpdatePromise();
- verifyCursorPosition(component, element.querySelector('.cursor'), 0, 3);
- editor.setCursorScreenPosition([1, 0]);
- await component.getNextUpdatePromise();
- verifyCursorPosition(component, element.querySelector('.cursor'), 1, 0);
- component.refs.lineTiles.style.paddingTop = '5px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- verifyCursorPosition(component, element.querySelector('.cursor'), 1, 0);
- editor.setCursorScreenPosition([2, 2]);
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- verifyCursorPosition(component, element.querySelector('.cursor'), 2, 2);
- editor.setText('');
- await component.getNextUpdatePromise();
- const placeholderTextLeft = element
- .querySelector('.placeholder-text')
- .getBoundingClientRect().left;
- const linesLeft = component.refs.lineTiles.getBoundingClientRect().left;
- expect(placeholderTextLeft).toBe(linesLeft);
- });
- it('places the hidden input element at the location of the last cursor if it is visible', async () => {
- const { component, editor } = buildComponent({
- height: 60,
- width: 120,
- rowsPerTile: 2
- });
- const { hiddenInput } = component.refs.cursorsAndInput.refs;
- setScrollTop(component, 100);
- await setScrollLeft(component, 40);
- expect(component.getRenderedStartRow()).toBe(4);
- expect(component.getRenderedEndRow()).toBe(10);
- // When out of view, the hidden input is positioned at 0, 0
- expect(editor.getCursorScreenPosition()).toEqual([0, 0]);
- expect(hiddenInput.offsetTop).toBe(0);
- expect(hiddenInput.offsetLeft).toBe(0);
- // Otherwise it is positioned at the last cursor position
- editor.addCursorAtScreenPosition([7, 4]);
- await component.getNextUpdatePromise();
- expect(hiddenInput.getBoundingClientRect().top).toBe(
- clientTopForLine(component, 7)
- );
- expect(Math.round(hiddenInput.getBoundingClientRect().left)).toBeNear(
- clientLeftForCharacter(component, 7, 4)
- );
- });
- it('soft wraps lines based on the content width when soft wrap is enabled', async () => {
- let baseCharacterWidth, gutterContainerWidth;
- {
- const { component, editor } = buildComponent();
- baseCharacterWidth = component.getBaseCharacterWidth();
- gutterContainerWidth = component.getGutterContainerWidth();
- editor.destroy();
- }
- const { component, element, editor } = buildComponent({
- width: gutterContainerWidth + baseCharacterWidth * 55,
- attach: false
- });
- editor.setSoftWrapped(true);
- jasmine.attachToDOM(element);
- expect(getEditorWidthInBaseCharacters(component)).toBe(55);
- expect(lineNodeForScreenRow(component, 3).textContent).toBe(
- ' var pivot = items.shift(), current, left = [], '
- );
- expect(lineNodeForScreenRow(component, 4).textContent).toBe(
- ' right = [];'
- );
- const { scrollContainer } = component.refs;
- expect(scrollContainer.clientWidth).toBe(scrollContainer.scrollWidth);
- });
- it('correctly forces the display layer to index visible rows when resizing (regression)', async () => {
- const text = 'a'.repeat(30) + '\n' + 'b'.repeat(1000);
- const { component, element, editor } = buildComponent({
- height: 300,
- width: 800,
- attach: false,
- text
- });
- editor.setSoftWrapped(true);
- jasmine.attachToDOM(element);
- element.style.width = 200 + 'px';
- await component.getNextUpdatePromise();
- expect(queryOnScreenLineElements(element).length).toBe(24);
- });
- it('decorates the line numbers of folded lines', async () => {
- const { component, editor } = buildComponent();
- editor.foldBufferRow(1);
- await component.getNextUpdatePromise();
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('folded')
- ).toBe(true);
- });
- it('makes lines at least as wide as the scrollContainer', async () => {
- const { component, element, editor } = buildComponent();
- const { scrollContainer } = component.refs;
- editor.setText('a');
- await component.getNextUpdatePromise();
- expect(element.querySelector('.line').offsetWidth).toBe(
- scrollContainer.offsetWidth - verticalScrollbarWidth
- );
- });
- it('resizes based on the content when the autoHeight and/or autoWidth options are true', async () => {
- const { component, element, editor } = buildComponent({
- autoHeight: true,
- autoWidth: true
- });
- const editorPadding = 3;
- element.style.padding = editorPadding + 'px';
- const initialWidth = element.offsetWidth;
- const initialHeight = element.offsetHeight;
- expect(initialWidth).toBe(
- component.getGutterContainerWidth() +
- component.getContentWidth() +
- verticalScrollbarWidth +
- 2 * editorPadding
- );
- expect(initialHeight).toBeNear(
- component.getContentHeight() +
- horizontalScrollbarHeight +
- 2 * editorPadding
- );
- // When autoWidth is enabled, width adjusts to content
- editor.setCursorScreenPosition([6, Infinity]);
- editor.insertText('x'.repeat(50));
- await component.getNextUpdatePromise();
- expect(element.offsetWidth).toBe(
- component.getGutterContainerWidth() +
- component.getContentWidth() +
- verticalScrollbarWidth +
- 2 * editorPadding
- );
- expect(element.offsetWidth).toBeGreaterThan(initialWidth);
- // When autoHeight is enabled, height adjusts to content
- editor.insertText('\n'.repeat(5));
- await component.getNextUpdatePromise();
- expect(element.offsetHeight).toBeNear(
- component.getContentHeight() +
- horizontalScrollbarHeight +
- 2 * editorPadding
- );
- expect(element.offsetHeight).toBeGreaterThan(initialHeight);
- });
- it('does not render the line number gutter at all if the isLineNumberGutterVisible parameter is false', () => {
- const { element } = buildComponent({
- lineNumberGutterVisible: false
- });
- expect(element.querySelector('.line-number')).toBe(null);
- });
- it('does not render the line numbers but still renders the line number gutter if showLineNumbers is false', async () => {
- function checkScrollContainerLeft(component) {
- const { scrollContainer, gutterContainer } = component.refs;
- expect(scrollContainer.getBoundingClientRect().left).toBeNear(
- Math.round(gutterContainer.element.getBoundingClientRect().right)
- );
- }
- const { component, element, editor } = buildComponent({
- showLineNumbers: false
- });
- expect(
- Array.from(element.querySelectorAll('.line-number')).every(
- e => e.textContent === ''
- )
- ).toBe(true);
- checkScrollContainerLeft(component);
- await editor.update({ showLineNumbers: true });
- expect(
- Array.from(element.querySelectorAll('.line-number')).map(
- e => e.textContent
- )
- ).toEqual([
- '00',
- '1',
- '2',
- '3',
- '4',
- '5',
- '6',
- '7',
- '8',
- '9',
- '10',
- '11',
- '12',
- '13'
- ]);
- checkScrollContainerLeft(component);
- await editor.update({ showLineNumbers: false });
- expect(
- Array.from(element.querySelectorAll('.line-number')).every(
- e => e.textContent === ''
- )
- ).toBe(true);
- checkScrollContainerLeft(component);
- });
- it('supports the placeholderText parameter', () => {
- const placeholderText = 'Placeholder Test';
- const { element } = buildComponent({ placeholderText, text: '' });
- expect(element.textContent).toContain(placeholderText);
- });
- it('adds the data-grammar attribute and updates it when the grammar changes', async () => {
- await atom.packages.activatePackage('language-javascript');
- const { editor, element, component } = buildComponent();
- expect(element.dataset.grammar).toBe('text plain null-grammar');
- atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.js');
- await component.getNextUpdatePromise();
- expect(element.dataset.grammar).toBe('source js');
- });
- it('adds the data-encoding attribute and updates it when the encoding changes', async () => {
- const { editor, element, component } = buildComponent();
- expect(element.dataset.encoding).toBe('utf8');
- editor.setEncoding('ascii');
- await component.getNextUpdatePromise();
- expect(element.dataset.encoding).toBe('ascii');
- });
- it('adds the has-selection class when the editor has a non-empty selection', async () => {
- const { editor, element, component } = buildComponent();
- expect(element.classList.contains('has-selection')).toBe(false);
- editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 10]]]);
- await component.getNextUpdatePromise();
- expect(element.classList.contains('has-selection')).toBe(true);
- editor.setSelectedBufferRanges([[[0, 0], [0, 0]], [[1, 0], [1, 0]]]);
- await component.getNextUpdatePromise();
- expect(element.classList.contains('has-selection')).toBe(false);
- });
- it('assigns buffer-row and screen-row to each line number as data fields', async () => {
- const { editor, element, component } = buildComponent();
- editor.setSoftWrapped(true);
- await component.getNextUpdatePromise();
- await setEditorWidthInCharacters(component, 40);
- {
- const bufferRows = queryOnScreenLineNumberElements(element).map(
- e => e.dataset.bufferRow
- );
- const screenRows = queryOnScreenLineNumberElements(element).map(
- e => e.dataset.screenRow
- );
- expect(bufferRows).toEqual([
- '0',
- '1',
- '2',
- '2',
- '3',
- '3',
- '4',
- '5',
- '6',
- '6',
- '6',
- '7',
- '8',
- '8',
- '8',
- '9',
- '10',
- '11',
- '11',
- '12'
- ]);
- expect(screenRows).toEqual([
- '0',
- '1',
- '2',
- '3',
- '4',
- '5',
- '6',
- '7',
- '8',
- '9',
- '10',
- '11',
- '12',
- '13',
- '14',
- '15',
- '16',
- '17',
- '18',
- '19'
- ]);
- }
- editor.getBuffer().insert([2, 0], '\n');
- await component.getNextUpdatePromise();
- {
- const bufferRows = queryOnScreenLineNumberElements(element).map(
- e => e.dataset.bufferRow
- );
- const screenRows = queryOnScreenLineNumberElements(element).map(
- e => e.dataset.screenRow
- );
- expect(bufferRows).toEqual([
- '0',
- '1',
- '2',
- '3',
- '3',
- '4',
- '4',
- '5',
- '6',
- '7',
- '7',
- '7',
- '8',
- '9',
- '9',
- '9',
- '10',
- '11',
- '12',
- '12',
- '13'
- ]);
- expect(screenRows).toEqual([
- '0',
- '1',
- '2',
- '3',
- '4',
- '5',
- '6',
- '7',
- '8',
- '9',
- '10',
- '11',
- '12',
- '13',
- '14',
- '15',
- '16',
- '17',
- '18',
- '19',
- '20'
- ]);
- }
- });
- it('does not blow away class names added to the element by packages when changing the class name', async () => {
- assertDocumentFocused();
- const { component, element } = buildComponent();
- element.classList.add('a', 'b');
- expect(element.className).toBe('editor a b');
- element.focus();
- await component.getNextUpdatePromise();
- expect(element.className).toBe('editor a b is-focused');
- document.body.focus();
- await component.getNextUpdatePromise();
- expect(element.className).toBe('editor a b');
- });
- it('does not blow away class names managed by the component when packages change the element class name', async () => {
- assertDocumentFocused();
- const { component, element } = buildComponent({ mini: true });
- element.classList.add('a', 'b');
- element.focus();
- await component.getNextUpdatePromise();
- expect(element.className).toBe('editor mini a b is-focused');
- element.className = 'a c d';
- await component.getNextUpdatePromise();
- expect(element.className).toBe('a c d editor is-focused mini');
- });
- it('ignores resize events when the editor is hidden', async () => {
- const { component, element } = buildComponent({
- autoHeight: false
- });
- element.style.height = 5 * component.getLineHeight() + 'px';
- await component.getNextUpdatePromise();
- const originalClientContainerHeight = component.getClientContainerHeight();
- const originalGutterContainerWidth = component.getGutterContainerWidth();
- const originalLineNumberGutterWidth = component.getLineNumberGutterWidth();
- expect(originalClientContainerHeight).toBeGreaterThan(0);
- expect(originalGutterContainerWidth).toBeGreaterThan(0);
- expect(originalLineNumberGutterWidth).toBeGreaterThan(0);
- element.style.display = 'none';
- // In production, resize events are triggered before the intersection
- // observer detects the editor's visibility has changed. In tests, we are
- // unable to reproduce this scenario and so we simulate them.
- expect(component.visible).toBe(true);
- component.didResize();
- component.didResizeGutterContainer();
- expect(component.getClientContainerHeight()).toBe(
- originalClientContainerHeight
- );
- expect(component.getGutterContainerWidth()).toBe(
- originalGutterContainerWidth
- );
- expect(component.getLineNumberGutterWidth()).toBe(
- originalLineNumberGutterWidth
- );
- // Ensure measurements stay the same after receiving the intersection
- // observer events.
- await conditionPromise(() => !component.visible);
- expect(component.getClientContainerHeight()).toBe(
- originalClientContainerHeight
- );
- expect(component.getGutterContainerWidth()).toBe(
- originalGutterContainerWidth
- );
- expect(component.getLineNumberGutterWidth()).toBe(
- originalLineNumberGutterWidth
- );
- });
- describe('randomized tests', () => {
- let originalTimeout;
- beforeEach(() => {
- originalTimeout = jasmine.getEnv().defaultTimeoutInterval;
- jasmine.getEnv().defaultTimeoutInterval = 60 * 1000;
- });
- afterEach(() => {
- jasmine.getEnv().defaultTimeoutInterval = originalTimeout;
- });
- it('renders the visible rows correctly after randomly mutating the editor', async () => {
- const initialSeed = Date.now();
- for (var i = 0; i < 20; i++) {
- let seed = initialSeed + i;
- // seed = 1520247533732
- const failureMessage = 'Randomized test failed with seed: ' + seed;
- const random = Random(seed);
- const rowsPerTile = random.intBetween(1, 6);
- const { component, element, editor } = buildComponent({
- rowsPerTile,
- autoHeight: false
- });
- editor.setSoftWrapped(Boolean(random(2)));
- await setEditorWidthInCharacters(component, random(20));
- await setEditorHeightInLines(component, random(10));
- element.style.fontSize = random(20) + 'px';
- element.style.lineHeight = random.floatBetween(0.1, 2.0);
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- element.focus();
- for (var j = 0; j < 5; j++) {
- const k = random(100);
- const range = getRandomBufferRange(random, editor.buffer);
- if (k < 10) {
- editor.setSoftWrapped(!editor.isSoftWrapped());
- } else if (k < 15) {
- if (random(2)) setEditorWidthInCharacters(component, random(20));
- if (random(2)) setEditorHeightInLines(component, random(10));
- } else if (k < 40) {
- editor.setSelectedBufferRange(range);
- editor.backspace();
- } else if (k < 80) {
- const linesToInsert = buildRandomLines(random, 5);
- editor.setCursorBufferPosition(range.start);
- editor.insertText(linesToInsert);
- } else if (k < 90) {
- if (random(2)) {
- editor.foldBufferRange(range);
- } else {
- editor.destroyFoldsIntersectingBufferRange(range);
- }
- } else if (k < 95) {
- editor.setSelectedBufferRange(range);
- } else {
- if (random(2)) {
- component.setScrollTop(random(component.getScrollHeight()));
- }
- if (random(2)) {
- component.setScrollLeft(random(component.getScrollWidth()));
- }
- }
- component.scheduleUpdate();
- await component.getNextUpdatePromise();
- const renderedLines = queryOnScreenLineElements(element).sort(
- (a, b) => a.dataset.screenRow - b.dataset.screenRow
- );
- const renderedLineNumbers = queryOnScreenLineNumberElements(
- element
- ).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow);
- const renderedStartRow = component.getRenderedStartRow();
- const expectedLines = editor.displayLayer.getScreenLines(
- renderedStartRow,
- component.getRenderedEndRow()
- );
- expect(renderedLines.length).toBe(
- expectedLines.length,
- failureMessage
- );
- expect(renderedLineNumbers.length).toBe(
- expectedLines.length,
- failureMessage
- );
- for (let k = 0; k < renderedLines.length; k++) {
- const expectedLine = expectedLines[k];
- const expectedText = expectedLine.lineText || ' ';
- const renderedLine = renderedLines[k];
- const renderedLineNumber = renderedLineNumbers[k];
- let renderedText = renderedLine.textContent;
- // We append zero width NBSPs after folds at the end of the
- // line in order to support measurement.
- if (expectedText.endsWith(editor.displayLayer.foldCharacter)) {
- renderedText = renderedText.substring(
- 0,
- renderedText.length - 1
- );
- }
- expect(renderedText).toBe(expectedText, failureMessage);
- expect(parseInt(renderedLine.dataset.screenRow)).toBe(
- renderedStartRow + k,
- failureMessage
- );
- expect(parseInt(renderedLineNumber.dataset.screenRow)).toBe(
- renderedStartRow + k,
- failureMessage
- );
- }
- }
- element.remove();
- editor.destroy();
- }
- });
- });
- });
- describe('mini editors', () => {
- it('adds the mini attribute and class even when the element is not attached', () => {
- {
- const { element } = buildComponent({ mini: true });
- expect(element.hasAttribute('mini')).toBe(true);
- expect(element.classList.contains('mini')).toBe(true);
- }
- {
- const { element } = buildComponent({
- mini: true,
- attach: false
- });
- expect(element.hasAttribute('mini')).toBe(true);
- expect(element.classList.contains('mini')).toBe(true);
- }
- });
- it('does not render the gutter container', () => {
- const { component, element } = buildComponent({ mini: true });
- expect(component.refs.gutterContainer).toBeUndefined();
- expect(element.querySelector('gutter-container')).toBeNull();
- });
- it('does not render line decorations for the cursor line', async () => {
- const { component, element, editor } = buildComponent({ mini: true });
- expect(
- element.querySelector('.line').classList.contains('cursor-line')
- ).toBe(false);
- editor.update({ mini: false });
- await component.getNextUpdatePromise();
- expect(
- element.querySelector('.line').classList.contains('cursor-line')
- ).toBe(true);
- editor.update({ mini: true });
- await component.getNextUpdatePromise();
- expect(
- element.querySelector('.line').classList.contains('cursor-line')
- ).toBe(false);
- });
- it('does not render scrollbars', async () => {
- const { component, editor } = buildComponent({
- mini: true,
- autoHeight: false
- });
- await setEditorWidthInCharacters(component, 10);
- editor.setText('x'.repeat(20) + 'y'.repeat(20));
- await component.getNextUpdatePromise();
- expect(component.canScrollVertically()).toBe(false);
- expect(component.canScrollHorizontally()).toBe(false);
- expect(component.refs.horizontalScrollbar).toBeUndefined();
- expect(component.refs.verticalScrollbar).toBeUndefined();
- });
- });
- describe('focus', () => {
- beforeEach(() => {
- assertDocumentFocused();
- });
- it('focuses the hidden input element and adds the is-focused class when focused', async () => {
- const { component, element } = buildComponent();
- const { hiddenInput } = component.refs.cursorsAndInput.refs;
- expect(document.activeElement).not.toBe(hiddenInput);
- element.focus();
- expect(document.activeElement).toBe(hiddenInput);
- await component.getNextUpdatePromise();
- expect(element.classList.contains('is-focused')).toBe(true);
- element.focus(); // focusing back to the element does not blur
- expect(document.activeElement).toBe(hiddenInput);
- expect(element.classList.contains('is-focused')).toBe(true);
- document.body.focus();
- expect(document.activeElement).not.toBe(hiddenInput);
- await component.getNextUpdatePromise();
- expect(element.classList.contains('is-focused')).toBe(false);
- });
- it('updates the component when the hidden input is focused directly', async () => {
- const { component, element } = buildComponent();
- const { hiddenInput } = component.refs.cursorsAndInput.refs;
- expect(element.classList.contains('is-focused')).toBe(false);
- expect(document.activeElement).not.toBe(hiddenInput);
- hiddenInput.focus();
- await component.getNextUpdatePromise();
- expect(element.classList.contains('is-focused')).toBe(true);
- });
- it('gracefully handles a focus event that occurs prior to the attachedCallback of the element', () => {
- const { component, element } = buildComponent({ attach: false });
- const parent = document.createElement(
- 'text-editor-component-test-element'
- );
- parent.appendChild(element);
- parent.didAttach = () => element.focus();
- jasmine.attachToDOM(parent);
- expect(document.activeElement).toBe(
- component.refs.cursorsAndInput.refs.hiddenInput
- );
- });
- it('gracefully handles a focus event that occurs prior to detecting the element has become visible', async () => {
- const { component, element } = buildComponent({ attach: false });
- element.style.display = 'none';
- jasmine.attachToDOM(element);
- element.style.display = 'block';
- element.focus();
- await component.getNextUpdatePromise();
- expect(document.activeElement).toBe(
- component.refs.cursorsAndInput.refs.hiddenInput
- );
- });
- it('emits blur events only when focus shifts to something other than the editor itself or its hidden input', () => {
- const { element } = buildComponent();
- let blurEventCount = 0;
- element.addEventListener('blur', () => blurEventCount++);
- element.focus();
- expect(blurEventCount).toBe(0);
- element.focus();
- expect(blurEventCount).toBe(0);
- document.body.focus();
- expect(blurEventCount).toBe(1);
- });
- });
- describe('autoscroll', () => {
- it('automatically scrolls vertically when the requested range is within the vertical scroll margin of the top or bottom', async () => {
- const { component, editor } = buildComponent({
- height: 120 + horizontalScrollbarHeight
- });
- expect(component.getLastVisibleRow()).toBe(7);
- editor.scrollToScreenRange([[4, 0], [6, 0]]);
- await component.getNextUpdatePromise();
- expect(component.getScrollBottom()).toBeNear(
- (6 + 1 + editor.verticalScrollMargin) * component.getLineHeight()
- );
- editor.scrollToScreenPosition([8, 0]);
- await component.getNextUpdatePromise();
- expect(component.getScrollBottom()).toBeNear(
- (8 + 1 + editor.verticalScrollMargin) *
- component.measurements.lineHeight
- );
- editor.scrollToScreenPosition([3, 0]);
- await component.getNextUpdatePromise();
- expect(component.getScrollTop()).toBeNear(
- (3 - editor.verticalScrollMargin) * component.measurements.lineHeight
- );
- editor.scrollToScreenPosition([2, 0]);
- await component.getNextUpdatePromise();
- expect(component.getScrollTop()).toBe(0);
- });
- it('does not vertically autoscroll by more than half of the visible lines if the editor is shorter than twice the scroll margin', async () => {
- const { component, element, editor } = buildComponent({
- autoHeight: false
- });
- element.style.height =
- 5.5 * component.measurements.lineHeight +
- horizontalScrollbarHeight +
- 'px';
- await component.getNextUpdatePromise();
- expect(component.getLastVisibleRow()).toBe(5);
- const scrollMarginInLines = 2;
- editor.scrollToScreenPosition([6, 0]);
- await component.getNextUpdatePromise();
- expect(component.getScrollBottom()).toBeNear(
- (6 + 1 + scrollMarginInLines) * component.measurements.lineHeight
- );
- editor.scrollToScreenPosition([6, 4]);
- await component.getNextUpdatePromise();
- expect(component.getScrollBottom()).toBeNear(
- (6 + 1 + scrollMarginInLines) * component.measurements.lineHeight
- );
- editor.scrollToScreenRange([[4, 4], [6, 4]]);
- await component.getNextUpdatePromise();
- expect(component.getScrollTop()).toBeNear(
- (4 - scrollMarginInLines) * component.measurements.lineHeight
- );
- editor.scrollToScreenRange([[4, 4], [6, 4]], { reversed: false });
- await component.getNextUpdatePromise();
- expect(component.getScrollBottom()).toBeNear(
- (6 + 1 + scrollMarginInLines) * component.measurements.lineHeight
- );
- });
- it('autoscrolls the given range to the center of the screen if the `center` option is true', async () => {
- const { component, editor } = buildComponent({ height: 50 });
- expect(component.getLastVisibleRow()).toBe(2);
- editor.scrollToScreenRange([[4, 0], [6, 0]], { center: true });
- await component.getNextUpdatePromise();
- const actualScrollCenter =
- (component.getScrollTop() + component.getScrollBottom()) / 2;
- const expectedScrollCenter = ((4 + 7) / 2) * component.getLineHeight();
- expect(actualScrollCenter).toBeCloseTo(expectedScrollCenter, 0);
- });
- it('automatically scrolls horizontally when the requested range is within the horizontal scroll margin of the right edge of the gutter or right edge of the scroll container', async () => {
- const { component, element, editor } = buildComponent();
- element.style.width =
- component.getGutterContainerWidth() +
- 3 *
- editor.horizontalScrollMargin *
- component.measurements.baseCharacterWidth +
- 'px';
- await component.getNextUpdatePromise();
- editor.scrollToScreenRange([[1, 12], [2, 28]]);
- await component.getNextUpdatePromise();
- let expectedScrollLeft =
- clientLeftForCharacter(component, 1, 12) -
- lineNodeForScreenRow(component, 1).getBoundingClientRect().left -
- editor.horizontalScrollMargin *
- component.measurements.baseCharacterWidth;
- expect(component.getScrollLeft()).toBeNear(expectedScrollLeft);
- editor.scrollToScreenRange([[1, 12], [2, 28]], { reversed: false });
- await component.getNextUpdatePromise();
- expectedScrollLeft =
- component.getGutterContainerWidth() +
- clientLeftForCharacter(component, 2, 28) -
- lineNodeForScreenRow(component, 2).getBoundingClientRect().left +
- editor.horizontalScrollMargin *
- component.measurements.baseCharacterWidth -
- component.getScrollContainerClientWidth();
- expect(component.getScrollLeft()).toBeNear(expectedScrollLeft);
- });
- it('does not horizontally autoscroll by more than half of the visible "base-width" characters if the editor is narrower than twice the scroll margin', async () => {
- const { component, editor } = buildComponent({ autoHeight: false });
- await setEditorWidthInCharacters(
- component,
- 1.5 * editor.horizontalScrollMargin
- );
- const editorWidthInChars =
- component.getScrollContainerClientWidth() /
- component.getBaseCharacterWidth();
- expect(Math.round(editorWidthInChars)).toBe(9);
- editor.scrollToScreenRange([[6, 10], [6, 15]]);
- await component.getNextUpdatePromise();
- let expectedScrollLeft = Math.floor(
- clientLeftForCharacter(component, 6, 10) -
- lineNodeForScreenRow(component, 1).getBoundingClientRect().left -
- Math.floor((editorWidthInChars - 1) / 2) *
- component.getBaseCharacterWidth()
- );
- expect(component.getScrollLeft()).toBeNear(expectedScrollLeft);
- });
- it('correctly autoscrolls after inserting a line that exceeds the current content width', async () => {
- const { component, element, editor } = buildComponent();
- element.style.width =
- component.getGutterContainerWidth() +
- component.getContentWidth() +
- 'px';
- await component.getNextUpdatePromise();
- editor.setCursorScreenPosition([0, Infinity]);
- editor.insertText('x'.repeat(100));
- await component.getNextUpdatePromise();
- expect(component.getScrollLeft()).toBeNear(
- component.getScrollWidth() - component.getScrollContainerClientWidth()
- );
- });
- it('does not try to measure lines that do not exist when the animation frame is delivered', async () => {
- const { component, editor } = buildComponent({
- autoHeight: false,
- height: 30,
- rowsPerTile: 2
- });
- editor.scrollToBufferPosition([11, 5]);
- editor.getBuffer().deleteRows(11, 12);
- await component.getNextUpdatePromise();
- expect(component.getScrollBottom()).toBeNear(
- (10 + 1) * component.measurements.lineHeight
- );
- });
- it('accounts for the presence of horizontal scrollbars that appear during the same frame as the autoscroll', async () => {
- const { component, element, editor } = buildComponent({
- autoHeight: false
- });
- element.style.height = component.getContentHeight() / 2 + 'px';
- element.style.width = component.getScrollWidth() + 'px';
- await component.getNextUpdatePromise();
- editor.setCursorScreenPosition([10, Infinity]);
- editor.insertText('\n\n' + 'x'.repeat(100));
- await component.getNextUpdatePromise();
- expect(component.getScrollTop()).toBeNear(
- component.getScrollHeight() - component.getScrollContainerClientHeight()
- );
- expect(component.getScrollLeft()).toBeNear(
- component.getScrollWidth() - component.getScrollContainerClientWidth()
- );
- // Scrolling to the top should not throw an error. This failed
- // previously due to horizontalPositionsToMeasure not being empty after
- // autoscrolling vertically to account for the horizontal scrollbar.
- spyOn(window, 'onerror');
- await setScrollTop(component, 0);
- expect(window.onerror).not.toHaveBeenCalled();
- });
- });
- describe('logical scroll positions', () => {
- it('allows the scrollTop to be changed and queried in terms of rows via setScrollTopRow and getScrollTopRow', () => {
- const { component, element } = buildComponent({
- attach: false,
- height: 80
- });
- // Caches the scrollTopRow if we don't have measurements
- component.setScrollTopRow(6);
- expect(component.getScrollTopRow()).toBe(6);
- // Assigns the scrollTop based on the logical position when attached
- jasmine.attachToDOM(element);
- const expectedScrollTop = Math.round(6 * component.getLineHeight());
- expect(component.getScrollTopRow()).toBeNear(6);
- expect(component.getScrollTop()).toBeNear(expectedScrollTop);
- expect(component.refs.content.style.transform).toBe(
- `translate(0px, -${expectedScrollTop}px)`
- );
- // Allows the scrollTopRow to be updated while attached
- component.setScrollTopRow(4);
- expect(component.getScrollTopRow()).toBeNear(4);
- expect(component.getScrollTop()).toBeNear(
- Math.round(4 * component.getLineHeight())
- );
- // Preserves the scrollTopRow when detached
- element.remove();
- expect(component.getScrollTopRow()).toBeNear(4);
- expect(component.getScrollTop()).toBeNear(
- Math.round(4 * component.getLineHeight())
- );
- component.setScrollTopRow(6);
- expect(component.getScrollTopRow()).toBeNear(6);
- expect(component.getScrollTop()).toBeNear(
- Math.round(6 * component.getLineHeight())
- );
- jasmine.attachToDOM(element);
- element.style.height = '60px';
- expect(component.getScrollTopRow()).toBeNear(6);
- expect(component.getScrollTop()).toBeNear(
- Math.round(6 * component.getLineHeight())
- );
- });
- it('allows the scrollLeft to be changed and queried in terms of base character columns via setScrollLeftColumn and getScrollLeftColumn', () => {
- const { component, element } = buildComponent({
- attach: false,
- width: 80
- });
- // Caches the scrollTopRow if we don't have measurements
- component.setScrollLeftColumn(2);
- expect(component.getScrollLeftColumn()).toBe(2);
- // Assigns the scrollTop based on the logical position when attached
- jasmine.attachToDOM(element);
- expect(component.getScrollLeft()).toBeCloseTo(
- 2 * component.getBaseCharacterWidth(),
- 0
- );
- // Allows the scrollTopRow to be updated while attached
- component.setScrollLeftColumn(4);
- expect(component.getScrollLeft()).toBeCloseTo(
- 4 * component.getBaseCharacterWidth(),
- 0
- );
- // Preserves the scrollTopRow when detached
- element.remove();
- expect(component.getScrollLeft()).toBeCloseTo(
- 4 * component.getBaseCharacterWidth(),
- 0
- );
- component.setScrollLeftColumn(6);
- expect(component.getScrollLeft()).toBeCloseTo(
- 6 * component.getBaseCharacterWidth(),
- 0
- );
- jasmine.attachToDOM(element);
- element.style.width = '60px';
- expect(component.getScrollLeft()).toBeCloseTo(
- 6 * component.getBaseCharacterWidth(),
- 0
- );
- });
- });
- describe('scrolling via the mouse wheel', () => {
- it('scrolls vertically or horizontally depending on whether deltaX or deltaY is larger', () => {
- const scrollSensitivity = 30;
- const { component } = buildComponent({
- height: 50,
- width: 50,
- scrollSensitivity
- });
- // stub in place for Event.preventDefault()
- const eventPreventDefaultStub = function() {};
- {
- const expectedScrollTop = 20 * (scrollSensitivity / 100);
- const expectedScrollLeft = component.getScrollLeft();
- component.didMouseWheel({
- wheelDeltaX: -5,
- wheelDeltaY: -20,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBeNear(expectedScrollTop);
- expect(component.getScrollLeft()).toBeNear(expectedScrollLeft);
- expect(component.refs.content.style.transform).toBe(
- `translate(${-expectedScrollLeft}px, ${-expectedScrollTop}px)`
- );
- }
- {
- const expectedScrollTop =
- component.getScrollTop() - 10 * (scrollSensitivity / 100);
- const expectedScrollLeft = component.getScrollLeft();
- component.didMouseWheel({
- wheelDeltaX: -5,
- wheelDeltaY: 10,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBeNear(expectedScrollTop);
- expect(component.getScrollLeft()).toBeNear(expectedScrollLeft);
- expect(component.refs.content.style.transform).toBe(
- `translate(${-expectedScrollLeft}px, ${-expectedScrollTop}px)`
- );
- }
- {
- const expectedScrollTop = component.getScrollTop();
- const expectedScrollLeft = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: -20,
- wheelDeltaY: 10,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBeNear(expectedScrollTop);
- expect(component.getScrollLeft()).toBeNear(expectedScrollLeft);
- expect(component.refs.content.style.transform).toBe(
- `translate(${-expectedScrollLeft}px, ${-expectedScrollTop}px)`
- );
- }
- {
- const expectedScrollTop = component.getScrollTop();
- const expectedScrollLeft =
- component.getScrollLeft() - 10 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: 10,
- wheelDeltaY: -8,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBeNear(expectedScrollTop);
- expect(component.getScrollLeft()).toBeNear(expectedScrollLeft);
- expect(component.refs.content.style.transform).toBe(
- `translate(${-expectedScrollLeft}px, ${-expectedScrollTop}px)`
- );
- }
- });
- it('inverts deltaX and deltaY when holding shift on Windows and Linux', async () => {
- const scrollSensitivity = 50;
- const { component } = buildComponent({
- height: 50,
- width: 50,
- scrollSensitivity
- });
- // stub in place for Event.preventDefault()
- const eventPreventDefaultStub = function() {};
- component.props.platform = 'linux';
- {
- const expectedScrollTop = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: 0,
- wheelDeltaY: -20,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBeNear(expectedScrollTop);
- expect(component.refs.content.style.transform).toBe(
- `translate(0px, -${expectedScrollTop}px)`
- );
- await setScrollTop(component, 0);
- }
- {
- const expectedScrollLeft = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: 0,
- wheelDeltaY: -20,
- shiftKey: true,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollLeft()).toBeNear(expectedScrollLeft);
- expect(component.refs.content.style.transform).toBe(
- `translate(-${expectedScrollLeft}px, 0px)`
- );
- await setScrollLeft(component, 0);
- }
- {
- const expectedScrollTop = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: -20,
- wheelDeltaY: 0,
- shiftKey: true,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBe(expectedScrollTop);
- expect(component.refs.content.style.transform).toBe(
- `translate(0px, -${expectedScrollTop}px)`
- );
- await setScrollTop(component, 0);
- }
- component.props.platform = 'win32';
- {
- const expectedScrollTop = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: 0,
- wheelDeltaY: -20,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBe(expectedScrollTop);
- expect(component.refs.content.style.transform).toBe(
- `translate(0px, -${expectedScrollTop}px)`
- );
- await setScrollTop(component, 0);
- }
- {
- const expectedScrollLeft = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: 0,
- wheelDeltaY: -20,
- shiftKey: true,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollLeft()).toBe(expectedScrollLeft);
- expect(component.refs.content.style.transform).toBe(
- `translate(-${expectedScrollLeft}px, 0px)`
- );
- await setScrollLeft(component, 0);
- }
- {
- const expectedScrollTop = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: -20,
- wheelDeltaY: 0,
- shiftKey: true,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBe(expectedScrollTop);
- expect(component.refs.content.style.transform).toBe(
- `translate(0px, -${expectedScrollTop}px)`
- );
- await setScrollTop(component, 0);
- }
- component.props.platform = 'darwin';
- {
- const expectedScrollTop = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: 0,
- wheelDeltaY: -20,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBe(expectedScrollTop);
- expect(component.refs.content.style.transform).toBe(
- `translate(0px, -${expectedScrollTop}px)`
- );
- await setScrollTop(component, 0);
- }
- {
- const expectedScrollTop = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: 0,
- wheelDeltaY: -20,
- shiftKey: true,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollTop()).toBe(expectedScrollTop);
- expect(component.refs.content.style.transform).toBe(
- `translate(0px, -${expectedScrollTop}px)`
- );
- await setScrollTop(component, 0);
- }
- {
- const expectedScrollLeft = 20 * (scrollSensitivity / 100);
- component.didMouseWheel({
- wheelDeltaX: -20,
- wheelDeltaY: 0,
- shiftKey: true,
- preventDefault: eventPreventDefaultStub
- });
- expect(component.getScrollLeft()).toBe(expectedScrollLeft);
- expect(component.refs.content.style.transform).toBe(
- `translate(-${expectedScrollLeft}px, 0px)`
- );
- await setScrollLeft(component, 0);
- }
- });
- });
- describe('scrolling via the API', () => {
- it('ignores scroll requests to NaN, null or undefined positions', async () => {
- const { component } = buildComponent({
- rowsPerTile: 2,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 3);
- await setEditorWidthInCharacters(component, 10);
- const initialScrollTop = Math.round(2 * component.getLineHeight());
- const initialScrollLeft = Math.round(
- 5 * component.getBaseCharacterWidth()
- );
- setScrollTop(component, initialScrollTop);
- setScrollLeft(component, initialScrollLeft);
- await component.getNextUpdatePromise();
- setScrollTop(component, NaN);
- setScrollLeft(component, NaN);
- await component.getNextUpdatePromise();
- expect(component.getScrollTop()).toBeNear(initialScrollTop);
- expect(component.getScrollLeft()).toBeNear(initialScrollLeft);
- setScrollTop(component, null);
- setScrollLeft(component, null);
- await component.getNextUpdatePromise();
- expect(component.getScrollTop()).toBeNear(initialScrollTop);
- expect(component.getScrollLeft()).toBeNear(initialScrollLeft);
- setScrollTop(component, undefined);
- setScrollLeft(component, undefined);
- await component.getNextUpdatePromise();
- expect(component.getScrollTop()).toBeNear(initialScrollTop);
- expect(component.getScrollLeft()).toBeNear(initialScrollLeft);
- });
- });
- describe('line and line number decorations', () => {
- it('adds decoration classes on screen lines spanned by decorated markers', async () => {
- const { component, editor } = buildComponent({
- softWrapped: true
- });
- await setEditorWidthInCharacters(component, 55);
- expect(lineNodeForScreenRow(component, 3).textContent).toBe(
- ' var pivot = items.shift(), current, left = [], '
- );
- expect(lineNodeForScreenRow(component, 4).textContent).toBe(
- ' right = [];'
- );
- const marker1 = editor.markScreenRange([[1, 10], [3, 10]]);
- const layer = editor.addMarkerLayer();
- layer.markScreenPosition([5, 0]);
- layer.markScreenPosition([8, 0]);
- const marker4 = layer.markScreenPosition([10, 0]);
- editor.decorateMarker(marker1, {
- type: ['line', 'line-number'],
- class: 'a'
- });
- const layerDecoration = editor.decorateMarkerLayer(layer, {
- type: ['line', 'line-number'],
- class: 'b'
- });
- layerDecoration.setPropertiesForMarker(marker4, {
- type: 'line',
- class: 'c'
- });
- await component.getNextUpdatePromise();
- expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 4).classList.contains('a')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 5).classList.contains('b')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 8).classList.contains('b')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 10).classList.contains('b')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 10).classList.contains('c')).toBe(
- true
- );
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('a')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 2).classList.contains('a')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 3).classList.contains('a')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 4).classList.contains('a')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 5).classList.contains('b')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 8).classList.contains('b')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 10).classList.contains('b')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 10).classList.contains('c')
- ).toBe(false);
- marker1.setScreenRange([[5, 0], [8, 0]]);
- await component.getNextUpdatePromise();
- expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 4).classList.contains('a')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 5).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 5).classList.contains('b')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 6).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 7).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 8).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 8).classList.contains('b')).toBe(
- true
- );
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('a')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 2).classList.contains('a')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 3).classList.contains('a')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 4).classList.contains('a')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 5).classList.contains('a')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 5).classList.contains('b')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 6).classList.contains('a')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 7).classList.contains('a')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 8).classList.contains('a')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 8).classList.contains('b')
- ).toBe(true);
- });
- it('honors the onlyEmpty and onlyNonEmpty decoration options', async () => {
- const { component, editor } = buildComponent();
- const marker = editor.markScreenPosition([1, 0]);
- editor.decorateMarker(marker, {
- type: ['line', 'line-number'],
- class: 'a',
- onlyEmpty: true
- });
- editor.decorateMarker(marker, {
- type: ['line', 'line-number'],
- class: 'b',
- onlyNonEmpty: true
- });
- editor.decorateMarker(marker, {
- type: ['line', 'line-number'],
- class: 'c'
- });
- await component.getNextUpdatePromise();
- expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 1).classList.contains('c')).toBe(
- true
- );
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('a')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('b')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('c')
- ).toBe(true);
- marker.setScreenRange([[1, 0], [2, 4]]);
- await component.getNextUpdatePromise();
- expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 1).classList.contains('c')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 2).classList.contains('b')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 2).classList.contains('c')).toBe(
- true
- );
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('a')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('b')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('c')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 2).classList.contains('b')
- ).toBe(true);
- expect(
- lineNumberNodeForScreenRow(component, 2).classList.contains('c')
- ).toBe(true);
- });
- it('honors the onlyHead option', async () => {
- const { component, editor } = buildComponent();
- const marker = editor.markScreenRange([[1, 4], [3, 4]]);
- editor.decorateMarker(marker, {
- type: ['line', 'line-number'],
- class: 'a',
- onlyHead: true
- });
- await component.getNextUpdatePromise();
- expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(
- true
- );
- expect(
- lineNumberNodeForScreenRow(component, 1).classList.contains('a')
- ).toBe(false);
- expect(
- lineNumberNodeForScreenRow(component, 3).classList.contains('a')
- ).toBe(true);
- });
- it('only decorates the last row of non-empty ranges that end at column 0 if omitEmptyLastRow is false', async () => {
- const { component, editor } = buildComponent();
- const marker = editor.markScreenRange([[1, 0], [3, 0]]);
- editor.decorateMarker(marker, {
- type: ['line', 'line-number'],
- class: 'a'
- });
- editor.decorateMarker(marker, {
- type: ['line', 'line-number'],
- class: 'b',
- omitEmptyLastRow: false
- });
- await component.getNextUpdatePromise();
- expect(lineNodeForScreenRow(component, 1).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 3).classList.contains('a')).toBe(
- false
- );
- expect(lineNodeForScreenRow(component, 1).classList.contains('b')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 2).classList.contains('b')).toBe(
- true
- );
- expect(lineNodeForScreenRow(component, 3).classList.contains('b')).toBe(
- true
- );
- });
- it('does not decorate invalidated markers', async () => {
- const { component, editor } = buildComponent();
- const marker = editor.markScreenRange([[1, 0], [3, 0]], {
- invalidate: 'touch'
- });
- editor.decorateMarker(marker, {
- type: ['line', 'line-number'],
- class: 'a'
- });
- await component.getNextUpdatePromise();
- expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(
- true
- );
- editor.getBuffer().insert([2, 0], 'x');
- expect(marker.isValid()).toBe(false);
- await component.getNextUpdatePromise();
- expect(lineNodeForScreenRow(component, 2).classList.contains('a')).toBe(
- false
- );
- });
- });
- describe('highlight decorations', () => {
- it('renders single-line highlights', async () => {
- const { component, element, editor } = buildComponent();
- const marker = editor.markScreenRange([[1, 2], [1, 10]]);
- editor.decorateMarker(marker, { type: 'highlight', class: 'a' });
- await component.getNextUpdatePromise();
- {
- const regions = element.querySelectorAll('.highlight.a .region.a');
- expect(regions.length).toBe(1);
- const regionRect = regions[0].getBoundingClientRect();
- expect(regionRect.top).toBe(
- lineNodeForScreenRow(component, 1).getBoundingClientRect().top
- );
- expect(Math.round(regionRect.left)).toBeNear(
- clientLeftForCharacter(component, 1, 2)
- );
- expect(Math.round(regionRect.right)).toBeNear(
- clientLeftForCharacter(component, 1, 10)
- );
- }
- marker.setScreenRange([[1, 4], [1, 8]]);
- await component.getNextUpdatePromise();
- {
- const regions = element.querySelectorAll('.highlight.a .region.a');
- expect(regions.length).toBe(1);
- const regionRect = regions[0].getBoundingClientRect();
- expect(regionRect.top).toBe(
- lineNodeForScreenRow(component, 1).getBoundingClientRect().top
- );
- expect(regionRect.bottom).toBe(
- lineNodeForScreenRow(component, 1).getBoundingClientRect().bottom
- );
- expect(Math.round(regionRect.left)).toBeNear(
- clientLeftForCharacter(component, 1, 4)
- );
- expect(Math.round(regionRect.right)).toBeNear(
- clientLeftForCharacter(component, 1, 8)
- );
- }
- });
- it('renders multi-line highlights', async () => {
- const { component, element, editor } = buildComponent({ rowsPerTile: 3 });
- const marker = editor.markScreenRange([[2, 4], [3, 4]]);
- editor.decorateMarker(marker, { type: 'highlight', class: 'a' });
- await component.getNextUpdatePromise();
- {
- expect(element.querySelectorAll('.highlight.a').length).toBe(1);
- const regions = element.querySelectorAll('.highlight.a .region.a');
- expect(regions.length).toBe(2);
- const region0Rect = regions[0].getBoundingClientRect();
- expect(region0Rect.top).toBe(
- lineNodeForScreenRow(component, 2).getBoundingClientRect().top
- );
- expect(region0Rect.bottom).toBe(
- lineNodeForScreenRow(component, 2).getBoundingClientRect().bottom
- );
- expect(Math.round(region0Rect.left)).toBeNear(
- clientLeftForCharacter(component, 2, 4)
- );
- expect(Math.round(region0Rect.right)).toBeNear(
- component.refs.content.getBoundingClientRect().right
- );
- const region1Rect = regions[1].getBoundingClientRect();
- expect(region1Rect.top).toBeNear(
- lineNodeForScreenRow(component, 3).getBoundingClientRect().top
- );
- expect(region1Rect.bottom).toBeNear(
- lineNodeForScreenRow(component, 3).getBoundingClientRect().bottom
- );
- expect(Math.round(region1Rect.left)).toBeNear(
- clientLeftForCharacter(component, 3, 0)
- );
- expect(Math.round(region1Rect.right)).toBeNear(
- clientLeftForCharacter(component, 3, 4)
- );
- }
- marker.setScreenRange([[2, 4], [5, 4]]);
- await component.getNextUpdatePromise();
- {
- expect(element.querySelectorAll('.highlight.a').length).toBe(1);
- const regions = element.querySelectorAll('.highlight.a .region.a');
- expect(regions.length).toBe(3);
- const region0Rect = regions[0].getBoundingClientRect();
- expect(region0Rect.top).toBeNear(
- lineNodeForScreenRow(component, 2).getBoundingClientRect().top
- );
- expect(region0Rect.bottom).toBeNear(
- lineNodeForScreenRow(component, 2).getBoundingClientRect().bottom
- );
- expect(Math.round(region0Rect.left)).toBeNear(
- clientLeftForCharacter(component, 2, 4)
- );
- expect(Math.round(region0Rect.right)).toBeNear(
- component.refs.content.getBoundingClientRect().right
- );
- const region1Rect = regions[1].getBoundingClientRect();
- expect(region1Rect.top).toBeNear(
- lineNodeForScreenRow(component, 3).getBoundingClientRect().top
- );
- expect(region1Rect.bottom).toBeNear(
- lineNodeForScreenRow(component, 5).getBoundingClientRect().top
- );
- expect(Math.round(region1Rect.left)).toBeNear(
- component.refs.content.getBoundingClientRect().left
- );
- expect(Math.round(region1Rect.right)).toBeNear(
- component.refs.content.getBoundingClientRect().right
- );
- const region2Rect = regions[2].getBoundingClientRect();
- expect(region2Rect.top).toBeNear(
- lineNodeForScreenRow(component, 5).getBoundingClientRect().top
- );
- expect(region2Rect.bottom).toBeNear(
- lineNodeForScreenRow(component, 6).getBoundingClientRect().top
- );
- expect(Math.round(region2Rect.left)).toBeNear(
- component.refs.content.getBoundingClientRect().left
- );
- expect(Math.round(region2Rect.right)).toBeNear(
- clientLeftForCharacter(component, 5, 4)
- );
- }
- });
- it('can flash highlight decorations', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 3,
- height: 200
- });
- const marker = editor.markScreenRange([[2, 4], [3, 4]]);
- const decoration = editor.decorateMarker(marker, {
- type: 'highlight',
- class: 'a'
- });
- decoration.flash('b', 10);
- // Flash on initial appearance of highlight
- await component.getNextUpdatePromise();
- const highlights = element.querySelectorAll('.highlight.a');
- expect(highlights.length).toBe(1);
- expect(highlights[0].classList.contains('b')).toBe(true);
- await conditionPromise(() => !highlights[0].classList.contains('b'));
- // Don't flash on next update if another flash wasn't requested
- await setScrollTop(component, 100);
- expect(highlights[0].classList.contains('b')).toBe(false);
- // Flashing the same class again before the first flash completes
- // removes the flash class and adds it back on the next frame to ensure
- // CSS transitions apply to the second flash.
- decoration.flash('e', 100);
- await component.getNextUpdatePromise();
- expect(highlights[0].classList.contains('e')).toBe(true);
- decoration.flash('e', 100);
- await component.getNextUpdatePromise();
- expect(highlights[0].classList.contains('e')).toBe(false);
- await conditionPromise(() => highlights[0].classList.contains('e'));
- await conditionPromise(() => !highlights[0].classList.contains('e'));
- });
- it("flashing a highlight decoration doesn't unflash other highlight decorations", async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 3,
- height: 200
- });
- const marker = editor.markScreenRange([[2, 4], [3, 4]]);
- const decoration = editor.decorateMarker(marker, {
- type: 'highlight',
- class: 'a'
- });
- // Flash one class
- decoration.flash('c', 1000);
- await component.getNextUpdatePromise();
- const highlights = element.querySelectorAll('.highlight.a');
- expect(highlights.length).toBe(1);
- expect(highlights[0].classList.contains('c')).toBe(true);
- // Flash another class while the previously-flashed class is still highlighted
- decoration.flash('d', 100);
- await component.getNextUpdatePromise();
- expect(highlights[0].classList.contains('c')).toBe(true);
- expect(highlights[0].classList.contains('d')).toBe(true);
- });
- it('supports layer decorations', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 12
- });
- const markerLayer = editor.addMarkerLayer();
- const marker1 = markerLayer.markScreenRange([[2, 4], [3, 4]]);
- const marker2 = markerLayer.markScreenRange([[5, 6], [7, 8]]);
- const decoration = editor.decorateMarkerLayer(markerLayer, {
- type: 'highlight',
- class: 'a'
- });
- await component.getNextUpdatePromise();
- const highlights = element.querySelectorAll('.highlight');
- expect(highlights[0].classList.contains('a')).toBe(true);
- expect(highlights[1].classList.contains('a')).toBe(true);
- decoration.setPropertiesForMarker(marker1, {
- type: 'highlight',
- class: 'b'
- });
- await component.getNextUpdatePromise();
- expect(highlights[0].classList.contains('b')).toBe(true);
- expect(highlights[1].classList.contains('a')).toBe(true);
- decoration.setPropertiesForMarker(marker1, null);
- decoration.setPropertiesForMarker(marker2, {
- type: 'highlight',
- class: 'c'
- });
- await component.getNextUpdatePromise();
- expect(highlights[0].classList.contains('a')).toBe(true);
- expect(highlights[1].classList.contains('c')).toBe(true);
- });
- it('clears highlights when recycling a tile that previously contained highlights and now does not', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 2,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 2);
- const marker = editor.markScreenRange([[1, 2], [1, 10]]);
- editor.decorateMarker(marker, { type: 'highlight', class: 'a' });
- await component.getNextUpdatePromise();
- expect(element.querySelectorAll('.highlight.a').length).toBe(1);
- await setScrollTop(component, component.getLineHeight() * 3);
- expect(element.querySelectorAll('.highlight.a').length).toBe(0);
- });
- it('does not move existing highlights when adding or removing other highlight decorations (regression)', async () => {
- const { component, element, editor } = buildComponent();
- const marker1 = editor.markScreenRange([[1, 6], [1, 10]]);
- editor.decorateMarker(marker1, { type: 'highlight', class: 'a' });
- await component.getNextUpdatePromise();
- const marker1Region = element.querySelector('.highlight.a');
- expect(
- Array.from(marker1Region.parentElement.children).indexOf(marker1Region)
- ).toBe(0);
- const marker2 = editor.markScreenRange([[1, 2], [1, 4]]);
- editor.decorateMarker(marker2, { type: 'highlight', class: 'b' });
- await component.getNextUpdatePromise();
- const marker2Region = element.querySelector('.highlight.b');
- expect(
- Array.from(marker1Region.parentElement.children).indexOf(marker1Region)
- ).toBe(0);
- expect(
- Array.from(marker2Region.parentElement.children).indexOf(marker2Region)
- ).toBe(1);
- marker2.destroy();
- await component.getNextUpdatePromise();
- expect(
- Array.from(marker1Region.parentElement.children).indexOf(marker1Region)
- ).toBe(0);
- });
- it('correctly positions highlights that end on rows preceding or following block decorations', async () => {
- const { editor, element, component } = buildComponent();
- const item1 = document.createElement('div');
- item1.style.height = '30px';
- item1.style.backgroundColor = 'blue';
- editor.decorateMarker(editor.markBufferPosition([4, 0]), {
- type: 'block',
- position: 'after',
- item: item1
- });
- const item2 = document.createElement('div');
- item2.style.height = '30px';
- item2.style.backgroundColor = 'yellow';
- editor.decorateMarker(editor.markBufferPosition([4, 0]), {
- type: 'block',
- position: 'before',
- item: item2
- });
- editor.decorateMarker(editor.markBufferRange([[3, 0], [4, Infinity]]), {
- type: 'highlight',
- class: 'highlight'
- });
- await component.getNextUpdatePromise();
- const regions = element.querySelectorAll('.highlight .region');
- expect(regions[0].offsetTop).toBeNear(3 * component.getLineHeight());
- expect(regions[0].offsetHeight).toBeNear(component.getLineHeight());
- expect(regions[1].offsetTop).toBeNear(4 * component.getLineHeight() + 30);
- });
- });
- describe('overlay decorations', () => {
- function attachFakeWindow(component) {
- const fakeWindow = document.createElement('div');
- fakeWindow.style.position = 'absolute';
- fakeWindow.style.padding = 20 + 'px';
- fakeWindow.style.backgroundColor = 'blue';
- fakeWindow.appendChild(component.element);
- jasmine.attachToDOM(fakeWindow);
- spyOn(component, 'getWindowInnerWidth').andCallFake(
- () => fakeWindow.getBoundingClientRect().width
- );
- spyOn(component, 'getWindowInnerHeight').andCallFake(
- () => fakeWindow.getBoundingClientRect().height
- );
- return fakeWindow;
- }
- it('renders overlay elements at the specified screen position unless it would overflow the window', async () => {
- const { component, editor } = buildComponent({
- width: 200,
- height: 100,
- attach: false
- });
- const fakeWindow = attachFakeWindow(component);
- await setScrollTop(component, 50);
- await setScrollLeft(component, 100);
- const marker = editor.markScreenPosition([4, 25]);
- const overlayElement = document.createElement('div');
- overlayElement.style.width = '50px';
- overlayElement.style.height = '50px';
- overlayElement.style.margin = '3px';
- overlayElement.style.backgroundColor = 'red';
- const decoration = editor.decorateMarker(marker, {
- type: 'overlay',
- item: overlayElement,
- class: 'a'
- });
- await component.getNextUpdatePromise();
- const overlayComponent = component.overlayComponents.values().next()
- .value;
- const overlayWrapper = overlayElement.parentElement;
- expect(overlayWrapper.classList.contains('a')).toBe(true);
- expect(overlayWrapper.getBoundingClientRect().top).toBeNear(
- clientTopForLine(component, 5)
- );
- expect(overlayWrapper.getBoundingClientRect().left).toBeNear(
- clientLeftForCharacter(component, 4, 25)
- );
- // Updates the horizontal position on scroll
- await setScrollLeft(component, 150);
- expect(overlayWrapper.getBoundingClientRect().left).toBeNear(
- clientLeftForCharacter(component, 4, 25)
- );
- // Shifts the overlay horizontally to ensure the overlay element does not
- // overflow the window
- await setScrollLeft(component, 30);
- expect(overlayElement.getBoundingClientRect().right).toBeNear(
- fakeWindow.getBoundingClientRect().right
- );
- await setScrollLeft(component, 280);
- expect(overlayElement.getBoundingClientRect().left).toBeNear(
- fakeWindow.getBoundingClientRect().left
- );
- // Updates the vertical position on scroll
- await setScrollTop(component, 60);
- expect(overlayWrapper.getBoundingClientRect().top).toBeNear(
- clientTopForLine(component, 5)
- );
- // Flips the overlay vertically to ensure the overlay element does not
- // overflow the bottom of the window
- setScrollLeft(component, 100);
- await setScrollTop(component, 0);
- expect(overlayWrapper.getBoundingClientRect().bottom).toBeNear(
- clientTopForLine(component, 4)
- );
- // Flips the overlay vertically on overlay resize if necessary
- await setScrollTop(component, 20);
- expect(overlayWrapper.getBoundingClientRect().top).toBeNear(
- clientTopForLine(component, 5)
- );
- overlayElement.style.height = 60 + 'px';
- await overlayComponent.getNextUpdatePromise();
- expect(overlayWrapper.getBoundingClientRect().bottom).toBeNear(
- clientTopForLine(component, 4)
- );
- // Does not flip the overlay vertically if it would overflow the top of the window
- overlayElement.style.height = 80 + 'px';
- await overlayComponent.getNextUpdatePromise();
- expect(overlayWrapper.getBoundingClientRect().top).toBeNear(
- clientTopForLine(component, 5)
- );
- // Can update overlay wrapper class
- decoration.setProperties({
- type: 'overlay',
- item: overlayElement,
- class: 'b'
- });
- await component.getNextUpdatePromise();
- expect(overlayWrapper.classList.contains('a')).toBe(false);
- expect(overlayWrapper.classList.contains('b')).toBe(true);
- decoration.setProperties({ type: 'overlay', item: overlayElement });
- await component.getNextUpdatePromise();
- expect(overlayWrapper.classList.contains('b')).toBe(false);
- });
- it('does not attempt to avoid overflowing the window if `avoidOverflow` is false on the decoration', async () => {
- const { component, editor } = buildComponent({
- width: 200,
- height: 100,
- attach: false
- });
- const fakeWindow = attachFakeWindow(component);
- const overlayElement = document.createElement('div');
- overlayElement.style.width = '50px';
- overlayElement.style.height = '50px';
- overlayElement.style.margin = '3px';
- overlayElement.style.backgroundColor = 'red';
- const marker = editor.markScreenPosition([4, 25]);
- editor.decorateMarker(marker, {
- type: 'overlay',
- item: overlayElement,
- avoidOverflow: false
- });
- await component.getNextUpdatePromise();
- await setScrollLeft(component, 30);
- expect(overlayElement.getBoundingClientRect().right).toBeGreaterThan(
- fakeWindow.getBoundingClientRect().right
- );
- await setScrollLeft(component, 280);
- expect(overlayElement.getBoundingClientRect().left).toBeLessThan(
- fakeWindow.getBoundingClientRect().left
- );
- });
- });
- describe('custom gutter decorations', () => {
- it('arranges custom gutters based on their priority', async () => {
- const { component, editor } = buildComponent();
- editor.addGutter({ name: 'e', priority: 2 });
- editor.addGutter({ name: 'a', priority: -2 });
- editor.addGutter({ name: 'd', priority: 1 });
- editor.addGutter({ name: 'b', priority: -1 });
- editor.addGutter({ name: 'c', priority: 0 });
- await component.getNextUpdatePromise();
- const gutters = component.refs.gutterContainer.element.querySelectorAll(
- '.gutter'
- );
- expect(
- Array.from(gutters).map(g => g.getAttribute('gutter-name'))
- ).toEqual(['a', 'b', 'c', 'line-number', 'd', 'e']);
- });
- it('adjusts the left edge of the scroll container based on changes to the gutter container width', async () => {
- const { component, editor } = buildComponent();
- const { scrollContainer, gutterContainer } = component.refs;
- function checkScrollContainerLeft() {
- expect(scrollContainer.getBoundingClientRect().left).toBeNear(
- Math.round(gutterContainer.element.getBoundingClientRect().right)
- );
- }
- checkScrollContainerLeft();
- const gutterA = editor.addGutter({ name: 'a' });
- await component.getNextUpdatePromise();
- checkScrollContainerLeft();
- const gutterB = editor.addGutter({ name: 'b' });
- await component.getNextUpdatePromise();
- checkScrollContainerLeft();
- gutterA.getElement().style.width = 100 + 'px';
- await component.getNextUpdatePromise();
- checkScrollContainerLeft();
- gutterA.hide();
- await component.getNextUpdatePromise();
- checkScrollContainerLeft();
- gutterA.show();
- await component.getNextUpdatePromise();
- checkScrollContainerLeft();
- gutterA.destroy();
- await component.getNextUpdatePromise();
- checkScrollContainerLeft();
- gutterB.destroy();
- await component.getNextUpdatePromise();
- checkScrollContainerLeft();
- });
- it('allows the element of custom gutters to be retrieved before being rendered in the editor component', async () => {
- const { component, element, editor } = buildComponent();
- const [lineNumberGutter] = editor.getGutters();
- const gutterA = editor.addGutter({ name: 'a', priority: -1 });
- const gutterB = editor.addGutter({ name: 'b', priority: 1 });
- const lineNumberGutterElement = lineNumberGutter.getElement();
- const gutterAElement = gutterA.getElement();
- const gutterBElement = gutterB.getElement();
- await component.getNextUpdatePromise();
- expect(element.contains(lineNumberGutterElement)).toBe(true);
- expect(element.contains(gutterAElement)).toBe(true);
- expect(element.contains(gutterBElement)).toBe(true);
- });
- it('can show and hide custom gutters', async () => {
- const { component, editor } = buildComponent();
- const gutterA = editor.addGutter({ name: 'a', priority: -1 });
- const gutterB = editor.addGutter({ name: 'b', priority: 1 });
- const gutterAElement = gutterA.getElement();
- const gutterBElement = gutterB.getElement();
- await component.getNextUpdatePromise();
- expect(gutterAElement.style.display).toBe('');
- expect(gutterBElement.style.display).toBe('');
- gutterA.hide();
- await component.getNextUpdatePromise();
- expect(gutterAElement.style.display).toBe('none');
- expect(gutterBElement.style.display).toBe('');
- gutterB.hide();
- await component.getNextUpdatePromise();
- expect(gutterAElement.style.display).toBe('none');
- expect(gutterBElement.style.display).toBe('none');
- gutterA.show();
- await component.getNextUpdatePromise();
- expect(gutterAElement.style.display).toBe('');
- expect(gutterBElement.style.display).toBe('none');
- });
- it('renders decorations in custom gutters', async () => {
- const { component, element, editor } = buildComponent();
- const gutterA = editor.addGutter({ name: 'a', priority: -1 });
- const gutterB = editor.addGutter({ name: 'b', priority: 1 });
- const marker1 = editor.markScreenRange([[2, 0], [4, 0]]);
- const marker2 = editor.markScreenRange([[6, 0], [7, 0]]);
- const marker3 = editor.markScreenRange([[9, 0], [12, 0]]);
- const decorationElement1 = document.createElement('div');
- const decorationElement2 = document.createElement('div');
- // Packages may adopt this class name for decorations to be styled the same as line numbers
- decorationElement2.className = 'line-number';
- const decoration1 = gutterA.decorateMarker(marker1, { class: 'a' });
- const decoration2 = gutterA.decorateMarker(marker2, {
- class: 'b',
- item: decorationElement1
- });
- const decoration3 = gutterB.decorateMarker(marker3, {
- item: decorationElement2
- });
- await component.getNextUpdatePromise();
- let [
- decorationNode1,
- decorationNode2
- ] = gutterA.getElement().firstChild.children;
- const [decorationNode3] = gutterB.getElement().firstChild.children;
- expect(decorationNode1.className).toBe('decoration a');
- expect(decorationNode1.getBoundingClientRect().top).toBeNear(
- clientTopForLine(component, 2)
- );
- expect(decorationNode1.getBoundingClientRect().bottom).toBeNear(
- clientTopForLine(component, 5)
- );
- expect(decorationNode1.firstChild).toBeNull();
- expect(decorationNode2.className).toBe('decoration b');
- expect(decorationNode2.getBoundingClientRect().top).toBeNear(
- clientTopForLine(component, 6)
- );
- expect(decorationNode2.getBoundingClientRect().bottom).toBeNear(
- clientTopForLine(component, 8)
- );
- expect(decorationNode2.firstChild).toBe(decorationElement1);
- expect(decorationElement1.offsetHeight).toBe(
- decorationNode2.offsetHeight
- );
- expect(decorationElement1.offsetWidth).toBe(decorationNode2.offsetWidth);
- expect(decorationNode3.className).toBe('decoration');
- expect(decorationNode3.getBoundingClientRect().top).toBeNear(
- clientTopForLine(component, 9)
- );
- expect(decorationNode3.getBoundingClientRect().bottom).toBeNear(
- clientTopForLine(component, 12) + component.getLineHeight()
- );
- expect(decorationNode3.firstChild).toBe(decorationElement2);
- expect(decorationElement2.offsetHeight).toBe(
- decorationNode3.offsetHeight
- );
- expect(decorationElement2.offsetWidth).toBe(decorationNode3.offsetWidth);
- // Inline styled height is updated when line height changes
- element.style.fontSize =
- parseInt(getComputedStyle(element).fontSize) + 10 + 'px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(decorationElement1.offsetHeight).toBe(
- decorationNode2.offsetHeight
- );
- expect(decorationElement2.offsetHeight).toBe(
- decorationNode3.offsetHeight
- );
- decoration1.setProperties({
- type: 'gutter',
- gutterName: 'a',
- class: 'c',
- item: decorationElement1
- });
- decoration2.setProperties({ type: 'gutter', gutterName: 'a' });
- decoration3.destroy();
- await component.getNextUpdatePromise();
- expect(decorationNode1.className).toBe('decoration c');
- expect(decorationNode1.firstChild).toBe(decorationElement1);
- expect(decorationElement1.offsetHeight).toBe(
- decorationNode1.offsetHeight
- );
- expect(decorationNode2.className).toBe('decoration');
- expect(decorationNode2.firstChild).toBeNull();
- expect(gutterB.getElement().firstChild.children.length).toBe(0);
- });
- it('renders custom line number gutters', async () => {
- const { component, editor } = buildComponent();
- const gutterA = editor.addGutter({
- name: 'a',
- priority: 1,
- type: 'line-number',
- class: 'a-number',
- labelFn: ({ bufferRow }) => `a - ${bufferRow}`
- });
- const gutterB = editor.addGutter({
- name: 'b',
- priority: 1,
- type: 'line-number',
- class: 'b-number',
- labelFn: ({ bufferRow }) => `b - ${bufferRow}`
- });
- editor.setText('0000\n0001\n0002\n0003\n0004\n');
- await component.getNextUpdatePromise();
- const gutterAElement = gutterA.getElement();
- const aNumbers = gutterAElement.querySelectorAll(
- 'div.line-number[data-buffer-row]'
- );
- const aLabels = Array.from(aNumbers, e => e.textContent);
- expect(aLabels).toEqual([
- 'a - 0',
- 'a - 1',
- 'a - 2',
- 'a - 3',
- 'a - 4',
- 'a - 5'
- ]);
- const gutterBElement = gutterB.getElement();
- const bNumbers = gutterBElement.querySelectorAll(
- 'div.line-number[data-buffer-row]'
- );
- const bLabels = Array.from(bNumbers, e => e.textContent);
- expect(bLabels).toEqual([
- 'b - 0',
- 'b - 1',
- 'b - 2',
- 'b - 3',
- 'b - 4',
- 'b - 5'
- ]);
- });
- it("updates the editor's soft wrap width when a custom gutter's measurement is available", () => {
- const { component, element, editor } = buildComponent({
- lineNumberGutterVisible: false,
- width: 400,
- softWrapped: true,
- attach: false
- });
- const gutter = editor.addGutter({ name: 'a', priority: 10 });
- gutter.getElement().style.width = '100px';
- jasmine.attachToDOM(element);
- expect(component.getGutterContainerWidth()).toBe(100);
- // Component client width - gutter container width - vertical scrollbar width
- const softWrapColumn = Math.floor(
- (400 - 100 - component.getVerticalScrollbarWidth()) /
- component.getBaseCharacterWidth()
- );
- expect(editor.getSoftWrapColumn()).toBe(softWrapColumn);
- });
- });
- describe('block decorations', () => {
- it('renders visible block decorations between the appropriate lines, refreshing and measuring them as needed', async () => {
- const editor = buildEditor({ autoHeight: false });
- const {
- item: item1,
- decoration: decoration1
- } = createBlockDecorationAtScreenRow(editor, 0, {
- height: 11,
- position: 'before'
- });
- const {
- item: item2,
- decoration: decoration2
- } = createBlockDecorationAtScreenRow(editor, 2, {
- height: 22,
- margin: 10,
- position: 'before'
- });
- // render an editor that already contains some block decorations
- const { component, element } = buildComponent({ editor, rowsPerTile: 3 });
- element.style.height =
- 4 * component.getLineHeight() + horizontalScrollbarHeight + 'px';
- await component.getNextUpdatePromise();
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(9);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item1) +
- getElementHeight(item2)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height:
- 3 * component.getLineHeight() +
- getElementHeight(item1) +
- getElementHeight(item2)
- },
- { tileStartRow: 3, height: 3 * component.getLineHeight() }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(item1.previousSibling).toBeNull();
- expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1));
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2));
- // add block decorations
- const {
- item: item3,
- decoration: decoration3
- } = createBlockDecorationAtScreenRow(editor, 4, {
- height: 33,
- position: 'before'
- });
- const { item: item4 } = createBlockDecorationAtScreenRow(editor, 7, {
- height: 44,
- position: 'before'
- });
- const { item: item5 } = createBlockDecorationAtScreenRow(editor, 7, {
- height: 50,
- marginBottom: 5,
- position: 'after'
- });
- const { item: item6 } = createBlockDecorationAtScreenRow(editor, 12, {
- height: 60,
- marginTop: 6,
- position: 'after'
- });
- await component.getNextUpdatePromise();
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(9);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item1) +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height:
- 3 * component.getLineHeight() +
- getElementHeight(item1) +
- getElementHeight(item2)
- },
- {
- tileStartRow: 3,
- height: 3 * component.getLineHeight() + getElementHeight(item3)
- }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(item1.previousSibling).toBeNull();
- expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1));
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2));
- expect(item3.previousSibling).toBe(lineNodeForScreenRow(component, 3));
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 4));
- expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(element.contains(item6)).toBe(false);
- // destroy decoration1
- decoration1.destroy();
- await component.getNextUpdatePromise();
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(9);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height: 3 * component.getLineHeight() + getElementHeight(item2)
- },
- {
- tileStartRow: 3,
- height: 3 * component.getLineHeight() + getElementHeight(item3)
- }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(element.contains(item1)).toBe(false);
- expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 1));
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 2));
- expect(item3.previousSibling).toBe(lineNodeForScreenRow(component, 3));
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 4));
- expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(element.contains(item6)).toBe(false);
- // move decoration2 and decoration3
- decoration2.getMarker().setHeadScreenPosition([1, 0]);
- decoration3.getMarker().setHeadScreenPosition([0, 0]);
- await component.getNextUpdatePromise();
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(9);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height:
- 3 * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3)
- },
- { tileStartRow: 3, height: 3 * component.getLineHeight() }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(element.contains(item1)).toBe(false);
- expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1));
- expect(item3.previousSibling).toBeNull();
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(element.contains(item6)).toBe(false);
- // change the text
- editor.getBuffer().setTextInRange([[0, 5], [0, 5]], '\n\n');
- await component.getNextUpdatePromise();
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(9);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height: 3 * component.getLineHeight() + getElementHeight(item3)
- },
- {
- tileStartRow: 3,
- height: 3 * component.getLineHeight() + getElementHeight(item2)
- }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(element.contains(item1)).toBe(false);
- expect(item2.previousSibling).toBeNull();
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3));
- expect(item3.previousSibling).toBeNull();
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(element.contains(item4)).toBe(false);
- expect(element.contains(item5)).toBe(false);
- expect(element.contains(item6)).toBe(false);
- // scroll past the first tile
- await setScrollTop(
- component,
- 3 * component.getLineHeight() + getElementHeight(item3)
- );
- expect(component.getRenderedStartRow()).toBe(3);
- expect(component.getRenderedEndRow()).toBe(12);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 3,
- height: 3 * component.getLineHeight() + getElementHeight(item2)
- },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(element.contains(item1)).toBe(false);
- expect(item2.previousSibling).toBeNull();
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3));
- expect(element.contains(item3)).toBe(false);
- expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 9));
- expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 9));
- expect(element.contains(item6)).toBe(false);
- await setScrollTop(component, 0);
- // undo the previous change
- editor.undo();
- await component.getNextUpdatePromise();
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(9);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height:
- 3 * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3)
- },
- { tileStartRow: 3, height: 3 * component.getLineHeight() }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(element.contains(item1)).toBe(false);
- expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1));
- expect(item3.previousSibling).toBeNull();
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(element.contains(item6)).toBe(false);
- // invalidate decorations. this also tests a case where two decorations in
- // the same tile change their height without affecting the tile height nor
- // the content height.
- item3.style.height = '22px';
- item3.style.margin = '10px';
- item2.style.height = '33px';
- item2.style.margin = '0px';
- await component.getNextUpdatePromise();
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(9);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height:
- 3 * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3)
- },
- { tileStartRow: 3, height: 3 * component.getLineHeight() }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(element.contains(item1)).toBe(false);
- expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1));
- expect(item3.previousSibling).toBeNull();
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(element.contains(item6)).toBe(false);
- // make decoration before row 0 as wide as the editor, and insert some text into it so that it wraps.
- item3.style.height = '';
- item3.style.margin = '';
- item3.style.width = '';
- item3.style.wordWrap = 'break-word';
- const contentWidthInCharacters = Math.floor(
- component.getScrollContainerClientWidth() /
- component.getBaseCharacterWidth()
- );
- item3.textContent = 'x'.repeat(contentWidthInCharacters * 2);
- await component.getNextUpdatePromise();
- // make the editor wider, so that the decoration doesn't wrap anymore.
- component.element.style.width =
- component.getGutterContainerWidth() +
- component.getScrollContainerClientWidth() * 2 +
- verticalScrollbarWidth +
- 'px';
- await component.getNextUpdatePromise();
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(9);
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height:
- 3 * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3)
- },
- { tileStartRow: 3, height: 3 * component.getLineHeight() }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(9);
- expect(element.contains(item1)).toBe(false);
- expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1));
- expect(item3.previousSibling).toBeNull();
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(element.contains(item6)).toBe(false);
- // make the editor taller and wider and the same time, ensuring the number
- // of rendered lines is correct.
- setEditorHeightInLines(component, 13);
- setEditorWidthInCharacters(component, 50);
- await conditionPromise(
- () =>
- component.getRenderedStartRow() === 0 &&
- component.getRenderedEndRow() === 13
- );
- expect(component.getScrollHeight()).toBeNear(
- editor.getScreenLineCount() * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3) +
- getElementHeight(item4) +
- getElementHeight(item5) +
- getElementHeight(item6)
- );
- assertTilesAreSizedAndPositionedCorrectly(component, [
- {
- tileStartRow: 0,
- height:
- 3 * component.getLineHeight() +
- getElementHeight(item2) +
- getElementHeight(item3)
- },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- {
- tileStartRow: 6,
- height:
- 3 * component.getLineHeight() +
- getElementHeight(item4) +
- getElementHeight(item5)
- }
- ]);
- assertLinesAreAlignedWithLineNumbers(component);
- expect(queryOnScreenLineElements(element).length).toBe(13);
- expect(element.contains(item1)).toBe(false);
- expect(item2.previousSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 1));
- expect(item3.previousSibling).toBeNull();
- expect(item3.nextSibling).toBe(lineNodeForScreenRow(component, 0));
- expect(item4.previousSibling).toBe(lineNodeForScreenRow(component, 6));
- expect(item4.nextSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(item5.previousSibling).toBe(lineNodeForScreenRow(component, 7));
- expect(item5.nextSibling).toBe(lineNodeForScreenRow(component, 8));
- expect(item6.previousSibling).toBe(lineNodeForScreenRow(component, 12));
- });
- it('correctly positions line numbers when block decorations are located at tile boundaries', async () => {
- const { editor, component } = buildComponent({ rowsPerTile: 3 });
- createBlockDecorationAtScreenRow(editor, 0, {
- height: 5,
- position: 'before'
- });
- createBlockDecorationAtScreenRow(editor, 2, {
- height: 7,
- position: 'after'
- });
- createBlockDecorationAtScreenRow(editor, 3, {
- height: 9,
- position: 'before'
- });
- createBlockDecorationAtScreenRow(editor, 3, {
- height: 11,
- position: 'after'
- });
- createBlockDecorationAtScreenRow(editor, 5, {
- height: 13,
- position: 'after'
- });
- await component.getNextUpdatePromise();
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() + 5 + 7 },
- {
- tileStartRow: 3,
- height: 3 * component.getLineHeight() + 9 + 11 + 13
- },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- });
- it('removes block decorations whose markers have been destroyed', async () => {
- const { editor, component } = buildComponent({ rowsPerTile: 3 });
- const { marker } = createBlockDecorationAtScreenRow(editor, 2, {
- height: 5,
- position: 'before'
- });
- await component.getNextUpdatePromise();
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() + 5 },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- marker.destroy();
- await component.getNextUpdatePromise();
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- });
- it('removes block decorations whose markers are invalidated, and adds them back when they become valid again', async () => {
- const editor = buildEditor({ rowsPerTile: 3, autoHeight: false });
- const { item, decoration, marker } = createBlockDecorationAtScreenRow(
- editor,
- 3,
- { height: 44, position: 'before', invalidate: 'touch' }
- );
- const { component } = buildComponent({ editor, rowsPerTile: 3 });
- // Invalidating the marker removes the block decoration.
- editor.getBuffer().deleteRows(2, 3);
- await component.getNextUpdatePromise();
- expect(item.parentElement).toBeNull();
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- // Moving invalid markers is ignored.
- marker.setScreenRange([[2, 0], [2, 0]]);
- await component.getNextUpdatePromise();
- expect(item.parentElement).toBeNull();
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- // Making the marker valid again adds back the block decoration.
- marker.bufferMarker.valid = true;
- marker.setScreenRange([[3, 0], [3, 0]]);
- await component.getNextUpdatePromise();
- expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 3));
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() },
- { tileStartRow: 3, height: 3 * component.getLineHeight() + 44 },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- // Destroying the decoration and invalidating the marker at the same time
- // removes the block decoration correctly.
- editor.getBuffer().deleteRows(2, 3);
- decoration.destroy();
- await component.getNextUpdatePromise();
- expect(item.parentElement).toBeNull();
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- });
- it('does not render block decorations when decorating invalid markers', async () => {
- const editor = buildEditor({ rowsPerTile: 3, autoHeight: false });
- const { component } = buildComponent({ editor, rowsPerTile: 3 });
- const marker = editor.markScreenPosition([3, 0], { invalidate: 'touch' });
- const item = document.createElement('div');
- item.style.height = 30 + 'px';
- item.style.width = 30 + 'px';
- editor.getBuffer().deleteRows(1, 4);
- editor.decorateMarker(marker, {
- type: 'block',
- item,
- position: 'before'
- });
- await component.getNextUpdatePromise();
- expect(item.parentElement).toBeNull();
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- // Making the marker valid again causes the corresponding block decoration
- // to be added to the editor.
- marker.bufferMarker.valid = true;
- marker.setScreenRange([[2, 0], [2, 0]]);
- await component.getNextUpdatePromise();
- expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 2));
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() + 30 },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- });
- it('does not try to remeasure block decorations whose markers are invalid (regression)', async () => {
- const editor = buildEditor({ rowsPerTile: 3, autoHeight: false });
- const { component } = buildComponent({ editor, rowsPerTile: 3 });
- createBlockDecorationAtScreenRow(editor, 2, {
- height: '12px',
- invalidate: 'touch'
- });
- editor.getBuffer().deleteRows(0, 3);
- await component.getNextUpdatePromise();
- // Trigger a re-measurement of all block decorations.
- await setEditorWidthInCharacters(component, 20);
- assertLinesAreAlignedWithLineNumbers(component);
- assertTilesAreSizedAndPositionedCorrectly(component, [
- { tileStartRow: 0, height: 3 * component.getLineHeight() },
- { tileStartRow: 3, height: 3 * component.getLineHeight() },
- { tileStartRow: 6, height: 3 * component.getLineHeight() }
- ]);
- });
- it('does not throw exceptions when destroying a block decoration inside a marker change event (regression)', async () => {
- const { editor, component } = buildComponent({ rowsPerTile: 3 });
- const marker = editor.markScreenPosition([2, 0]);
- marker.onDidChange(() => {
- marker.destroy();
- });
- const item = document.createElement('div');
- editor.decorateMarker(marker, { type: 'block', item });
- await component.getNextUpdatePromise();
- expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 2));
- marker.setBufferRange([[0, 0], [0, 0]]);
- expect(marker.isDestroyed()).toBe(true);
- await component.getNextUpdatePromise();
- expect(item.parentElement).toBeNull();
- });
- it('does not attempt to render block decorations located outside the visible range', async () => {
- const { editor, component } = buildComponent({
- autoHeight: false,
- rowsPerTile: 2
- });
- await setEditorHeightInLines(component, 2);
- expect(component.getRenderedStartRow()).toBe(0);
- expect(component.getRenderedEndRow()).toBe(4);
- const marker1 = editor.markScreenRange([[3, 0], [5, 0]], {
- reversed: false
- });
- const item1 = document.createElement('div');
- editor.decorateMarker(marker1, { type: 'block', item: item1 });
- const marker2 = editor.markScreenRange([[3, 0], [5, 0]], {
- reversed: true
- });
- const item2 = document.createElement('div');
- editor.decorateMarker(marker2, { type: 'block', item: item2 });
- await component.getNextUpdatePromise();
- expect(item1.parentElement).toBeNull();
- expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3));
- await setScrollTop(component, 4 * component.getLineHeight());
- expect(component.getRenderedStartRow()).toBe(4);
- expect(component.getRenderedEndRow()).toBe(8);
- expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 5));
- expect(item2.parentElement).toBeNull();
- });
- it('measures block decorations correctly when they are added before the component width has been updated', async () => {
- {
- const { editor, component, element } = buildComponent({
- autoHeight: false,
- width: 500,
- attach: false
- });
- const marker = editor.markScreenPosition([0, 0]);
- const item = document.createElement('div');
- item.textContent = 'block decoration';
- editor.decorateMarker(marker, {
- type: 'block',
- item
- });
- jasmine.attachToDOM(element);
- assertLinesAreAlignedWithLineNumbers(component);
- }
- {
- const { editor, component, element } = buildComponent({
- autoHeight: false,
- width: 800
- });
- const marker = editor.markScreenPosition([0, 0]);
- const item = document.createElement('div');
- item.textContent = 'block decoration that could wrap many times';
- editor.decorateMarker(marker, {
- type: 'block',
- item
- });
- element.style.width = '50px';
- await component.getNextUpdatePromise();
- assertLinesAreAlignedWithLineNumbers(component);
- }
- });
- it('bases the width of the block decoration measurement area on the editor scroll width', async () => {
- const { component, element } = buildComponent({
- autoHeight: false,
- width: 150
- });
- expect(component.refs.blockDecorationMeasurementArea.offsetWidth).toBe(
- component.getScrollWidth()
- );
- element.style.width = '800px';
- await component.getNextUpdatePromise();
- expect(component.refs.blockDecorationMeasurementArea.offsetWidth).toBe(
- component.getScrollWidth()
- );
- });
- it('does not change the cursor position when clicking on a block decoration', async () => {
- const { editor, component } = buildComponent();
- const decorationElement = document.createElement('div');
- decorationElement.textContent = 'Parent';
- const childElement = document.createElement('div');
- childElement.textContent = 'Child';
- decorationElement.appendChild(childElement);
- const marker = editor.markScreenPosition([4, 0]);
- editor.decorateMarker(marker, { type: 'block', item: decorationElement });
- await component.getNextUpdatePromise();
- const decorationElementClientRect = decorationElement.getBoundingClientRect();
- component.didMouseDownOnContent({
- target: decorationElement,
- detail: 1,
- button: 0,
- clientX: decorationElementClientRect.left,
- clientY: decorationElementClientRect.top
- });
- expect(editor.getCursorScreenPosition()).toEqual([0, 0]);
- const childElementClientRect = childElement.getBoundingClientRect();
- component.didMouseDownOnContent({
- target: childElement,
- detail: 1,
- button: 0,
- clientX: childElementClientRect.left,
- clientY: childElementClientRect.top
- });
- expect(editor.getCursorScreenPosition()).toEqual([0, 0]);
- });
- it('uses the order property to control the order of block decorations at the same screen row', async () => {
- const editor = buildEditor({ autoHeight: false });
- const { component, element } = buildComponent({ editor });
- element.style.height =
- 10 * component.getLineHeight() + horizontalScrollbarHeight + 'px';
- await component.getNextUpdatePromise();
- // Order parameters that differ from creation order; that collide; and that are not provided.
- const [beforeItems, beforeDecorations] = [
- 30,
- 20,
- undefined,
- 20,
- 10,
- undefined
- ]
- .map(order => {
- return createBlockDecorationAtScreenRow(editor, 2, {
- height: 10,
- position: 'before',
- order
- });
- })
- .reduce(
- (lists, result) => {
- lists[0].push(result.item);
- lists[1].push(result.decoration);
- return lists;
- },
- [[], []]
- );
- const [afterItems] = [undefined, 1, 6, undefined, 6, 2]
- .map(order => {
- return createBlockDecorationAtScreenRow(editor, 2, {
- height: 10,
- position: 'after',
- order
- });
- })
- .reduce(
- (lists, result) => {
- lists[0].push(result.item);
- lists[1].push(result.decoration);
- return lists;
- },
- [[], []]
- );
- await component.getNextUpdatePromise();
- expect(beforeItems[4].previousSibling).toBe(
- lineNodeForScreenRow(component, 1)
- );
- expect(beforeItems[4].nextSibling).toBe(beforeItems[1]);
- expect(beforeItems[1].nextSibling).toBe(beforeItems[3]);
- expect(beforeItems[3].nextSibling).toBe(beforeItems[0]);
- expect(beforeItems[0].nextSibling).toBe(beforeItems[2]);
- expect(beforeItems[2].nextSibling).toBe(beforeItems[5]);
- expect(beforeItems[5].nextSibling).toBe(
- lineNodeForScreenRow(component, 2)
- );
- expect(afterItems[1].previousSibling).toBe(
- lineNodeForScreenRow(component, 2)
- );
- expect(afterItems[1].nextSibling).toBe(afterItems[5]);
- expect(afterItems[5].nextSibling).toBe(afterItems[2]);
- expect(afterItems[2].nextSibling).toBe(afterItems[4]);
- expect(afterItems[4].nextSibling).toBe(afterItems[0]);
- expect(afterItems[0].nextSibling).toBe(afterItems[3]);
- // Create a decoration somewhere else and move it to the same screen row as the existing decorations
- const { item: later, decoration } = createBlockDecorationAtScreenRow(
- editor,
- 4,
- { height: 20, position: 'after', order: 3 }
- );
- await component.getNextUpdatePromise();
- expect(later.previousSibling).toBe(lineNodeForScreenRow(component, 4));
- expect(later.nextSibling).toBe(lineNodeForScreenRow(component, 5));
- decoration.getMarker().setHeadScreenPosition([2, 0]);
- await component.getNextUpdatePromise();
- expect(later.previousSibling).toBe(afterItems[5]);
- expect(later.nextSibling).toBe(afterItems[2]);
- // Move a decoration away from its screen row and ensure the rest maintain their order
- beforeDecorations[3].getMarker().setHeadScreenPosition([5, 0]);
- await component.getNextUpdatePromise();
- expect(beforeItems[3].previousSibling).toBe(
- lineNodeForScreenRow(component, 4)
- );
- expect(beforeItems[3].nextSibling).toBe(
- lineNodeForScreenRow(component, 5)
- );
- expect(beforeItems[4].previousSibling).toBe(
- lineNodeForScreenRow(component, 1)
- );
- expect(beforeItems[4].nextSibling).toBe(beforeItems[1]);
- expect(beforeItems[1].nextSibling).toBe(beforeItems[0]);
- expect(beforeItems[0].nextSibling).toBe(beforeItems[2]);
- expect(beforeItems[2].nextSibling).toBe(beforeItems[5]);
- expect(beforeItems[5].nextSibling).toBe(
- lineNodeForScreenRow(component, 2)
- );
- });
- function createBlockDecorationAtScreenRow(
- editor,
- screenRow,
- { height, margin, marginTop, marginBottom, position, order, invalidate }
- ) {
- const marker = editor.markScreenPosition([screenRow, 0], {
- invalidate: invalidate || 'never'
- });
- const item = document.createElement('div');
- item.style.height = height + 'px';
- if (margin != null) item.style.margin = margin + 'px';
- if (marginTop != null) item.style.marginTop = marginTop + 'px';
- if (marginBottom != null) item.style.marginBottom = marginBottom + 'px';
- item.style.width = 30 + 'px';
- const decoration = editor.decorateMarker(marker, {
- type: 'block',
- item,
- position,
- order
- });
- return { item, decoration, marker };
- }
- function assertTilesAreSizedAndPositionedCorrectly(component, tiles) {
- let top = 0;
- for (let tile of tiles) {
- const linesTileElement = lineNodeForScreenRow(
- component,
- tile.tileStartRow
- ).parentElement;
- const linesTileBoundingRect = linesTileElement.getBoundingClientRect();
- expect(linesTileBoundingRect.height).toBeNear(tile.height);
- expect(linesTileBoundingRect.top).toBeNear(top);
- const lineNumbersTileElement = lineNumberNodeForScreenRow(
- component,
- tile.tileStartRow
- ).parentElement;
- const lineNumbersTileBoundingRect = lineNumbersTileElement.getBoundingClientRect();
- expect(lineNumbersTileBoundingRect.height).toBeNear(tile.height);
- expect(lineNumbersTileBoundingRect.top).toBeNear(top);
- top += tile.height;
- }
- }
- function assertLinesAreAlignedWithLineNumbers(component) {
- const startRow = component.getRenderedStartRow();
- const endRow = component.getRenderedEndRow();
- for (let row = startRow; row < endRow; row++) {
- const lineNode = lineNodeForScreenRow(component, row);
- const lineNumberNode = lineNumberNodeForScreenRow(component, row);
- expect(lineNumberNode.getBoundingClientRect().top).toBeNear(
- lineNode.getBoundingClientRect().top
- );
- }
- }
- });
- describe('cursor decorations', () => {
- it('allows default cursors to be customized', async () => {
- const { component, element, editor } = buildComponent();
- editor.addCursorAtScreenPosition([1, 0]);
- const [cursorMarker1, cursorMarker2] = editor
- .getCursors()
- .map(c => c.getMarker());
- editor.decorateMarker(cursorMarker1, { type: 'cursor', class: 'a' });
- editor.decorateMarker(cursorMarker2, {
- type: 'cursor',
- class: 'b',
- style: { visibility: 'hidden' }
- });
- editor.decorateMarker(cursorMarker2, {
- type: 'cursor',
- style: { backgroundColor: 'red' }
- });
- await component.getNextUpdatePromise();
- const cursorNodes = element.querySelectorAll('.cursor');
- expect(cursorNodes.length).toBe(2);
- expect(cursorNodes[0].className).toBe('cursor a');
- expect(cursorNodes[1].className).toBe('cursor b');
- expect(cursorNodes[1].style.visibility).toBe('hidden');
- expect(cursorNodes[1].style.backgroundColor).toBe('red');
- });
- it('allows markers that are not actually associated with cursors to be decorated as if they were cursors', async () => {
- const { component, element, editor } = buildComponent();
- const marker = editor.markScreenPosition([1, 0]);
- editor.decorateMarker(marker, { type: 'cursor', class: 'a' });
- await component.getNextUpdatePromise();
- const cursorNodes = element.querySelectorAll('.cursor');
- expect(cursorNodes.length).toBe(2);
- expect(cursorNodes[0].className).toBe('cursor');
- expect(cursorNodes[1].className).toBe('cursor a');
- });
- });
- describe('text decorations', () => {
- it('injects spans with custom class names and inline styles based on text decorations', async () => {
- const { component, element, editor } = buildComponent({ rowsPerTile: 2 });
- const markerLayer = editor.addMarkerLayer();
- const marker1 = markerLayer.markBufferRange([[0, 2], [2, 7]]);
- const marker2 = markerLayer.markBufferRange([[0, 2], [3, 8]]);
- const marker3 = markerLayer.markBufferRange([[1, 13], [2, 7]]);
- editor.decorateMarker(marker1, {
- type: 'text',
- class: 'a',
- style: { color: 'red' }
- });
- editor.decorateMarker(marker2, {
- type: 'text',
- class: 'b',
- style: { color: 'blue' }
- });
- editor.decorateMarker(marker3, {
- type: 'text',
- class: 'c',
- style: { color: 'green' }
- });
- await component.getNextUpdatePromise();
- expect(textContentOnRowMatchingSelector(component, 0, '.a')).toBe(
- editor.lineTextForScreenRow(0).slice(2)
- );
- expect(textContentOnRowMatchingSelector(component, 1, '.a')).toBe(
- editor.lineTextForScreenRow(1)
- );
- expect(textContentOnRowMatchingSelector(component, 2, '.a')).toBe(
- editor.lineTextForScreenRow(2).slice(0, 7)
- );
- expect(textContentOnRowMatchingSelector(component, 3, '.a')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 0, '.b')).toBe(
- editor.lineTextForScreenRow(0).slice(2)
- );
- expect(textContentOnRowMatchingSelector(component, 1, '.b')).toBe(
- editor.lineTextForScreenRow(1)
- );
- expect(textContentOnRowMatchingSelector(component, 2, '.b')).toBe(
- editor.lineTextForScreenRow(2)
- );
- expect(textContentOnRowMatchingSelector(component, 3, '.b')).toBe(
- editor.lineTextForScreenRow(3).slice(0, 8)
- );
- expect(textContentOnRowMatchingSelector(component, 0, '.c')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 1, '.c')).toBe(
- editor.lineTextForScreenRow(1).slice(13)
- );
- expect(textContentOnRowMatchingSelector(component, 2, '.c')).toBe(
- editor.lineTextForScreenRow(2).slice(0, 7)
- );
- expect(textContentOnRowMatchingSelector(component, 3, '.c')).toBe('');
- for (const span of element.querySelectorAll('.a:not(.c)')) {
- expect(span.style.color).toBe('red');
- }
- for (const span of element.querySelectorAll('.b:not(.c):not(.a)')) {
- expect(span.style.color).toBe('blue');
- }
- for (const span of element.querySelectorAll('.c')) {
- expect(span.style.color).toBe('green');
- }
- marker2.setHeadScreenPosition([3, 10]);
- await component.getNextUpdatePromise();
- expect(textContentOnRowMatchingSelector(component, 3, '.b')).toBe(
- editor.lineTextForScreenRow(3).slice(0, 10)
- );
- });
- it('correctly handles text decorations starting before the first rendered row and/or ending after the last rendered row', async () => {
- const { component, element, editor } = buildComponent({
- autoHeight: false,
- rowsPerTile: 1
- });
- element.style.height = 4 * component.getLineHeight() + 'px';
- await component.getNextUpdatePromise();
- await setScrollTop(component, 4 * component.getLineHeight());
- expect(component.getRenderedStartRow()).toBeNear(4);
- expect(component.getRenderedEndRow()).toBeNear(9);
- const markerLayer = editor.addMarkerLayer();
- const marker1 = markerLayer.markBufferRange([[0, 0], [4, 5]]);
- const marker2 = markerLayer.markBufferRange([[7, 2], [10, 8]]);
- editor.decorateMarker(marker1, { type: 'text', class: 'a' });
- editor.decorateMarker(marker2, { type: 'text', class: 'b' });
- await component.getNextUpdatePromise();
- expect(textContentOnRowMatchingSelector(component, 4, '.a')).toBe(
- editor.lineTextForScreenRow(4).slice(0, 5)
- );
- expect(textContentOnRowMatchingSelector(component, 5, '.a')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 6, '.a')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 7, '.a')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 8, '.a')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 4, '.b')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 5, '.b')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 6, '.b')).toBe('');
- expect(textContentOnRowMatchingSelector(component, 7, '.b')).toBe(
- editor.lineTextForScreenRow(7).slice(2)
- );
- expect(textContentOnRowMatchingSelector(component, 8, '.b')).toBe(
- editor.lineTextForScreenRow(8)
- );
- });
- it('does not create empty spans when a text decoration contains a row but another text decoration starts or ends at the beginning of it', async () => {
- const { component, element, editor } = buildComponent();
- const markerLayer = editor.addMarkerLayer();
- const marker1 = markerLayer.markBufferRange([[0, 2], [4, 0]]);
- const marker2 = markerLayer.markBufferRange([[2, 0], [5, 8]]);
- editor.decorateMarker(marker1, { type: 'text', class: 'a' });
- editor.decorateMarker(marker2, { type: 'text', class: 'b' });
- await component.getNextUpdatePromise();
- for (const decorationSpan of element.querySelectorAll('.a, .b')) {
- expect(decorationSpan.textContent).not.toBe('');
- }
- });
- it('does not create empty text nodes when a text decoration ends right after a text tag', async () => {
- const { component, editor } = buildComponent();
- const marker = editor.markBufferRange([[0, 8], [0, 29]]);
- editor.decorateMarker(marker, { type: 'text', class: 'a' });
- await component.getNextUpdatePromise();
- for (const textNode of textNodesForScreenRow(component, 0)) {
- expect(textNode.textContent).not.toBe('');
- }
- });
- function textContentOnRowMatchingSelector(component, row, selector) {
- return Array.from(
- lineNodeForScreenRow(component, row).querySelectorAll(selector)
- )
- .map(span => span.textContent)
- .join('');
- }
- });
- describe('mouse input', () => {
- describe('on the lines', () => {
- describe('when there is only one cursor', () => {
- it('positions the cursor on single-click or when middle-clicking', async () => {
- atom.config.set('editor.selectionClipboard', false);
- for (const button of [0, 1]) {
- const { component, editor } = buildComponent();
- const { lineHeight } = component.measurements;
- editor.setCursorScreenPosition([Infinity, Infinity], {
- autoscroll: false
- });
- component.didMouseDownOnContent({
- detail: 1,
- button,
- clientX: clientLeftForCharacter(component, 0, 0) - 1,
- clientY: clientTopForLine(component, 0) - 1
- });
- expect(editor.getCursorScreenPosition()).toEqual([0, 0]);
- const maxRow = editor.getLastScreenRow();
- editor.setCursorScreenPosition([Infinity, Infinity], {
- autoscroll: false
- });
- component.didMouseDownOnContent({
- detail: 1,
- button,
- clientX:
- clientLeftForCharacter(
- component,
- maxRow,
- editor.lineLengthForScreenRow(maxRow)
- ) + 1,
- clientY: clientTopForLine(component, maxRow) + 1
- });
- expect(editor.getCursorScreenPosition()).toEqual([
- maxRow,
- editor.lineLengthForScreenRow(maxRow)
- ]);
- component.didMouseDownOnContent({
- detail: 1,
- button,
- clientX:
- clientLeftForCharacter(
- component,
- 0,
- editor.lineLengthForScreenRow(0)
- ) + 1,
- clientY: clientTopForLine(component, 0) + lineHeight / 2
- });
- expect(editor.getCursorScreenPosition()).toEqual([
- 0,
- editor.lineLengthForScreenRow(0)
- ]);
- component.didMouseDownOnContent({
- detail: 1,
- button,
- clientX:
- (clientLeftForCharacter(component, 3, 0) +
- clientLeftForCharacter(component, 3, 1)) /
- 2,
- clientY: clientTopForLine(component, 1) + lineHeight / 2
- });
- expect(editor.getCursorScreenPosition()).toEqual([1, 0]);
- component.didMouseDownOnContent({
- detail: 1,
- button,
- clientX:
- (clientLeftForCharacter(component, 3, 14) +
- clientLeftForCharacter(component, 3, 15)) /
- 2,
- clientY: clientTopForLine(component, 3) + lineHeight / 2
- });
- expect(editor.getCursorScreenPosition()).toEqual([3, 14]);
- component.didMouseDownOnContent({
- detail: 1,
- button,
- clientX:
- (clientLeftForCharacter(component, 3, 14) +
- clientLeftForCharacter(component, 3, 15)) /
- 2 +
- 1,
- clientY: clientTopForLine(component, 3) + lineHeight / 2
- });
- expect(editor.getCursorScreenPosition()).toEqual([3, 15]);
- editor.getBuffer().setTextInRange([[3, 14], [3, 15]], '🐣');
- await component.getNextUpdatePromise();
- component.didMouseDownOnContent({
- detail: 1,
- button,
- clientX:
- (clientLeftForCharacter(component, 3, 14) +
- clientLeftForCharacter(component, 3, 16)) /
- 2,
- clientY: clientTopForLine(component, 3) + lineHeight / 2
- });
- expect(editor.getCursorScreenPosition()).toEqual([3, 14]);
- component.didMouseDownOnContent({
- detail: 1,
- button,
- clientX:
- (clientLeftForCharacter(component, 3, 14) +
- clientLeftForCharacter(component, 3, 16)) /
- 2 +
- 1,
- clientY: clientTopForLine(component, 3) + lineHeight / 2
- });
- expect(editor.getCursorScreenPosition()).toEqual([3, 16]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- }
- });
- });
- describe('when the input is for the primary mouse button', () => {
- it('selects words on double-click', () => {
- const { component, editor } = buildComponent();
- const { clientX, clientY } = clientPositionForCharacter(
- component,
- 1,
- 16
- );
- component.didMouseDownOnContent({
- detail: 1,
- button: 0,
- clientX,
- clientY
- });
- component.didMouseDownOnContent({
- detail: 2,
- button: 0,
- clientX,
- clientY
- });
- expect(editor.getSelectedScreenRange()).toEqual([[1, 13], [1, 21]]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('selects lines on triple-click', () => {
- const { component, editor } = buildComponent();
- const { clientX, clientY } = clientPositionForCharacter(
- component,
- 1,
- 16
- );
- component.didMouseDownOnContent({
- detail: 1,
- button: 0,
- clientX,
- clientY
- });
- component.didMouseDownOnContent({
- detail: 2,
- button: 0,
- clientX,
- clientY
- });
- component.didMouseDownOnContent({
- detail: 3,
- button: 0,
- clientX,
- clientY
- });
- expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [2, 0]]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('adds or removes cursors when holding cmd or ctrl when single-clicking', () => {
- atom.config.set('editor.multiCursorOnClick', true);
- const { component, editor } = buildComponent({ platform: 'darwin' });
- expect(editor.getCursorScreenPositions()).toEqual([[0, 0]]);
- // add cursor at 1, 16
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 1,
- button: 0,
- metaKey: true
- })
- );
- expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]);
- // remove cursor at 0, 0
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 0, 0), {
- detail: 1,
- button: 0,
- metaKey: true
- })
- );
- expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]);
- // cmd-click cursor at 1, 16 but don't remove it because it's the last one
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 1,
- button: 0,
- metaKey: true
- })
- );
- expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]);
- // cmd-clicking within a selection destroys it
- editor.addSelectionForScreenRange([[2, 10], [2, 15]], {
- autoscroll: false
- });
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 16], [1, 16]],
- [[2, 10], [2, 15]]
- ]);
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 2, 13), {
- detail: 1,
- button: 0,
- metaKey: true
- })
- );
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 16], [1, 16]]
- ]);
- // ctrl-click does not add cursors on macOS, nor does it move the cursor
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 4), {
- detail: 1,
- button: 0,
- ctrlKey: true
- })
- );
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 16], [1, 16]]
- ]);
- // ctrl-click adds cursors on platforms *other* than macOS
- component.props.platform = 'win32';
- editor.setCursorScreenPosition([1, 4], { autoscroll: false });
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 1,
- button: 0,
- ctrlKey: true
- })
- );
- expect(editor.getCursorScreenPositions()).toEqual([[1, 4], [1, 16]]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('adds word selections when holding cmd or ctrl when double-clicking', () => {
- atom.config.set('editor.multiCursorOnClick', true);
- const { component, editor } = buildComponent();
- editor.addCursorAtScreenPosition([1, 16], { autoscroll: false });
- expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]);
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 1,
- button: 0,
- metaKey: true
- })
- );
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 2,
- button: 0,
- metaKey: true
- })
- );
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[0, 0], [0, 0]],
- [[1, 13], [1, 21]]
- ]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('adds line selections when holding cmd or ctrl when triple-clicking', () => {
- atom.config.set('editor.multiCursorOnClick', true);
- const { component, editor } = buildComponent();
- editor.addCursorAtScreenPosition([1, 16], { autoscroll: false });
- expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]]);
- const { clientX, clientY } = clientPositionForCharacter(
- component,
- 1,
- 16
- );
- component.didMouseDownOnContent({
- detail: 1,
- button: 0,
- metaKey: true,
- clientX,
- clientY
- });
- component.didMouseDownOnContent({
- detail: 2,
- button: 0,
- metaKey: true,
- clientX,
- clientY
- });
- component.didMouseDownOnContent({
- detail: 3,
- button: 0,
- metaKey: true,
- clientX,
- clientY
- });
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[0, 0], [0, 0]],
- [[1, 0], [2, 0]]
- ]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('does not add cursors when holding cmd or ctrl when single-clicking', () => {
- atom.config.set('editor.multiCursorOnClick', false);
- const { component, editor } = buildComponent({ platform: 'darwin' });
- expect(editor.getCursorScreenPositions()).toEqual([[0, 0]]);
- // moves cursor to 1, 16
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 1,
- button: 0,
- metaKey: true
- })
- );
- expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]);
- // ctrl-click does not add cursors on macOS, nor does it move the cursor
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 4), {
- detail: 1,
- button: 0,
- ctrlKey: true
- })
- );
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 16], [1, 16]]
- ]);
- // ctrl-click does not add cursors on platforms *other* than macOS
- component.props.platform = 'win32';
- editor.setCursorScreenPosition([1, 4], { autoscroll: false });
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 1,
- button: 0,
- ctrlKey: true
- })
- );
- expect(editor.getCursorScreenPositions()).toEqual([[1, 16]]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('does not add word selections when holding cmd or ctrl when double-clicking', () => {
- atom.config.set('editor.multiCursorOnClick', false);
- const { component, editor } = buildComponent();
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 1,
- button: 0,
- metaKey: true
- })
- );
- component.didMouseDownOnContent(
- Object.assign(clientPositionForCharacter(component, 1, 16), {
- detail: 2,
- button: 0,
- metaKey: true
- })
- );
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 13], [1, 21]]
- ]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('does not add line selections when holding cmd or ctrl when triple-clicking', () => {
- atom.config.set('editor.multiCursorOnClick', false);
- const { component, editor } = buildComponent();
- const { clientX, clientY } = clientPositionForCharacter(
- component,
- 1,
- 16
- );
- component.didMouseDownOnContent({
- detail: 1,
- button: 0,
- metaKey: true,
- clientX,
- clientY
- });
- component.didMouseDownOnContent({
- detail: 2,
- button: 0,
- metaKey: true,
- clientX,
- clientY
- });
- component.didMouseDownOnContent({
- detail: 3,
- button: 0,
- metaKey: true,
- clientX,
- clientY
- });
- expect(editor.getSelectedScreenRanges()).toEqual([[[1, 0], [2, 0]]]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('expands the last selection on shift-click', () => {
- const { component, editor } = buildComponent();
- editor.setCursorScreenPosition([2, 18], { autoscroll: false });
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0,
- shiftKey: true
- },
- clientPositionForCharacter(component, 1, 4)
- )
- );
- expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [2, 18]]);
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0,
- shiftKey: true
- },
- clientPositionForCharacter(component, 4, 4)
- )
- );
- expect(editor.getSelectedScreenRange()).toEqual([[2, 18], [4, 4]]);
- // reorients word-wise selections to keep the word selected regardless of
- // where the subsequent shift-click occurs
- editor.setCursorScreenPosition([2, 18], { autoscroll: false });
- editor.getLastSelection().selectWord({ autoscroll: false });
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0,
- shiftKey: true
- },
- clientPositionForCharacter(component, 1, 4)
- )
- );
- expect(editor.getSelectedScreenRange()).toEqual([[1, 2], [2, 20]]);
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0,
- shiftKey: true
- },
- clientPositionForCharacter(component, 3, 11)
- )
- );
- expect(editor.getSelectedScreenRange()).toEqual([[2, 14], [3, 13]]);
- // reorients line-wise selections to keep the line selected regardless of
- // where the subsequent shift-click occurs
- editor.setCursorScreenPosition([2, 18], { autoscroll: false });
- editor.getLastSelection().selectLine(null, { autoscroll: false });
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0,
- shiftKey: true
- },
- clientPositionForCharacter(component, 1, 4)
- )
- );
- expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 0]]);
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0,
- shiftKey: true
- },
- clientPositionForCharacter(component, 3, 11)
- )
- );
- expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [4, 0]]);
- expect(editor.testAutoscrollRequests).toEqual([]);
- });
- it('expands the last selection on drag', () => {
- atom.config.set('editor.multiCursorOnClick', true);
- const { component, editor } = buildComponent();
- spyOn(component, 'handleMouseDragUntilMouseUp');
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0
- },
- clientPositionForCharacter(component, 1, 4)
- )
- );
- {
- const {
- didDrag,
- didStopDragging
- } = component.handleMouseDragUntilMouseUp.argsForCall[0][0];
- didDrag(clientPositionForCharacter(component, 8, 8));
- expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [8, 8]]);
- didDrag(clientPositionForCharacter(component, 4, 8));
- expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [4, 8]]);
- didStopDragging();
- expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [4, 8]]);
- }
- // Click-drag a second selection... selections are not merged until the
- // drag stops.
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0,
- metaKey: 1
- },
- clientPositionForCharacter(component, 8, 8)
- )
- );
- {
- const {
- didDrag,
- didStopDragging
- } = component.handleMouseDragUntilMouseUp.argsForCall[1][0];
- didDrag(clientPositionForCharacter(component, 2, 8));
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 4], [4, 8]],
- [[2, 8], [8, 8]]
- ]);
- didDrag(clientPositionForCharacter(component, 6, 8));
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 4], [4, 8]],
- [[6, 8], [8, 8]]
- ]);
- didDrag(clientPositionForCharacter(component, 2, 8));
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 4], [4, 8]],
- [[2, 8], [8, 8]]
- ]);
- didStopDragging();
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[1, 4], [8, 8]]
- ]);
- }
- });
- it('expands the selection word-wise on double-click-drag', () => {
- const { component, editor } = buildComponent();
- spyOn(component, 'handleMouseDragUntilMouseUp');
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 1,
- button: 0
- },
- clientPositionForCharacter(component, 1, 4)
- )
- );
- component.didMouseDownOnContent(
- Object.assign(
- {
- detail: 2,
- button: 0
- },
- clientPositionForCharacter(component, 1, 4)
- )
- );
- const {
- didDrag
- } = component.handleMouseDragUntilMouseUp.argsForCall[1][0];
- didDrag(clientPositionForCharacter(component, 0, 8));
- expect(editor.getSelectedScreenRange()).toEqual([[0, 4], [1, 5]]);
- didDrag(clientPositionForCharacter(component, 2, 10));
- expect(editor.getSelectedScreenRange()).toEqual([[1, 2], [2, 13]]);
- });
- it('expands the selection line-wise on triple-click-drag', () => {
- const { component, editor } = buildComponent();
- spyOn(component, 'handleMouseDragUntilMouseUp');
- const tripleClickPosition = clientPositionForCharacter(
- component,
- 2,
- 8
- );
- component.didMouseDownOnContent(
- Object.assign({ detail: 1, button: 0 }, tripleClickPosition)
- );
- component.didMouseDownOnContent(
- Object.assign({ detail: 2, button: 0 }, tripleClickPosition)
- );
- component.didMouseDownOnContent(
- Object.assign({ detail: 3, button: 0 }, tripleClickPosition)
- );
- const {
- didDrag
- } = component.handleMouseDragUntilMouseUp.argsForCall[2][0];
- didDrag(clientPositionForCharacter(component, 1, 8));
- expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [3, 0]]);
- didDrag(clientPositionForCharacter(component, 4, 10));
- expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [5, 0]]);
- });
- it('destroys folds when clicking on their fold markers', async () => {
- const { component, element, editor } = buildComponent();
- editor.foldBufferRow(1);
- await component.getNextUpdatePromise();
- const target = element.querySelector('.fold-marker');
- const { clientX, clientY } = clientPositionForCharacter(
- component,
- 1,
- editor.lineLengthForScreenRow(1)
- );
- component.didMouseDownOnContent({
- detail: 1,
- button: 0,
- target,
- clientX,
- clientY
- });
- expect(editor.isFoldedAtBufferRow(1)).toBe(false);
- expect(editor.getCursorScreenPosition()).toEqual([0, 0]);
- });
- it('autoscrolls the content when dragging near the edge of the scroll container', async () => {
- const { component } = buildComponent({
- width: 200,
- height: 200
- });
- spyOn(component, 'handleMouseDragUntilMouseUp');
- let previousScrollTop = 0;
- let previousScrollLeft = 0;
- function assertScrolledDownAndRight() {
- expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop);
- previousScrollTop = component.getScrollTop();
- expect(component.getScrollLeft()).toBeGreaterThan(
- previousScrollLeft
- );
- previousScrollLeft = component.getScrollLeft();
- }
- function assertScrolledUpAndLeft() {
- expect(component.getScrollTop()).toBeLessThan(previousScrollTop);
- previousScrollTop = component.getScrollTop();
- expect(component.getScrollLeft()).toBeLessThan(previousScrollLeft);
- previousScrollLeft = component.getScrollLeft();
- }
- component.didMouseDownOnContent({
- detail: 1,
- button: 0,
- clientX: 100,
- clientY: 100
- });
- const {
- didDrag
- } = component.handleMouseDragUntilMouseUp.argsForCall[0][0];
- didDrag({ clientX: 199, clientY: 199 });
- assertScrolledDownAndRight();
- didDrag({ clientX: 199, clientY: 199 });
- assertScrolledDownAndRight();
- didDrag({ clientX: 199, clientY: 199 });
- assertScrolledDownAndRight();
- didDrag({
- clientX: component.getGutterContainerWidth() + 1,
- clientY: 1
- });
- assertScrolledUpAndLeft();
- didDrag({
- clientX: component.getGutterContainerWidth() + 1,
- clientY: 1
- });
- assertScrolledUpAndLeft();
- didDrag({
- clientX: component.getGutterContainerWidth() + 1,
- clientY: 1
- });
- assertScrolledUpAndLeft();
- // Don't artificially update scroll position beyond possible values
- expect(component.getScrollTop()).toBe(0);
- expect(component.getScrollLeft()).toBe(0);
- didDrag({
- clientX: component.getGutterContainerWidth() + 1,
- clientY: 1
- });
- expect(component.getScrollTop()).toBe(0);
- expect(component.getScrollLeft()).toBe(0);
- const maxScrollTop = component.getMaxScrollTop();
- const maxScrollLeft = component.getMaxScrollLeft();
- setScrollTop(component, maxScrollTop);
- await setScrollLeft(component, maxScrollLeft);
- didDrag({ clientX: 199, clientY: 199 });
- didDrag({ clientX: 199, clientY: 199 });
- didDrag({ clientX: 199, clientY: 199 });
- expect(component.getScrollTop()).toBeNear(maxScrollTop);
- expect(component.getScrollLeft()).toBeNear(maxScrollLeft);
- });
- });
- it('pastes the previously selected text when clicking the middle mouse button on Linux', async () => {
- spyOn(electron.ipcRenderer, 'send').andCallFake(function(
- eventName,
- selectedText
- ) {
- if (eventName === 'write-text-to-selection-clipboard') {
- clipboard.writeText(selectedText, 'selection');
- }
- });
- const { component, editor } = buildComponent({ platform: 'linux' });
- // Middle mouse pasting.
- atom.config.set('editor.selectionClipboard', true);
- editor.setSelectedBufferRange([[1, 6], [1, 10]]);
- await conditionPromise(() => TextEditor.clipboard.read() === 'sort');
- component.didMouseDownOnContent({
- button: 1,
- clientX: clientLeftForCharacter(component, 10, 0),
- clientY: clientTopForLine(component, 10)
- });
- expect(TextEditor.clipboard.read()).toBe('sort');
- expect(editor.lineTextForBufferRow(10)).toBe('sort');
- editor.undo();
- // Doesn't paste when middle mouse button is clicked
- atom.config.set('editor.selectionClipboard', false);
- editor.setSelectedBufferRange([[1, 6], [1, 10]]);
- component.didMouseDownOnContent({
- button: 1,
- clientX: clientLeftForCharacter(component, 10, 0),
- clientY: clientTopForLine(component, 10)
- });
- expect(TextEditor.clipboard.read()).toBe('sort');
- expect(editor.lineTextForBufferRow(10)).toBe('');
- // Ensure left clicks don't interfere.
- atom.config.set('editor.selectionClipboard', true);
- editor.setSelectedBufferRange([[1, 2], [1, 5]]);
- await conditionPromise(() => TextEditor.clipboard.read() === 'var');
- component.didMouseDownOnContent({
- button: 0,
- detail: 1,
- clientX: clientLeftForCharacter(component, 10, 0),
- clientY: clientTopForLine(component, 10)
- });
- component.didMouseDownOnContent({
- button: 1,
- clientX: clientLeftForCharacter(component, 10, 0),
- clientY: clientTopForLine(component, 10)
- });
- expect(editor.lineTextForBufferRow(10)).toBe('var');
- });
- it('does not paste into a read only editor when clicking the middle mouse button on Linux', async () => {
- spyOn(electron.ipcRenderer, 'send').andCallFake(function(
- eventName,
- selectedText
- ) {
- if (eventName === 'write-text-to-selection-clipboard') {
- clipboard.writeText(selectedText, 'selection');
- }
- });
- const { component, editor } = buildComponent({
- platform: 'linux',
- readOnly: true
- });
- // Select the word 'sort' on line 2 and copy to clipboard
- editor.setSelectedBufferRange([[1, 6], [1, 10]]);
- await conditionPromise(() => TextEditor.clipboard.read() === 'sort');
- // Middle-click in the buffer at line 11, column 1
- component.didMouseDownOnContent({
- button: 1,
- clientX: clientLeftForCharacter(component, 10, 0),
- clientY: clientTopForLine(component, 10)
- });
- // Ensure that the correct text was copied but not pasted
- expect(TextEditor.clipboard.read()).toBe('sort');
- expect(editor.lineTextForBufferRow(10)).toBe('');
- });
- });
- describe('on the line number gutter', () => {
- it('selects all buffer rows intersecting the clicked screen row when a line number is clicked', async () => {
- const { component, editor } = buildComponent();
- spyOn(component, 'handleMouseDragUntilMouseUp');
- editor.setSoftWrapped(true);
- await component.getNextUpdatePromise();
- await setEditorWidthInCharacters(component, 50);
- editor.foldBufferRange([[4, Infinity], [7, Infinity]]);
- await component.getNextUpdatePromise();
- // Selects entire buffer line when clicked screen line is soft-wrapped
- component.didMouseDownOnLineNumberGutter({
- button: 0,
- clientY: clientTopForLine(component, 3)
- });
- expect(editor.getSelectedScreenRange()).toEqual([[3, 0], [5, 0]]);
- expect(editor.getSelectedBufferRange()).toEqual([[3, 0], [4, 0]]);
- // Selects entire screen line, even if folds cause that selection to
- // span multiple buffer lines
- component.didMouseDownOnLineNumberGutter({
- button: 0,
- clientY: clientTopForLine(component, 5)
- });
- expect(editor.getSelectedScreenRange()).toEqual([[5, 0], [6, 0]]);
- expect(editor.getSelectedBufferRange()).toEqual([[4, 0], [8, 0]]);
- });
- it('adds new selections when a line number is meta-clicked', async () => {
- const { component, editor } = buildComponent();
- editor.setSoftWrapped(true);
- await component.getNextUpdatePromise();
- await setEditorWidthInCharacters(component, 50);
- editor.foldBufferRange([[4, Infinity], [7, Infinity]]);
- await component.getNextUpdatePromise();
- // Selects entire buffer line when clicked screen line is soft-wrapped
- component.didMouseDownOnLineNumberGutter({
- button: 0,
- metaKey: true,
- clientY: clientTopForLine(component, 3)
- });
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[0, 0], [0, 0]],
- [[3, 0], [5, 0]]
- ]);
- expect(editor.getSelectedBufferRanges()).toEqual([
- [[0, 0], [0, 0]],
- [[3, 0], [4, 0]]
- ]);
- // Selects entire screen line, even if folds cause that selection to
- // span multiple buffer lines
- component.didMouseDownOnLineNumberGutter({
- button: 0,
- metaKey: true,
- clientY: clientTopForLine(component, 5)
- });
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[0, 0], [0, 0]],
- [[3, 0], [5, 0]],
- [[5, 0], [6, 0]]
- ]);
- expect(editor.getSelectedBufferRanges()).toEqual([
- [[0, 0], [0, 0]],
- [[3, 0], [4, 0]],
- [[4, 0], [8, 0]]
- ]);
- });
- it('expands the last selection when a line number is shift-clicked', async () => {
- const { component, editor } = buildComponent();
- spyOn(component, 'handleMouseDragUntilMouseUp');
- editor.setSoftWrapped(true);
- await component.getNextUpdatePromise();
- await setEditorWidthInCharacters(component, 50);
- editor.foldBufferRange([[4, Infinity], [7, Infinity]]);
- await component.getNextUpdatePromise();
- editor.setSelectedScreenRange([[3, 4], [3, 8]]);
- editor.addCursorAtScreenPosition([2, 10]);
- component.didMouseDownOnLineNumberGutter({
- button: 0,
- shiftKey: true,
- clientY: clientTopForLine(component, 5)
- });
- expect(editor.getSelectedBufferRanges()).toEqual([
- [[3, 4], [3, 8]],
- [[2, 10], [8, 0]]
- ]);
- // Original selection is preserved when shift-click-dragging
- const {
- didDrag,
- didStopDragging
- } = component.handleMouseDragUntilMouseUp.argsForCall[0][0];
- didDrag({
- clientY: clientTopForLine(component, 1)
- });
- expect(editor.getSelectedBufferRanges()).toEqual([
- [[3, 4], [3, 8]],
- [[1, 0], [2, 10]]
- ]);
- didDrag({
- clientY: clientTopForLine(component, 5)
- });
- didStopDragging();
- expect(editor.getSelectedBufferRanges()).toEqual([[[2, 10], [8, 0]]]);
- });
- it('expands the selection when dragging', async () => {
- const { component, editor } = buildComponent();
- spyOn(component, 'handleMouseDragUntilMouseUp');
- editor.setSoftWrapped(true);
- await component.getNextUpdatePromise();
- await setEditorWidthInCharacters(component, 50);
- editor.foldBufferRange([[4, Infinity], [7, Infinity]]);
- await component.getNextUpdatePromise();
- editor.setSelectedScreenRange([[3, 4], [3, 6]]);
- component.didMouseDownOnLineNumberGutter({
- button: 0,
- metaKey: true,
- clientY: clientTopForLine(component, 2)
- });
- const {
- didDrag,
- didStopDragging
- } = component.handleMouseDragUntilMouseUp.argsForCall[0][0];
- didDrag({
- clientY: clientTopForLine(component, 1)
- });
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[3, 4], [3, 6]],
- [[1, 0], [3, 0]]
- ]);
- didDrag({
- clientY: clientTopForLine(component, 5)
- });
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[3, 4], [3, 6]],
- [[2, 0], [6, 0]]
- ]);
- expect(editor.isFoldedAtBufferRow(4)).toBe(true);
- didDrag({
- clientY: clientTopForLine(component, 3)
- });
- expect(editor.getSelectedScreenRanges()).toEqual([
- [[3, 4], [3, 6]],
- [[2, 0], [4, 4]]
- ]);
- didStopDragging();
- expect(editor.getSelectedScreenRanges()).toEqual([[[2, 0], [4, 4]]]);
- });
- it('toggles folding when clicking on the right icon of a foldable line number', async () => {
- const { component, element, editor } = buildComponent();
- let target = element
- .querySelectorAll('.line-number')[1]
- .querySelector('.icon-right');
- expect(editor.isFoldedAtScreenRow(1)).toBe(false);
- component.didMouseDownOnLineNumberGutter({
- target,
- button: 0,
- clientY: clientTopForLine(component, 1)
- });
- expect(editor.isFoldedAtScreenRow(1)).toBe(true);
- await component.getNextUpdatePromise();
- component.didMouseDownOnLineNumberGutter({
- target,
- button: 0,
- clientY: clientTopForLine(component, 1)
- });
- await component.getNextUpdatePromise();
- expect(editor.isFoldedAtScreenRow(1)).toBe(false);
- editor.foldBufferRange([[5, 12], [5, 17]]);
- await component.getNextUpdatePromise();
- expect(editor.isFoldedAtScreenRow(5)).toBe(true);
- target = element
- .querySelectorAll('.line-number')[4]
- .querySelector('.icon-right');
- component.didMouseDownOnLineNumberGutter({
- target,
- button: 0,
- clientY: clientTopForLine(component, 4)
- });
- expect(editor.isFoldedAtScreenRow(4)).toBe(false);
- });
- it('autoscrolls when dragging near the top or bottom of the gutter', async () => {
- const { component } = buildComponent({
- width: 200,
- height: 200
- });
- spyOn(component, 'handleMouseDragUntilMouseUp');
- let previousScrollTop = 0;
- let previousScrollLeft = 0;
- function assertScrolledDown() {
- expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop);
- previousScrollTop = component.getScrollTop();
- expect(component.getScrollLeft()).toBe(previousScrollLeft);
- previousScrollLeft = component.getScrollLeft();
- }
- function assertScrolledUp() {
- expect(component.getScrollTop()).toBeLessThan(previousScrollTop);
- previousScrollTop = component.getScrollTop();
- expect(component.getScrollLeft()).toBe(previousScrollLeft);
- previousScrollLeft = component.getScrollLeft();
- }
- component.didMouseDownOnLineNumberGutter({
- detail: 1,
- button: 0,
- clientX: 0,
- clientY: 100
- });
- const {
- didDrag
- } = component.handleMouseDragUntilMouseUp.argsForCall[0][0];
- didDrag({ clientX: 199, clientY: 199 });
- assertScrolledDown();
- didDrag({ clientX: 199, clientY: 199 });
- assertScrolledDown();
- didDrag({ clientX: 199, clientY: 199 });
- assertScrolledDown();
- didDrag({
- clientX: component.getGutterContainerWidth() + 1,
- clientY: 1
- });
- assertScrolledUp();
- didDrag({
- clientX: component.getGutterContainerWidth() + 1,
- clientY: 1
- });
- assertScrolledUp();
- didDrag({
- clientX: component.getGutterContainerWidth() + 1,
- clientY: 1
- });
- assertScrolledUp();
- // Don't artificially update scroll measurements beyond the minimum or
- // maximum possible scroll positions
- expect(component.getScrollTop()).toBe(0);
- expect(component.getScrollLeft()).toBe(0);
- didDrag({
- clientX: component.getGutterContainerWidth() + 1,
- clientY: 1
- });
- expect(component.getScrollTop()).toBe(0);
- expect(component.getScrollLeft()).toBe(0);
- const maxScrollTop = component.getMaxScrollTop();
- const maxScrollLeft = component.getMaxScrollLeft();
- setScrollTop(component, maxScrollTop);
- await setScrollLeft(component, maxScrollLeft);
- didDrag({ clientX: 199, clientY: 199 });
- didDrag({ clientX: 199, clientY: 199 });
- didDrag({ clientX: 199, clientY: 199 });
- expect(component.getScrollTop()).toBeNear(maxScrollTop);
- expect(component.getScrollLeft()).toBeNear(maxScrollLeft);
- });
- });
- describe('on the scrollbars', () => {
- it('delegates the mousedown events to the parent component unless the mousedown was on the actual scrollbar', async () => {
- const { component, editor } = buildComponent({ height: 100 });
- await setEditorWidthInCharacters(component, 6);
- const verticalScrollbar = component.refs.verticalScrollbar;
- const horizontalScrollbar = component.refs.horizontalScrollbar;
- const leftEdgeOfVerticalScrollbar =
- verticalScrollbar.element.getBoundingClientRect().right -
- verticalScrollbarWidth;
- const topEdgeOfHorizontalScrollbar =
- horizontalScrollbar.element.getBoundingClientRect().bottom -
- horizontalScrollbarHeight;
- verticalScrollbar.didMouseDown({
- button: 0,
- detail: 1,
- clientY: clientTopForLine(component, 4),
- clientX: leftEdgeOfVerticalScrollbar
- });
- expect(editor.getCursorScreenPosition()).toEqual([0, 0]);
- verticalScrollbar.didMouseDown({
- button: 0,
- detail: 1,
- clientY: clientTopForLine(component, 4),
- clientX: leftEdgeOfVerticalScrollbar - 1
- });
- expect(editor.getCursorScreenPosition()).toEqual([4, 6]);
- horizontalScrollbar.didMouseDown({
- button: 0,
- detail: 1,
- clientY: topEdgeOfHorizontalScrollbar,
- clientX: component.refs.content.getBoundingClientRect().left
- });
- expect(editor.getCursorScreenPosition()).toEqual([4, 6]);
- horizontalScrollbar.didMouseDown({
- button: 0,
- detail: 1,
- clientY: topEdgeOfHorizontalScrollbar - 1,
- clientX: component.refs.content.getBoundingClientRect().left
- });
- expect(editor.getCursorScreenPosition()).toEqual([4, 0]);
- });
- });
- });
- describe('paste event', () => {
- it("prevents the browser's default processing for the event on Linux", () => {
- const { component } = buildComponent({ platform: 'linux' });
- const event = { preventDefault: () => {} };
- spyOn(event, 'preventDefault');
- component.didPaste(event);
- expect(event.preventDefault).toHaveBeenCalled();
- });
- });
- describe('keyboard input', () => {
- it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => {
- const { editor, component } = buildComponent({
- text: '',
- chromeVersion: 57
- });
- editor.insertText('x');
- editor.setCursorBufferPosition([0, 1]);
- // Simulate holding the A key to open the press-and-hold menu,
- // then closing it via ESC.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeydown({ code: 'KeyA' });
- component.didKeydown({ code: 'KeyA' });
- component.didKeyup({ code: 'KeyA' });
- component.didKeydown({ code: 'Escape' });
- component.didKeyup({ code: 'Escape' });
- expect(editor.getText()).toBe('xa');
- // Ensure another "a" can be typed correctly.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeyup({ code: 'KeyA' });
- expect(editor.getText()).toBe('xaa');
- editor.undo();
- expect(editor.getText()).toBe('x');
- // Simulate holding the A key to open the press-and-hold menu,
- // then selecting an alternative by typing a number.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeydown({ code: 'KeyA' });
- component.didKeydown({ code: 'KeyA' });
- component.didKeyup({ code: 'KeyA' });
- component.didKeydown({ code: 'Digit2' });
- component.didKeyup({ code: 'Digit2' });
- component.didTextInput({
- data: 'á',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- expect(editor.getText()).toBe('xá');
- // Ensure another "a" can be typed correctly.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeyup({ code: 'KeyA' });
- expect(editor.getText()).toBe('xáa');
- editor.undo();
- expect(editor.getText()).toBe('x');
- // Simulate holding the A key to open the press-and-hold menu,
- // then selecting an alternative by clicking on it.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeydown({ code: 'KeyA' });
- component.didKeydown({ code: 'KeyA' });
- component.didKeyup({ code: 'KeyA' });
- component.didTextInput({
- data: 'á',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- expect(editor.getText()).toBe('xá');
- // Ensure another "a" can be typed correctly.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeyup({ code: 'KeyA' });
- expect(editor.getText()).toBe('xáa');
- editor.undo();
- expect(editor.getText()).toBe('x');
- // Simulate holding the A key to open the press-and-hold menu,
- // cycling through the alternatives with the arrows, then selecting one of them with Enter.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeydown({ code: 'KeyA' });
- component.didKeydown({ code: 'KeyA' });
- component.didKeyup({ code: 'KeyA' });
- component.didKeydown({ code: 'ArrowRight' });
- component.didCompositionStart({ data: '' });
- component.didCompositionUpdate({ data: 'à' });
- component.didKeyup({ code: 'ArrowRight' });
- expect(editor.getText()).toBe('xà');
- component.didKeydown({ code: 'ArrowRight' });
- component.didCompositionUpdate({ data: 'á' });
- component.didKeyup({ code: 'ArrowRight' });
- expect(editor.getText()).toBe('xá');
- component.didKeydown({ code: 'Enter' });
- component.didCompositionUpdate({ data: 'á' });
- component.didTextInput({
- data: 'á',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didCompositionEnd({
- data: 'á',
- target: component.refs.cursorsAndInput.refs.hiddenInput
- });
- component.didKeyup({ code: 'Enter' });
- expect(editor.getText()).toBe('xá');
- // Ensure another "a" can be typed correctly.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeyup({ code: 'KeyA' });
- expect(editor.getText()).toBe('xáa');
- editor.undo();
- expect(editor.getText()).toBe('x');
- // Simulate holding the A key to open the press-and-hold menu,
- // cycling through the alternatives with the arrows, then closing it via ESC.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeydown({ code: 'KeyA' });
- component.didKeydown({ code: 'KeyA' });
- component.didKeyup({ code: 'KeyA' });
- component.didKeydown({ code: 'ArrowRight' });
- component.didCompositionStart({ data: '' });
- component.didCompositionUpdate({ data: 'à' });
- component.didKeyup({ code: 'ArrowRight' });
- expect(editor.getText()).toBe('xà');
- component.didKeydown({ code: 'ArrowRight' });
- component.didCompositionUpdate({ data: 'á' });
- component.didKeyup({ code: 'ArrowRight' });
- expect(editor.getText()).toBe('xá');
- component.didKeydown({ code: 'Escape' });
- component.didCompositionUpdate({ data: 'a' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didCompositionEnd({
- data: 'a',
- target: component.refs.cursorsAndInput.refs.hiddenInput
- });
- component.didKeyup({ code: 'Escape' });
- expect(editor.getText()).toBe('xa');
- // Ensure another "a" can be typed correctly.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeyup({ code: 'KeyA' });
- expect(editor.getText()).toBe('xaa');
- editor.undo();
- expect(editor.getText()).toBe('x');
- // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key,
- // cycling through the alternatives with the arrows, then closing it via ESC.
- component.didKeydown({ code: 'KeyO' });
- component.didKeypress({ code: 'KeyO' });
- component.didTextInput({
- data: 'o',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeyup({ code: 'KeyO' });
- component.didKeydown({ code: 'KeyA' });
- component.didKeydown({ code: 'KeyA' });
- component.didKeydown({ code: 'ArrowRight' });
- component.didCompositionStart({ data: '' });
- component.didCompositionUpdate({ data: 'à' });
- component.didKeyup({ code: 'ArrowRight' });
- expect(editor.getText()).toBe('xoà');
- component.didKeydown({ code: 'ArrowRight' });
- component.didCompositionUpdate({ data: 'á' });
- component.didKeyup({ code: 'ArrowRight' });
- expect(editor.getText()).toBe('xoá');
- component.didKeydown({ code: 'Escape' });
- component.didCompositionUpdate({ data: 'a' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didCompositionEnd({
- data: 'a',
- target: component.refs.cursorsAndInput.refs.hiddenInput
- });
- component.didKeyup({ code: 'Escape' });
- expect(editor.getText()).toBe('xoa');
- // Ensure another "a" can be typed correctly.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeyup({ code: 'KeyA' });
- editor.undo();
- expect(editor.getText()).toBe('x');
- // Simulate holding the A key to open the press-and-hold menu,
- // cycling through the alternatives with the arrows, then closing it by changing focus.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeydown({ code: 'KeyA' });
- component.didKeydown({ code: 'KeyA' });
- component.didKeyup({ code: 'KeyA' });
- component.didKeydown({ code: 'ArrowRight' });
- component.didCompositionStart({ data: '' });
- component.didCompositionUpdate({ data: 'à' });
- component.didKeyup({ code: 'ArrowRight' });
- expect(editor.getText()).toBe('xà');
- component.didKeydown({ code: 'ArrowRight' });
- component.didCompositionUpdate({ data: 'á' });
- component.didKeyup({ code: 'ArrowRight' });
- expect(editor.getText()).toBe('xá');
- component.didCompositionUpdate({ data: 'á' });
- component.didTextInput({
- data: 'á',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didCompositionEnd({
- data: 'á',
- target: component.refs.cursorsAndInput.refs.hiddenInput
- });
- expect(editor.getText()).toBe('xá');
- // Ensure another "a" can be typed correctly.
- component.didKeydown({ code: 'KeyA' });
- component.didKeypress({ code: 'KeyA' });
- component.didTextInput({
- data: 'a',
- stopPropagation: () => {},
- preventDefault: () => {}
- });
- component.didKeyup({ code: 'KeyA' });
- expect(editor.getText()).toBe('xáa');
- editor.undo();
- expect(editor.getText()).toBe('x');
- });
- });
- describe('styling changes', () => {
- /**
- * TODO: FAILING TEST - This test fails with the following output:
- * Expected 7.234375 not to be 7.234375.
- * Expected 7.234375 not to be 7.234375.
- * Expected 7.234375 not to be 7.234375.
- */
- xit('updates the rendered content based on new measurements when the font dimensions change', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 1,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 3);
- editor.setCursorScreenPosition([1, 29], { autoscroll: false });
- await component.getNextUpdatePromise();
- let cursorNode = element.querySelector('.cursor');
- const initialBaseCharacterWidth = editor.getDefaultCharWidth();
- const initialDoubleCharacterWidth = editor.getDoubleWidthCharWidth();
- const initialHalfCharacterWidth = editor.getHalfWidthCharWidth();
- const initialKoreanCharacterWidth = editor.getKoreanCharWidth();
- const initialRenderedLineCount = queryOnScreenLineElements(element)
- .length;
- const initialFontSize = parseInt(getComputedStyle(element).fontSize);
- expect(initialKoreanCharacterWidth).toBeDefined();
- expect(initialDoubleCharacterWidth).toBeDefined();
- expect(initialHalfCharacterWidth).toBeDefined();
- expect(initialBaseCharacterWidth).toBeDefined();
- expect(initialDoubleCharacterWidth).not.toBe(initialBaseCharacterWidth);
- expect(initialHalfCharacterWidth).not.toBe(initialBaseCharacterWidth);
- expect(initialKoreanCharacterWidth).not.toBe(initialBaseCharacterWidth);
- verifyCursorPosition(component, cursorNode, 1, 29);
- element.style.fontSize = initialFontSize - 5 + 'px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(editor.getDefaultCharWidth()).toBeLessThan(
- initialBaseCharacterWidth
- );
- expect(editor.getDoubleWidthCharWidth()).toBeLessThan(
- initialDoubleCharacterWidth
- );
- expect(editor.getHalfWidthCharWidth()).toBeLessThan(
- initialHalfCharacterWidth
- );
- expect(editor.getKoreanCharWidth()).toBeLessThan(
- initialKoreanCharacterWidth
- );
- expect(queryOnScreenLineElements(element).length).toBeGreaterThan(
- initialRenderedLineCount
- );
- verifyCursorPosition(component, cursorNode, 1, 29);
- element.style.fontSize = initialFontSize + 10 + 'px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(editor.getDefaultCharWidth()).toBeGreaterThan(
- initialBaseCharacterWidth
- );
- expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(
- initialDoubleCharacterWidth
- );
- expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(
- initialHalfCharacterWidth
- );
- expect(editor.getKoreanCharWidth()).toBeGreaterThan(
- initialKoreanCharacterWidth
- );
- expect(queryOnScreenLineElements(element).length).toBeLessThan(
- initialRenderedLineCount
- );
- verifyCursorPosition(component, cursorNode, 1, 29);
- });
- it('maintains the scrollTopRow and scrollLeftColumn when the font size changes', async () => {
- const { component, element } = buildComponent({
- rowsPerTile: 1,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 3);
- await setEditorWidthInCharacters(component, 20);
- component.setScrollTopRow(4);
- component.setScrollLeftColumn(10);
- await component.getNextUpdatePromise();
- const initialFontSize = parseInt(getComputedStyle(element).fontSize);
- element.style.fontSize = initialFontSize - 5 + 'px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(component.getScrollTopRow()).toBe(4);
- element.style.fontSize = initialFontSize + 5 + 'px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- expect(component.getScrollTopRow()).toBe(4);
- });
- it('gracefully handles the editor being hidden after a styling change', async () => {
- const { component, element } = buildComponent({
- autoHeight: false
- });
- element.style.fontSize =
- parseInt(getComputedStyle(element).fontSize) + 5 + 'px';
- TextEditor.didUpdateStyles();
- element.style.display = 'none';
- await component.getNextUpdatePromise();
- });
- it('does not throw an exception when the editor is soft-wrapped and changing the font size changes also the longest screen line', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 3,
- autoHeight: false
- });
- editor.setText(
- 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do\n' +
- 'eiusmod tempor incididunt ut labore et dolore magna' +
- 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation'
- );
- editor.setSoftWrapped(true);
- await setEditorHeightInLines(component, 2);
- await setEditorWidthInCharacters(component, 56);
- await setScrollTop(component, 3 * component.getLineHeight());
- element.style.fontSize = '20px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- });
- it('updates the width of the lines div based on the longest screen line', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 1,
- autoHeight: false
- });
- editor.setText(
- 'Lorem ipsum dolor sit\n' +
- 'amet, consectetur adipisicing\n' +
- 'elit, sed do\n' +
- 'eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation'
- );
- await setEditorHeightInLines(component, 2);
- element.style.fontSize = '20px';
- TextEditor.didUpdateStyles();
- await component.getNextUpdatePromise();
- // Capture the width of the lines before requesting the width of
- // longest line, because making that request forces a DOM update
- const actualWidth = element.querySelector('.lines').style.width;
- const expectedWidth = Math.ceil(
- component.pixelPositionForScreenPosition(Point(3, Infinity)).left +
- component.getBaseCharacterWidth()
- );
- expect(actualWidth).toBe(expectedWidth + 'px');
- });
- });
- describe('synchronous updates', () => {
- let editorElementWasUpdatedSynchronously;
- beforeEach(() => {
- editorElementWasUpdatedSynchronously =
- TextEditorElement.prototype.updatedSynchronously;
- });
- afterEach(() => {
- TextEditorElement.prototype.setUpdatedSynchronously(
- editorElementWasUpdatedSynchronously
- );
- });
- it('updates synchronously when updatedSynchronously is true', () => {
- const editor = buildEditor();
- const { element } = new TextEditorComponent({
- model: editor,
- updatedSynchronously: true
- });
- jasmine.attachToDOM(element);
- editor.setText('Lorem ipsum dolor');
- expect(
- queryOnScreenLineElements(element).map(l => l.textContent)
- ).toEqual([editor.lineTextForScreenRow(0)]);
- });
- it('does not throw an exception on attachment when setting the soft-wrap column', () => {
- const { element, editor } = buildComponent({
- width: 435,
- attach: false,
- updatedSynchronously: true
- });
- editor.setSoftWrapped(true);
- spyOn(window, 'onerror').andCallThrough();
- jasmine.attachToDOM(element); // should not throw an exception
- expect(window.onerror).not.toHaveBeenCalled();
- });
- it('updates synchronously when creating a component via TextEditor and TextEditorElement.prototype.updatedSynchronously is true', () => {
- TextEditorElement.prototype.setUpdatedSynchronously(true);
- const editor = buildEditor();
- const element = editor.element;
- jasmine.attachToDOM(element);
- editor.setText('Lorem ipsum dolor');
- expect(
- queryOnScreenLineElements(element).map(l => l.textContent)
- ).toEqual([editor.lineTextForScreenRow(0)]);
- });
- it('measures dimensions synchronously when measureDimensions is called on the component', () => {
- TextEditorElement.prototype.setUpdatedSynchronously(true);
- const editor = buildEditor({ autoHeight: false });
- const element = editor.element;
- jasmine.attachToDOM(element);
- element.style.height = '100px';
- expect(element.component.getClientContainerHeight()).not.toBe(100);
- element.component.measureDimensions();
- expect(element.component.getClientContainerHeight()).toBe(100);
- });
- });
- describe('pixelPositionForScreenPosition(point)', () => {
- it('returns the pixel position for the given point, regardless of whether or not it is currently on screen', async () => {
- const { component, editor } = buildComponent({
- rowsPerTile: 2,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 3);
- await setScrollTop(component, 3 * component.getLineHeight());
- const { component: referenceComponent } = buildComponent();
- const referenceContentRect = referenceComponent.refs.content.getBoundingClientRect();
- {
- const { top, left } = component.pixelPositionForScreenPosition({
- row: 0,
- column: 0
- });
- expect(top).toBe(
- clientTopForLine(referenceComponent, 0) - referenceContentRect.top
- );
- expect(left).toBe(
- clientLeftForCharacter(referenceComponent, 0, 0) -
- referenceContentRect.left
- );
- }
- {
- const { top, left } = component.pixelPositionForScreenPosition({
- row: 0,
- column: 5
- });
- expect(top).toBe(
- clientTopForLine(referenceComponent, 0) - referenceContentRect.top
- );
- expect(left).toBeNear(
- clientLeftForCharacter(referenceComponent, 0, 5) -
- referenceContentRect.left
- );
- }
- {
- const { top, left } = component.pixelPositionForScreenPosition({
- row: 12,
- column: 1
- });
- expect(top).toBeNear(
- clientTopForLine(referenceComponent, 12) - referenceContentRect.top
- );
- expect(left).toBeNear(
- clientLeftForCharacter(referenceComponent, 12, 1) -
- referenceContentRect.left
- );
- }
- // Measuring a currently rendered line while an autoscroll that causes
- // that line to go off-screen is in progress.
- {
- editor.setCursorScreenPosition([10, 0]);
- const { top, left } = component.pixelPositionForScreenPosition({
- row: 3,
- column: 5
- });
- expect(top).toBeNear(
- clientTopForLine(referenceComponent, 3) - referenceContentRect.top
- );
- expect(left).toBeNear(
- clientLeftForCharacter(referenceComponent, 3, 5) -
- referenceContentRect.left
- );
- }
- });
- it('does not get the component into an inconsistent state when the model has unflushed changes (regression)', async () => {
- const { component, editor } = buildComponent({
- rowsPerTile: 2,
- autoHeight: false,
- text: ''
- });
- await setEditorHeightInLines(component, 10);
- const updatePromise = editor.getBuffer().append('hi\n');
- component.screenPositionForPixelPosition({ top: 800, left: 1 });
- await updatePromise;
- });
- it('does not shift cursors downward or render off-screen content when measuring off-screen lines (regression)', async () => {
- const { component, element } = buildComponent({
- rowsPerTile: 2,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 3);
- component.pixelPositionForScreenPosition({
- row: 12,
- column: 1
- });
- expect(element.querySelector('.cursor').getBoundingClientRect().top).toBe(
- component.refs.lineTiles.getBoundingClientRect().top
- );
- expect(
- element.querySelector('.line[data-screen-row="12"]').style.visibility
- ).toBe('hidden');
- // Ensure previously measured off screen lines don't have any weird
- // styling when they come on screen in the next frame
- await setEditorHeightInLines(component, 13);
- const previouslyMeasuredLineElement = element.querySelector(
- '.line[data-screen-row="12"]'
- );
- expect(previouslyMeasuredLineElement.style.display).toBe('');
- expect(previouslyMeasuredLineElement.style.visibility).toBe('');
- });
- });
- describe('screenPositionForPixelPosition', () => {
- it('returns the screen position for the given pixel position, regardless of whether or not it is currently on screen', async () => {
- const { component, editor } = buildComponent({
- rowsPerTile: 2,
- autoHeight: false
- });
- await setEditorHeightInLines(component, 3);
- await setScrollTop(component, 3 * component.getLineHeight());
- const { component: referenceComponent } = buildComponent();
- {
- const pixelPosition = referenceComponent.pixelPositionForScreenPosition(
- { row: 0, column: 0 }
- );
- pixelPosition.top += component.getLineHeight() / 3;
- pixelPosition.left += component.getBaseCharacterWidth() / 3;
- expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual(
- [0, 0]
- );
- }
- {
- const pixelPosition = referenceComponent.pixelPositionForScreenPosition(
- { row: 0, column: 5 }
- );
- pixelPosition.top += component.getLineHeight() / 3;
- pixelPosition.left += component.getBaseCharacterWidth() / 3;
- expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual(
- [0, 5]
- );
- }
- {
- const pixelPosition = referenceComponent.pixelPositionForScreenPosition(
- { row: 5, column: 7 }
- );
- pixelPosition.top += component.getLineHeight() / 3;
- pixelPosition.left += component.getBaseCharacterWidth() / 3;
- expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual(
- [5, 7]
- );
- }
- {
- const pixelPosition = referenceComponent.pixelPositionForScreenPosition(
- { row: 12, column: 1 }
- );
- pixelPosition.top += component.getLineHeight() / 3;
- pixelPosition.left += component.getBaseCharacterWidth() / 3;
- expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual(
- [12, 1]
- );
- }
- // Measuring a currently rendered line while an autoscroll that causes
- // that line to go off-screen is in progress.
- {
- const pixelPosition = referenceComponent.pixelPositionForScreenPosition(
- { row: 3, column: 4 }
- );
- pixelPosition.top += component.getLineHeight() / 3;
- pixelPosition.left += component.getBaseCharacterWidth() / 3;
- editor.setCursorBufferPosition([10, 0]);
- expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual(
- [3, 4]
- );
- }
- });
- });
- describe('model methods that delegate to the component / element', () => {
- it('delegates setHeight and getHeight to the component', async () => {
- const { component, editor } = buildComponent({
- autoHeight: false
- });
- spyOn(Grim, 'deprecate');
- expect(editor.getHeight()).toBe(component.getScrollContainerHeight());
- expect(Grim.deprecate.callCount).toBe(1);
- editor.setHeight(100);
- await component.getNextUpdatePromise();
- expect(component.getScrollContainerHeight()).toBe(100);
- expect(Grim.deprecate.callCount).toBe(2);
- });
- it('delegates setWidth and getWidth to the component', async () => {
- const { component, editor } = buildComponent();
- spyOn(Grim, 'deprecate');
- expect(editor.getWidth()).toBe(component.getScrollContainerWidth());
- expect(Grim.deprecate.callCount).toBe(1);
- editor.setWidth(100);
- await component.getNextUpdatePromise();
- expect(component.getScrollContainerWidth()).toBe(100);
- expect(Grim.deprecate.callCount).toBe(2);
- });
- it('delegates getFirstVisibleScreenRow, getLastVisibleScreenRow, and getVisibleRowRange to the component', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 3,
- autoHeight: false
- });
- element.style.height = 4 * component.measurements.lineHeight + 'px';
- await component.getNextUpdatePromise();
- await setScrollTop(component, 5 * component.getLineHeight());
- expect(editor.getFirstVisibleScreenRow()).toBe(
- component.getFirstVisibleRow()
- );
- expect(editor.getLastVisibleScreenRow()).toBe(
- component.getLastVisibleRow()
- );
- expect(editor.getVisibleRowRange()).toEqual([
- component.getFirstVisibleRow(),
- component.getLastVisibleRow()
- ]);
- });
- it('assigns scrollTop on the component when calling setFirstVisibleScreenRow', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 3,
- autoHeight: false
- });
- element.style.height =
- 4 * component.measurements.lineHeight +
- horizontalScrollbarHeight +
- 'px';
- await component.getNextUpdatePromise();
- expect(component.getMaxScrollTop() / component.getLineHeight()).toBeNear(
- 9
- );
- expect(component.refs.verticalScrollbar.element.scrollTop).toBe(
- 0 * component.getLineHeight()
- );
- editor.setFirstVisibleScreenRow(1);
- expect(component.getFirstVisibleRow()).toBe(1);
- await component.getNextUpdatePromise();
- expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear(
- 1 * component.getLineHeight()
- );
- editor.setFirstVisibleScreenRow(5);
- expect(component.getFirstVisibleRow()).toBe(5);
- await component.getNextUpdatePromise();
- expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear(
- 5 * component.getLineHeight()
- );
- editor.setFirstVisibleScreenRow(11);
- expect(component.getFirstVisibleRow()).toBe(9);
- await component.getNextUpdatePromise();
- expect(component.refs.verticalScrollbar.element.scrollTop).toBeNear(
- 9 * component.getLineHeight()
- );
- });
- it('delegates setFirstVisibleScreenColumn and getFirstVisibleScreenColumn to the component', async () => {
- const { component, element, editor } = buildComponent({
- rowsPerTile: 3,
- autoHeight: false
- });
- element.style.width = 30 * component.getBaseCharacterWidth() + 'px';
- await component.getNextUpdatePromise();
- expect(editor.getFirstVisibleScreenColumn()).toBe(0);
- expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(0);
- setScrollLeft(component, 5.5 * component.getBaseCharacterWidth());
- expect(editor.getFirstVisibleScreenColumn()).toBe(5);
- await component.getNextUpdatePromise();
- expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeCloseTo(
- 5.5 * component.getBaseCharacterWidth(),
- -1
- );
- editor.setFirstVisibleScreenColumn(12);
- expect(component.getScrollLeft()).toBeCloseTo(
- 12 * component.getBaseCharacterWidth(),
- -1
- );
- await component.getNextUpdatePromise();
- expect(component.refs.horizontalScrollbar.element.scrollLeft).toBeCloseTo(
- 12 * component.getBaseCharacterWidth(),
- -1
- );
- });
- });
- describe('handleMouseDragUntilMouseUp', () => {
- it('repeatedly schedules `didDrag` calls on new animation frames after moving the mouse, and calls `didStopDragging` on mouseup', async () => {
- const { component } = buildComponent();
- let dragEvents;
- let dragging = false;
- component.handleMouseDragUntilMouseUp({
- didDrag: event => {
- dragging = true;
- dragEvents.push(event);
- },
- didStopDragging: () => {
- dragging = false;
- }
- });
- expect(dragging).toBe(false);
- dragEvents = [];
- const moveEvent1 = new MouseEvent('mousemove');
- window.dispatchEvent(moveEvent1);
- expect(dragging).toBe(false);
- await getNextAnimationFramePromise();
- expect(dragging).toBe(true);
- expect(dragEvents).toEqual([moveEvent1]);
- await getNextAnimationFramePromise();
- expect(dragging).toBe(true);
- expect(dragEvents).toEqual([moveEvent1, moveEvent1]);
- dragEvents = [];
- const moveEvent2 = new MouseEvent('mousemove');
- window.dispatchEvent(moveEvent2);
- expect(dragging).toBe(true);
- expect(dragEvents).toEqual([]);
- await getNextAnimationFramePromise();
- expect(dragging).toBe(true);
- expect(dragEvents).toEqual([moveEvent2]);
- await getNextAnimationFramePromise();
- expect(dragging).toBe(true);
- expect(dragEvents).toEqual([moveEvent2, moveEvent2]);
- dragEvents = [];
- window.dispatchEvent(new MouseEvent('mouseup'));
- expect(dragging).toBe(false);
- expect(dragEvents).toEqual([]);
- window.dispatchEvent(new MouseEvent('mousemove'));
- await getNextAnimationFramePromise();
- expect(dragging).toBe(false);
- expect(dragEvents).toEqual([]);
- });
- it('calls `didStopDragging` if the user interacts with the keyboard while dragging', async () => {
- const { component, editor } = buildComponent();
- let dragging = false;
- function startDragging() {
- component.handleMouseDragUntilMouseUp({
- didDrag: event => {
- dragging = true;
- },
- didStopDragging: () => {
- dragging = false;
- }
- });
- }
- startDragging();
- window.dispatchEvent(new MouseEvent('mousemove'));
- await getNextAnimationFramePromise();
- expect(dragging).toBe(true);
- // Buffer changes don't cause dragging to be stopped.
- editor.insertText('X');
- expect(dragging).toBe(true);
- // Keyboard interaction prevents users from dragging further.
- component.didKeydown({ code: 'KeyX' });
- expect(dragging).toBe(false);
- window.dispatchEvent(new MouseEvent('mousemove'));
- await getNextAnimationFramePromise();
- expect(dragging).toBe(false);
- // Pressing a modifier key does not terminate dragging, (to ensure we can add new selections with the mouse)
- startDragging();
- window.dispatchEvent(new MouseEvent('mousemove'));
- await getNextAnimationFramePromise();
- expect(dragging).toBe(true);
- component.didKeydown({ key: 'Control' });
- component.didKeydown({ key: 'Alt' });
- component.didKeydown({ key: 'Shift' });
- component.didKeydown({ key: 'Meta' });
- expect(dragging).toBe(true);
- });
- function getNextAnimationFramePromise() {
- return new Promise(resolve => requestAnimationFrame(resolve));
- }
- });
- });
- function buildEditor(params = {}) {
- const text = params.text != null ? params.text : SAMPLE_TEXT;
- const buffer = new TextBuffer({ text });
- const editorParams = { buffer, readOnly: params.readOnly };
- if (params.height != null) params.autoHeight = false;
- for (const paramName of [
- 'mini',
- 'autoHeight',
- 'autoWidth',
- 'lineNumberGutterVisible',
- 'showLineNumbers',
- 'placeholderText',
- 'softWrapped',
- 'scrollSensitivity'
- ]) {
- if (params[paramName] != null) editorParams[paramName] = params[paramName];
- }
- atom.grammars.autoAssignLanguageMode(buffer);
- const editor = new TextEditor(editorParams);
- editor.testAutoscrollRequests = [];
- editor.onDidRequestAutoscroll(request => {
- editor.testAutoscrollRequests.push(request);
- });
- editors.push(editor);
- return editor;
- }
- function buildComponent(params = {}) {
- const editor = params.editor || buildEditor(params);
- const component = new TextEditorComponent({
- model: editor,
- rowsPerTile: params.rowsPerTile,
- updatedSynchronously: params.updatedSynchronously || false,
- platform: params.platform,
- chromeVersion: params.chromeVersion
- });
- const { element } = component;
- if (!editor.getAutoHeight()) {
- element.style.height = params.height ? params.height + 'px' : '600px';
- }
- if (!editor.getAutoWidth()) {
- element.style.width = params.width ? params.width + 'px' : '800px';
- }
- if (params.attach !== false) jasmine.attachToDOM(element);
- return { component, element, editor };
- }
- function getEditorWidthInBaseCharacters(component) {
- return Math.round(
- component.getScrollContainerWidth() / component.getBaseCharacterWidth()
- );
- }
- async function setEditorHeightInLines(component, heightInLines) {
- component.element.style.height =
- component.getLineHeight() * heightInLines + 'px';
- await component.getNextUpdatePromise();
- }
- async function setEditorWidthInCharacters(component, widthInCharacters) {
- component.element.style.width =
- component.getGutterContainerWidth() +
- widthInCharacters * component.measurements.baseCharacterWidth +
- verticalScrollbarWidth +
- 'px';
- await component.getNextUpdatePromise();
- }
- function verifyCursorPosition(component, cursorNode, row, column) {
- const rect = cursorNode.getBoundingClientRect();
- expect(Math.round(rect.top)).toBeNear(clientTopForLine(component, row));
- expect(Math.round(rect.left)).toBe(
- Math.round(clientLeftForCharacter(component, row, column))
- );
- }
- function clientTopForLine(component, row) {
- return lineNodeForScreenRow(component, row).getBoundingClientRect().top;
- }
- function clientLeftForCharacter(component, row, column) {
- const textNodes = textNodesForScreenRow(component, row);
- let textNodeStartColumn = 0;
- for (const textNode of textNodes) {
- const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length;
- if (column < textNodeEndColumn) {
- const range = document.createRange();
- range.setStart(textNode, column - textNodeStartColumn);
- range.setEnd(textNode, column - textNodeStartColumn);
- return range.getBoundingClientRect().left;
- }
- textNodeStartColumn = textNodeEndColumn;
- }
- const lastTextNode = textNodes[textNodes.length - 1];
- const range = document.createRange();
- range.setStart(lastTextNode, 0);
- range.setEnd(lastTextNode, lastTextNode.textContent.length);
- return range.getBoundingClientRect().right;
- }
- function clientPositionForCharacter(component, row, column) {
- return {
- clientX: clientLeftForCharacter(component, row, column),
- clientY: clientTopForLine(component, row)
- };
- }
- function lineNumberNodeForScreenRow(component, row) {
- const gutterElement =
- component.refs.gutterContainer.refs.lineNumberGutter.element;
- const tileStartRow = component.tileStartRowForRow(row);
- const tileIndex = component.renderedTileStartRows.indexOf(tileStartRow);
- return gutterElement.children[tileIndex + 1].children[row - tileStartRow];
- }
- function lineNodeForScreenRow(component, row) {
- const renderedScreenLine = component.renderedScreenLineForRow(row);
- return component.lineComponentsByScreenLineId.get(renderedScreenLine.id)
- .element;
- }
- function textNodesForScreenRow(component, row) {
- const screenLine = component.renderedScreenLineForRow(row);
- return component.lineComponentsByScreenLineId.get(screenLine.id).textNodes;
- }
- function setScrollTop(component, scrollTop) {
- component.setScrollTop(scrollTop);
- component.scheduleUpdate();
- return component.getNextUpdatePromise();
- }
- function setScrollLeft(component, scrollLeft) {
- component.setScrollLeft(scrollLeft);
- component.scheduleUpdate();
- return component.getNextUpdatePromise();
- }
- function getHorizontalScrollbarHeight(component) {
- const element = component.refs.horizontalScrollbar.element;
- return element.offsetHeight - element.clientHeight;
- }
- function getVerticalScrollbarWidth(component) {
- const element = component.refs.verticalScrollbar.element;
- return element.offsetWidth - element.clientWidth;
- }
- function assertDocumentFocused() {
- if (!document.hasFocus()) {
- throw new Error('The document needs to be focused to run this test');
- }
- }
- function getElementHeight(element) {
- const topRuler = document.createElement('div');
- const bottomRuler = document.createElement('div');
- let height;
- if (document.body.contains(element)) {
- element.parentElement.insertBefore(topRuler, element);
- element.parentElement.insertBefore(bottomRuler, element.nextSibling);
- height = bottomRuler.offsetTop - topRuler.offsetTop;
- } else {
- jasmine.attachToDOM(topRuler);
- jasmine.attachToDOM(element);
- jasmine.attachToDOM(bottomRuler);
- height = bottomRuler.offsetTop - topRuler.offsetTop;
- element.remove();
- }
- topRuler.remove();
- bottomRuler.remove();
- return height;
- }
- function queryOnScreenLineNumberElements(element) {
- return Array.from(element.querySelectorAll('.line-number:not(.dummy)'));
- }
- function queryOnScreenLineElements(element) {
- return Array.from(
- element.querySelectorAll('.line:not(.dummy):not([data-off-screen])')
- );
- }
|