DefaultTextEditor.js 121 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754
  1. /*
  2. * Copyright (C) 2011 Google Inc. All rights reserved.
  3. * Copyright (C) 2010 Apple Inc. All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are
  7. * met:
  8. *
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above
  12. * copyright notice, this list of conditions and the following disclaimer
  13. * in the documentation and/or other materials provided with the
  14. * distribution.
  15. * * Neither the name of Google Inc. nor the names of its
  16. * contributors may be used to endorse or promote products derived from
  17. * this software without specific prior written permission.
  18. *
  19. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  20. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  21. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  22. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  23. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  24. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  25. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  26. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  27. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  28. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  29. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30. */
  31. /**
  32. * @constructor
  33. * @extends {WebInspector.View}
  34. * @implements {WebInspector.TextEditor}
  35. * @param {?string} url
  36. * @param {WebInspector.TextEditorDelegate} delegate
  37. */
  38. WebInspector.DefaultTextEditor = function(url, delegate)
  39. {
  40. WebInspector.View.call(this);
  41. this._delegate = delegate;
  42. this._url = url;
  43. this.registerRequiredCSS("textEditor.css");
  44. this.element.className = "text-editor monospace";
  45. this.markAsLayoutBoundary();
  46. // Prevent middle-click pasting in the editor unless it is explicitly enabled for certain component.
  47. this.element.addEventListener("mouseup", preventDefaultOnMouseUp.bind(this), false);
  48. function preventDefaultOnMouseUp(event)
  49. {
  50. if (event.button === 1)
  51. event.consume(true);
  52. }
  53. this._textModel = new WebInspector.TextEditorModel();
  54. this._textModel.addEventListener(WebInspector.TextEditorModel.Events.TextChanged, this._textChanged, this);
  55. var syncScrollListener = this._syncScroll.bind(this);
  56. var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this);
  57. var syncLineHeightListener = this._syncLineHeight.bind(this);
  58. this._mainPanel = new WebInspector.TextEditorMainPanel(this._delegate, this._textModel, url, syncScrollListener, syncDecorationsForLineListener);
  59. this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener, syncLineHeightListener);
  60. this._mainPanel.element.addEventListener("scroll", this._handleScrollChanged.bind(this), false);
  61. this._gutterPanel.element.addEventListener("mousedown", this._onMouseDown.bind(this), true);
  62. // Explicitly enable middle-click pasting in the editor main panel.
  63. this._mainPanel.element.addEventListener("mouseup", consumeMouseUp.bind(this), false);
  64. function consumeMouseUp(event)
  65. {
  66. if (event.button === 1)
  67. event.consume(false);
  68. }
  69. this.element.appendChild(this._mainPanel.element);
  70. this.element.appendChild(this._gutterPanel.element);
  71. // Forward mouse wheel events from the unscrollable gutter to the main panel.
  72. function forwardWheelEvent(event)
  73. {
  74. var clone = document.createEvent("WheelEvent");
  75. clone.initWebKitWheelEvent(event.wheelDeltaX, event.wheelDeltaY,
  76. event.view,
  77. event.screenX, event.screenY,
  78. event.clientX, event.clientY,
  79. event.ctrlKey, event.altKey, event.shiftKey, event.metaKey);
  80. this._mainPanel.element.dispatchEvent(clone);
  81. }
  82. this._gutterPanel.element.addEventListener("mousewheel", forwardWheelEvent.bind(this), false);
  83. this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
  84. this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
  85. this._wordMovementController = new WebInspector.DefaultTextEditor.WordMovementController(this, this._textModel);
  86. this._registerShortcuts();
  87. }
  88. /**
  89. * @constructor
  90. * @param {WebInspector.TextRange} range
  91. * @param {string} text
  92. */
  93. WebInspector.DefaultTextEditor.EditInfo = function(range, text)
  94. {
  95. this.range = range;
  96. this.text = text;
  97. }
  98. WebInspector.DefaultTextEditor.prototype = {
  99. /**
  100. * @return {boolean}
  101. */
  102. isClean: function()
  103. {
  104. return this._textModel.isClean();
  105. },
  106. markClean: function()
  107. {
  108. this._textModel.markClean();
  109. },
  110. /**
  111. * @param {number} lineNumber
  112. * @param {number} column
  113. * @return {?{startColumn: number, endColumn: number, type: string}}
  114. */
  115. tokenAtTextPosition: function(lineNumber, column)
  116. {
  117. return this._mainPanel.tokenAtTextPosition(lineNumber, column);
  118. },
  119. /*
  120. * @param {number} lineNumber
  121. * @param {number} column
  122. * @return {?{x: number, y: number, height: number}}
  123. */
  124. cursorPositionToCoordinates: function(lineNumber, column)
  125. {
  126. return this._mainPanel.cursorPositionToCoordinates(lineNumber, column);
  127. },
  128. /**
  129. * @param {number} x
  130. * @param {number} y
  131. * @return {?WebInspector.TextRange}
  132. */
  133. coordinatesToCursorPosition: function(x, y)
  134. {
  135. return this._mainPanel.coordinatesToCursorPosition(x, y);
  136. },
  137. /**
  138. * @param {WebInspector.TextRange} range
  139. * @return {string}
  140. */
  141. copyRange: function(range)
  142. {
  143. return this._textModel.copyRange(range);
  144. },
  145. /**
  146. * @param {string} regex
  147. * @param {string} cssClass
  148. * @return {Object}
  149. */
  150. highlightRegex: function(regex, cssClass)
  151. {
  152. return this._mainPanel.highlightRegex(regex, cssClass);
  153. },
  154. /**
  155. * @param {Object} highlightDescriptor
  156. */
  157. removeHighlight: function(highlightDescriptor)
  158. {
  159. this._mainPanel.removeHighlight(highlightDescriptor);
  160. },
  161. /**
  162. * @param {WebInspector.TextRange} range
  163. * @param {string} cssClass
  164. * @return {Object}
  165. */
  166. highlightRange: function(range, cssClass)
  167. {
  168. return this._mainPanel.highlightRange(range, cssClass);
  169. },
  170. /**
  171. * @param {string} mimeType
  172. */
  173. set mimeType(mimeType)
  174. {
  175. this._mainPanel.mimeType = mimeType;
  176. },
  177. /**
  178. * @param {boolean} readOnly
  179. */
  180. setReadOnly: function(readOnly)
  181. {
  182. if (this._mainPanel.readOnly() === readOnly)
  183. return;
  184. this._mainPanel.setReadOnly(readOnly, this.isShowing());
  185. WebInspector.markBeingEdited(this.element, !readOnly);
  186. },
  187. /**
  188. * @return {boolean}
  189. */
  190. readOnly: function()
  191. {
  192. return this._mainPanel.readOnly();
  193. },
  194. /**
  195. * @return {Element}
  196. */
  197. defaultFocusedElement: function()
  198. {
  199. return this._mainPanel.defaultFocusedElement();
  200. },
  201. /**
  202. * @param {number} lineNumber
  203. */
  204. revealLine: function(lineNumber)
  205. {
  206. this._mainPanel.revealLine(lineNumber);
  207. },
  208. _onMouseDown: function(event)
  209. {
  210. var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
  211. if (!target)
  212. return;
  213. this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: target.lineNumber, event: event });
  214. },
  215. /**
  216. * @param {number} lineNumber
  217. * @param {boolean} disabled
  218. * @param {boolean} conditional
  219. */
  220. addBreakpoint: function(lineNumber, disabled, conditional)
  221. {
  222. this.beginUpdates();
  223. this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint");
  224. if (disabled)
  225. this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-disabled");
  226. else
  227. this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
  228. if (conditional)
  229. this._gutterPanel.addDecoration(lineNumber, "webkit-breakpoint-conditional");
  230. else
  231. this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
  232. this.endUpdates();
  233. },
  234. /**
  235. * @param {number} lineNumber
  236. */
  237. removeBreakpoint: function(lineNumber)
  238. {
  239. this.beginUpdates();
  240. this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint");
  241. this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
  242. this._gutterPanel.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
  243. this.endUpdates();
  244. },
  245. /**
  246. * @param {number} lineNumber
  247. */
  248. setExecutionLine: function(lineNumber)
  249. {
  250. this._executionLineNumber = lineNumber;
  251. this._mainPanel.addDecoration(lineNumber, "webkit-execution-line");
  252. this._gutterPanel.addDecoration(lineNumber, "webkit-execution-line");
  253. },
  254. clearExecutionLine: function()
  255. {
  256. if (typeof this._executionLineNumber === "number") {
  257. this._mainPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
  258. this._gutterPanel.removeDecoration(this._executionLineNumber, "webkit-execution-line");
  259. }
  260. delete this._executionLineNumber;
  261. },
  262. /**
  263. * @param {number} lineNumber
  264. * @param {Element} element
  265. */
  266. addDecoration: function(lineNumber, element)
  267. {
  268. this._mainPanel.addDecoration(lineNumber, element);
  269. this._gutterPanel.addDecoration(lineNumber, element);
  270. this._syncDecorationsForLine(lineNumber);
  271. },
  272. /**
  273. * @param {number} lineNumber
  274. * @param {Element} element
  275. */
  276. removeDecoration: function(lineNumber, element)
  277. {
  278. this._mainPanel.removeDecoration(lineNumber, element);
  279. this._gutterPanel.removeDecoration(lineNumber, element);
  280. this._syncDecorationsForLine(lineNumber);
  281. },
  282. /**
  283. * @param {WebInspector.TextRange} range
  284. */
  285. markAndRevealRange: function(range)
  286. {
  287. if (range)
  288. this.setSelection(range);
  289. this._mainPanel.markAndRevealRange(range);
  290. },
  291. /**
  292. * @param {number} lineNumber
  293. */
  294. highlightLine: function(lineNumber)
  295. {
  296. if (typeof lineNumber !== "number" || lineNumber < 0)
  297. return;
  298. lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1);
  299. this._mainPanel.highlightLine(lineNumber);
  300. },
  301. clearLineHighlight: function()
  302. {
  303. this._mainPanel.clearLineHighlight();
  304. },
  305. /**
  306. * @return {Array.<Element>}
  307. */
  308. elementsToRestoreScrollPositionsFor: function()
  309. {
  310. return [this._mainPanel.element];
  311. },
  312. /**
  313. * @param {WebInspector.TextEditor} textEditor
  314. */
  315. inheritScrollPositions: function(textEditor)
  316. {
  317. this._mainPanel.element._scrollTop = textEditor._mainPanel.element.scrollTop;
  318. this._mainPanel.element._scrollLeft = textEditor._mainPanel.element.scrollLeft;
  319. },
  320. beginUpdates: function()
  321. {
  322. this._mainPanel.beginUpdates();
  323. this._gutterPanel.beginUpdates();
  324. },
  325. endUpdates: function()
  326. {
  327. this._mainPanel.endUpdates();
  328. this._gutterPanel.endUpdates();
  329. this._updatePanelOffsets();
  330. },
  331. onResize: function()
  332. {
  333. this._mainPanel.resize();
  334. this._gutterPanel.resize();
  335. this._updatePanelOffsets();
  336. },
  337. _textChanged: function(event)
  338. {
  339. this._mainPanel.textChanged(event.data.oldRange, event.data.newRange);
  340. this._gutterPanel.textChanged(event.data.oldRange, event.data.newRange);
  341. this._updatePanelOffsets();
  342. if (event.data.editRange)
  343. this._delegate.onTextChanged(event.data.oldRange, event.data.newRange);
  344. },
  345. /**
  346. * @param {WebInspector.TextRange} range
  347. * @param {string} text
  348. * @return {WebInspector.TextRange}
  349. */
  350. editRange: function(range, text)
  351. {
  352. return this._textModel.editRange(range, text, this.lastSelection());
  353. },
  354. _updatePanelOffsets: function()
  355. {
  356. var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
  357. if (lineNumbersWidth)
  358. this._mainPanel.element.style.setProperty("left", (lineNumbersWidth + 2) + "px");
  359. else
  360. this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
  361. },
  362. _syncScroll: function()
  363. {
  364. var mainElement = this._mainPanel.element;
  365. var gutterElement = this._gutterPanel.element;
  366. // Handle horizontal scroll bar at the bottom of the main panel.
  367. this._gutterPanel.syncClientHeight(mainElement.clientHeight);
  368. gutterElement.scrollTop = mainElement.scrollTop;
  369. },
  370. /**
  371. * @param {number} lineNumber
  372. */
  373. _syncDecorationsForLine: function(lineNumber)
  374. {
  375. if (lineNumber >= this._textModel.linesCount)
  376. return;
  377. var mainChunk = this._mainPanel.chunkForLine(lineNumber);
  378. if (mainChunk.linesCount === 1 && mainChunk.isDecorated()) {
  379. var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber);
  380. var height = mainChunk.height;
  381. if (height)
  382. gutterChunk.element.style.setProperty("height", height + "px");
  383. else
  384. gutterChunk.element.style.removeProperty("height");
  385. } else {
  386. var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
  387. if (gutterChunk.linesCount === 1)
  388. gutterChunk.element.style.removeProperty("height");
  389. }
  390. },
  391. /**
  392. * @param {Element} gutterRow
  393. */
  394. _syncLineHeight: function(gutterRow)
  395. {
  396. if (this._lineHeightSynced)
  397. return;
  398. if (gutterRow && gutterRow.offsetHeight) {
  399. // Force equal line heights for the child panels.
  400. this.element.style.setProperty("line-height", gutterRow.offsetHeight + "px");
  401. this._lineHeightSynced = true;
  402. }
  403. },
  404. _registerShortcuts: function()
  405. {
  406. var keys = WebInspector.KeyboardShortcut.Keys;
  407. var modifiers = WebInspector.KeyboardShortcut.Modifiers;
  408. this._shortcuts = {};
  409. this._shortcuts[WebInspector.KeyboardShortcut.SelectAll] = this._handleSelectAll.bind(this);
  410. this._wordMovementController._registerShortcuts(this._shortcuts);
  411. },
  412. _handleSelectAll: function()
  413. {
  414. this.setSelection(this._textModel.range());
  415. return true;
  416. },
  417. _handleKeyDown: function(e)
  418. {
  419. // If the event was not triggered from the entire editor, then
  420. // ignore it. https://bugs.webkit.org/show_bug.cgi?id=102906
  421. if (e.target.enclosingNodeOrSelfWithClass("webkit-line-decorations"))
  422. return;
  423. var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
  424. var handler = this._shortcuts[shortcutKey];
  425. if (handler && handler()) {
  426. e.consume(true);
  427. return;
  428. }
  429. this._mainPanel.handleKeyDown(shortcutKey, e);
  430. },
  431. _contextMenu: function(event)
  432. {
  433. var anchor = event.target.enclosingNodeOrSelfWithNodeName("a");
  434. if (anchor)
  435. return;
  436. var contextMenu = new WebInspector.ContextMenu(event);
  437. var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
  438. if (target)
  439. this._delegate.populateLineGutterContextMenu(contextMenu, target.lineNumber);
  440. else {
  441. this._mainPanel.populateContextMenu(event.target, contextMenu);
  442. }
  443. contextMenu.show();
  444. },
  445. _handleScrollChanged: function(event)
  446. {
  447. var visibleFrom = this._mainPanel.scrollTop();
  448. var firstVisibleLineNumber = this._mainPanel.lineNumberAtOffset(visibleFrom);
  449. this._delegate.scrollChanged(firstVisibleLineNumber);
  450. },
  451. /**
  452. * @param {number} lineNumber
  453. */
  454. scrollToLine: function(lineNumber)
  455. {
  456. this._mainPanel.scrollToLine(lineNumber);
  457. },
  458. /**
  459. * @return {WebInspector.TextRange}
  460. */
  461. selection: function()
  462. {
  463. return this._mainPanel.selection();
  464. },
  465. /**
  466. * @return {WebInspector.TextRange?}
  467. */
  468. lastSelection: function()
  469. {
  470. return this._mainPanel.lastSelection();
  471. },
  472. /**
  473. * @param {WebInspector.TextRange} textRange
  474. */
  475. setSelection: function(textRange)
  476. {
  477. this._mainPanel.setSelection(textRange);
  478. },
  479. /**
  480. * @param {string} text
  481. */
  482. setText: function(text)
  483. {
  484. this._textModel.setText(text);
  485. },
  486. /**
  487. * @return {string}
  488. */
  489. text: function()
  490. {
  491. return this._textModel.text();
  492. },
  493. /**
  494. * @return {WebInspector.TextRange}
  495. */
  496. range: function()
  497. {
  498. return this._textModel.range();
  499. },
  500. /**
  501. * @param {number} lineNumber
  502. * @return {string}
  503. */
  504. line: function(lineNumber)
  505. {
  506. return this._textModel.line(lineNumber);
  507. },
  508. /**
  509. * @return {number}
  510. */
  511. get linesCount()
  512. {
  513. return this._textModel.linesCount;
  514. },
  515. /**
  516. * @param {number} line
  517. * @param {string} name
  518. * @param {?Object} value
  519. */
  520. setAttribute: function(line, name, value)
  521. {
  522. this._textModel.setAttribute(line, name, value);
  523. },
  524. /**
  525. * @param {number} line
  526. * @param {string} name
  527. * @return {?Object} value
  528. */
  529. getAttribute: function(line, name)
  530. {
  531. return this._textModel.getAttribute(line, name);
  532. },
  533. /**
  534. * @param {number} line
  535. * @param {string} name
  536. */
  537. removeAttribute: function(line, name)
  538. {
  539. this._textModel.removeAttribute(line, name);
  540. },
  541. wasShown: function()
  542. {
  543. if (!this.readOnly())
  544. WebInspector.markBeingEdited(this.element, true);
  545. this._mainPanel.wasShown();
  546. },
  547. willHide: function()
  548. {
  549. this._mainPanel.willHide();
  550. this._gutterPanel.willHide();
  551. if (!this.readOnly())
  552. WebInspector.markBeingEdited(this.element, false);
  553. },
  554. /**
  555. * @param {Element} element
  556. * @param {Array.<Object>} resultRanges
  557. * @param {string} styleClass
  558. * @param {Array.<Object>=} changes
  559. */
  560. highlightRangesWithStyleClass: function(element, resultRanges, styleClass, changes)
  561. {
  562. this._mainPanel.beginDomUpdates();
  563. WebInspector.highlightRangesWithStyleClass(element, resultRanges, styleClass, changes);
  564. this._mainPanel.endDomUpdates();
  565. },
  566. /**
  567. * @param {number} scrollTop
  568. * @param {number} clientHeight
  569. * @param {number} chunkSize
  570. */
  571. overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
  572. {
  573. this._mainPanel.overrideViewportForTest(scrollTop, clientHeight, chunkSize);
  574. },
  575. __proto__: WebInspector.View.prototype
  576. }
  577. /**
  578. * @constructor
  579. * @param {WebInspector.TextEditorModel} textModel
  580. */
  581. WebInspector.TextEditorChunkedPanel = function(textModel)
  582. {
  583. this._textModel = textModel;
  584. this.element = document.createElement("div");
  585. this.element.addEventListener("scroll", this._scroll.bind(this), false);
  586. this._defaultChunkSize = 50;
  587. this._paintCoalescingLevel = 0;
  588. this._domUpdateCoalescingLevel = 0;
  589. }
  590. WebInspector.TextEditorChunkedPanel.prototype = {
  591. /**
  592. * @param {number} lineNumber
  593. */
  594. scrollToLine: function(lineNumber)
  595. {
  596. if (lineNumber >= this._textModel.linesCount)
  597. return;
  598. var chunk = this.makeLineAChunk(lineNumber);
  599. this.element.scrollTop = chunk.offsetTop;
  600. },
  601. /**
  602. * @param {number} lineNumber
  603. */
  604. revealLine: function(lineNumber)
  605. {
  606. if (lineNumber >= this._textModel.linesCount)
  607. return;
  608. var chunk = this.makeLineAChunk(lineNumber);
  609. chunk.element.scrollIntoViewIfNeeded();
  610. },
  611. /**
  612. * @param {number} lineNumber
  613. * @param {string|Element} decoration
  614. */
  615. addDecoration: function(lineNumber, decoration)
  616. {
  617. if (lineNumber >= this._textModel.linesCount)
  618. return;
  619. var chunk = this.makeLineAChunk(lineNumber);
  620. chunk.addDecoration(decoration);
  621. },
  622. /**
  623. * @param {number} lineNumber
  624. * @param {string|Element} decoration
  625. */
  626. removeDecoration: function(lineNumber, decoration)
  627. {
  628. if (lineNumber >= this._textModel.linesCount)
  629. return;
  630. var chunk = this.chunkForLine(lineNumber);
  631. chunk.removeDecoration(decoration);
  632. },
  633. buildChunks: function()
  634. {
  635. this.beginDomUpdates();
  636. this._container.removeChildren();
  637. this._textChunks = [];
  638. for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
  639. var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
  640. this._textChunks.push(chunk);
  641. this._container.appendChild(chunk.element);
  642. }
  643. this.repaintAll();
  644. this.endDomUpdates();
  645. },
  646. /**
  647. * @param {number} lineNumber
  648. * @return {Object}
  649. */
  650. makeLineAChunk: function(lineNumber)
  651. {
  652. var chunkNumber = this.chunkNumberForLine(lineNumber);
  653. var oldChunk = this._textChunks[chunkNumber];
  654. if (!oldChunk) {
  655. console.error("No chunk for line number: " + lineNumber);
  656. return null;
  657. }
  658. if (oldChunk.linesCount === 1)
  659. return oldChunk;
  660. return this.splitChunkOnALine(lineNumber, chunkNumber, true);
  661. },
  662. /**
  663. * @param {number} lineNumber
  664. * @param {number} chunkNumber
  665. * @param {boolean=} createSuffixChunk
  666. * @return {Object}
  667. */
  668. splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
  669. {
  670. this.beginDomUpdates();
  671. var oldChunk = this._textChunks[chunkNumber];
  672. var wasExpanded = oldChunk.expanded();
  673. oldChunk.collapse();
  674. var insertIndex = chunkNumber + 1;
  675. // Prefix chunk.
  676. if (lineNumber > oldChunk.startLine) {
  677. var prefixChunk = this.createNewChunk(oldChunk.startLine, lineNumber);
  678. this._textChunks.splice(insertIndex++, 0, prefixChunk);
  679. this._container.insertBefore(prefixChunk.element, oldChunk.element);
  680. }
  681. // Line chunk.
  682. var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount;
  683. var lineChunk = this.createNewChunk(lineNumber, endLine);
  684. this._textChunks.splice(insertIndex++, 0, lineChunk);
  685. this._container.insertBefore(lineChunk.element, oldChunk.element);
  686. // Suffix chunk.
  687. if (oldChunk.startLine + oldChunk.linesCount > endLine) {
  688. var suffixChunk = this.createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount);
  689. this._textChunks.splice(insertIndex, 0, suffixChunk);
  690. this._container.insertBefore(suffixChunk.element, oldChunk.element);
  691. }
  692. // Remove enclosing chunk.
  693. this._textChunks.splice(chunkNumber, 1);
  694. this._container.removeChild(oldChunk.element);
  695. if (wasExpanded) {
  696. if (prefixChunk)
  697. prefixChunk.expand();
  698. lineChunk.expand();
  699. if (suffixChunk)
  700. suffixChunk.expand();
  701. }
  702. this.endDomUpdates();
  703. return lineChunk;
  704. },
  705. createNewChunk: function(startLine, endLine)
  706. {
  707. throw new Error("createNewChunk() should be implemented by descendants");
  708. },
  709. _scroll: function()
  710. {
  711. this._scheduleRepaintAll();
  712. if (this._syncScrollListener)
  713. this._syncScrollListener();
  714. },
  715. _scheduleRepaintAll: function()
  716. {
  717. if (this._repaintAllTimer)
  718. clearTimeout(this._repaintAllTimer);
  719. this._repaintAllTimer = setTimeout(this.repaintAll.bind(this), 50);
  720. },
  721. beginUpdates: function()
  722. {
  723. this._paintCoalescingLevel++;
  724. },
  725. endUpdates: function()
  726. {
  727. this._paintCoalescingLevel--;
  728. if (!this._paintCoalescingLevel)
  729. this.repaintAll();
  730. },
  731. beginDomUpdates: function()
  732. {
  733. this._domUpdateCoalescingLevel++;
  734. },
  735. endDomUpdates: function()
  736. {
  737. this._domUpdateCoalescingLevel--;
  738. },
  739. /**
  740. * @param {number} lineNumber
  741. * @return {number}
  742. */
  743. chunkNumberForLine: function(lineNumber)
  744. {
  745. function compareLineNumbers(value, chunk)
  746. {
  747. return value < chunk.startLine ? -1 : 1;
  748. }
  749. var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
  750. return insertBefore - 1;
  751. },
  752. /**
  753. * @param {number} lineNumber
  754. * @return {Object}
  755. */
  756. chunkForLine: function(lineNumber)
  757. {
  758. return this._textChunks[this.chunkNumberForLine(lineNumber)];
  759. },
  760. /**
  761. * @param {number} visibleFrom
  762. * @return {number}
  763. */
  764. _findFirstVisibleChunkNumber: function(visibleFrom)
  765. {
  766. function compareOffsetTops(value, chunk)
  767. {
  768. return value < chunk.offsetTop ? -1 : 1;
  769. }
  770. var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
  771. return insertBefore - 1;
  772. },
  773. /**
  774. * @param {number} visibleFrom
  775. * @param {number} visibleTo
  776. * @return {{start: number, end: number}}
  777. */
  778. findVisibleChunks: function(visibleFrom, visibleTo)
  779. {
  780. var span = (visibleTo - visibleFrom) * 0.5;
  781. visibleFrom = Math.max(visibleFrom - span, 0);
  782. visibleTo = visibleTo + span;
  783. var from = this._findFirstVisibleChunkNumber(visibleFrom);
  784. for (var to = from + 1; to < this._textChunks.length; ++to) {
  785. if (this._textChunks[to].offsetTop >= visibleTo)
  786. break;
  787. }
  788. return { start: from, end: to };
  789. },
  790. /**
  791. * @param {number} visibleFrom
  792. * @return {number}
  793. */
  794. lineNumberAtOffset: function(visibleFrom)
  795. {
  796. var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
  797. if (!chunk.expanded())
  798. return chunk.startLine;
  799. var lineNumbers = [];
  800. for (var i = 0; i < chunk.linesCount; ++i) {
  801. lineNumbers.push(chunk.startLine + i);
  802. }
  803. function compareLineRowOffsetTops(value, lineNumber)
  804. {
  805. var lineRow = chunk.expandedLineRow(lineNumber);
  806. return value < lineRow.offsetTop ? -1 : 1;
  807. }
  808. var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
  809. return lineNumbers[insertBefore - 1];
  810. },
  811. repaintAll: function()
  812. {
  813. delete this._repaintAllTimer;
  814. if (this._paintCoalescingLevel)
  815. return;
  816. var visibleFrom = this.scrollTop();
  817. var visibleTo = visibleFrom + this.clientHeight();
  818. if (visibleTo) {
  819. var result = this.findVisibleChunks(visibleFrom, visibleTo);
  820. this.expandChunks(result.start, result.end);
  821. }
  822. },
  823. scrollTop: function()
  824. {
  825. return typeof this._scrollTopOverrideForTest === "number" ? this._scrollTopOverrideForTest : this.element.scrollTop;
  826. },
  827. clientHeight: function()
  828. {
  829. return typeof this._clientHeightOverrideForTest === "number" ? this._clientHeightOverrideForTest : this.element.clientHeight;
  830. },
  831. /**
  832. * @param {number} fromIndex
  833. * @param {number} toIndex
  834. */
  835. expandChunks: function(fromIndex, toIndex)
  836. {
  837. // First collapse chunks to collect the DOM elements into a cache to reuse them later.
  838. for (var i = 0; i < fromIndex; ++i)
  839. this._textChunks[i].collapse();
  840. for (var i = toIndex; i < this._textChunks.length; ++i)
  841. this._textChunks[i].collapse();
  842. for (var i = fromIndex; i < toIndex; ++i)
  843. this._textChunks[i].expand();
  844. },
  845. /**
  846. * @param {Element} firstElement
  847. * @param {Element=} lastElement
  848. * @return {number}
  849. */
  850. totalHeight: function(firstElement, lastElement)
  851. {
  852. lastElement = (lastElement || firstElement).nextElementSibling;
  853. if (lastElement)
  854. return lastElement.offsetTop - firstElement.offsetTop;
  855. var offsetParent = firstElement.offsetParent;
  856. if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
  857. return offsetParent.scrollHeight - firstElement.offsetTop;
  858. var total = 0;
  859. while (firstElement && firstElement !== lastElement) {
  860. total += firstElement.offsetHeight;
  861. firstElement = firstElement.nextElementSibling;
  862. }
  863. return total;
  864. },
  865. resize: function()
  866. {
  867. this.repaintAll();
  868. }
  869. }
  870. /**
  871. * @constructor
  872. * @extends {WebInspector.TextEditorChunkedPanel}
  873. * @param {WebInspector.TextEditorModel} textModel
  874. * @param {function(number)} syncDecorationsForLineListener
  875. * @param {function(Element)} syncLineHeightListener
  876. */
  877. WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener, syncLineHeightListener)
  878. {
  879. WebInspector.TextEditorChunkedPanel.call(this, textModel);
  880. this._syncDecorationsForLineListener = syncDecorationsForLineListener;
  881. this._syncLineHeightListener = syncLineHeightListener;
  882. this.element.className = "text-editor-lines";
  883. this._container = document.createElement("div");
  884. this._container.className = "inner-container";
  885. this.element.appendChild(this._container);
  886. this._freeCachedElements();
  887. this.buildChunks();
  888. this._decorations = {};
  889. }
  890. WebInspector.TextEditorGutterPanel.prototype = {
  891. _freeCachedElements: function()
  892. {
  893. this._cachedRows = [];
  894. },
  895. willHide: function()
  896. {
  897. this._freeCachedElements();
  898. },
  899. /**
  900. * @param {number} startLine
  901. * @param {number} endLine
  902. * @return {WebInspector.TextEditorGutterChunk}
  903. */
  904. createNewChunk: function(startLine, endLine)
  905. {
  906. return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
  907. },
  908. /**
  909. * @param {WebInspector.TextRange} oldRange
  910. * @param {WebInspector.TextRange} newRange
  911. */
  912. textChanged: function(oldRange, newRange)
  913. {
  914. this.beginDomUpdates();
  915. var linesDiff = newRange.linesCount - oldRange.linesCount;
  916. if (linesDiff) {
  917. // Remove old chunks (if needed).
  918. for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0; --chunkNumber) {
  919. var chunk = this._textChunks[chunkNumber];
  920. if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
  921. break;
  922. chunk.collapse();
  923. this._container.removeChild(chunk.element);
  924. }
  925. this._textChunks.length = chunkNumber + 1;
  926. // Add new chunks (if needed).
  927. var totalLines = 0;
  928. if (this._textChunks.length) {
  929. var lastChunk = this._textChunks[this._textChunks.length - 1];
  930. totalLines = lastChunk.startLine + lastChunk.linesCount;
  931. }
  932. for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
  933. var chunk = this.createNewChunk(i, i + this._defaultChunkSize);
  934. this._textChunks.push(chunk);
  935. this._container.appendChild(chunk.element);
  936. }
  937. // Shift decorations if necessary
  938. var decorationsToRestore = {};
  939. for (var lineNumber in this._decorations) {
  940. lineNumber = parseInt(lineNumber, 10);
  941. // Do not move decorations before the start position.
  942. if (lineNumber < oldRange.startLine)
  943. continue;
  944. // Decorations follow the first character of line.
  945. if (lineNumber === oldRange.startLine && oldRange.startColumn)
  946. continue;
  947. var lineDecorationsCopy = this._decorations[lineNumber].slice();
  948. for (var i = 0; i < lineDecorationsCopy.length; ++i)
  949. this.removeDecoration(lineNumber, lineDecorationsCopy[i]);
  950. // Do not restore the decorations before the end position.
  951. if (lineNumber >= oldRange.endLine)
  952. decorationsToRestore[lineNumber] = lineDecorationsCopy;
  953. }
  954. for (var lineNumber in decorationsToRestore) {
  955. lineNumber = parseInt(lineNumber, 10);
  956. var lineDecorationsCopy = decorationsToRestore[lineNumber];
  957. for (var i = 0; i < lineDecorationsCopy.length; ++i)
  958. this.addDecoration(lineNumber + linesDiff, lineDecorationsCopy[i]);
  959. }
  960. this.repaintAll();
  961. } else {
  962. // Decorations may have been removed, so we may have to sync those lines.
  963. var chunkNumber = this.chunkNumberForLine(newRange.startLine);
  964. var chunk = this._textChunks[chunkNumber];
  965. while (chunk && chunk.startLine <= newRange.endLine) {
  966. if (chunk.linesCount === 1)
  967. this._syncDecorationsForLineListener(chunk.startLine);
  968. chunk = this._textChunks[++chunkNumber];
  969. }
  970. }
  971. this.endDomUpdates();
  972. },
  973. /**
  974. * @param {number} clientHeight
  975. */
  976. syncClientHeight: function(clientHeight)
  977. {
  978. if (this.element.offsetHeight > clientHeight)
  979. this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
  980. else
  981. this._container.style.removeProperty("padding-bottom");
  982. },
  983. /**
  984. * @param {number} lineNumber
  985. * @param {string|Element} decoration
  986. */
  987. addDecoration: function(lineNumber, decoration)
  988. {
  989. WebInspector.TextEditorChunkedPanel.prototype.addDecoration.call(this, lineNumber, decoration);
  990. var decorations = this._decorations[lineNumber];
  991. if (!decorations) {
  992. decorations = [];
  993. this._decorations[lineNumber] = decorations;
  994. }
  995. decorations.push(decoration);
  996. },
  997. /**
  998. * @param {number} lineNumber
  999. * @param {string|Element} decoration
  1000. */
  1001. removeDecoration: function(lineNumber, decoration)
  1002. {
  1003. WebInspector.TextEditorChunkedPanel.prototype.removeDecoration.call(this, lineNumber, decoration);
  1004. var decorations = this._decorations[lineNumber];
  1005. if (decorations) {
  1006. decorations.remove(decoration);
  1007. if (!decorations.length)
  1008. delete this._decorations[lineNumber];
  1009. }
  1010. },
  1011. __proto__: WebInspector.TextEditorChunkedPanel.prototype
  1012. }
  1013. /**
  1014. * @constructor
  1015. * @param {WebInspector.TextEditorGutterPanel} chunkedPanel
  1016. * @param {number} startLine
  1017. * @param {number} endLine
  1018. */
  1019. WebInspector.TextEditorGutterChunk = function(chunkedPanel, startLine, endLine)
  1020. {
  1021. this._chunkedPanel = chunkedPanel;
  1022. this._textModel = chunkedPanel._textModel;
  1023. this.startLine = startLine;
  1024. endLine = Math.min(this._textModel.linesCount, endLine);
  1025. this.linesCount = endLine - startLine;
  1026. this._expanded = false;
  1027. this.element = document.createElement("div");
  1028. this.element.lineNumber = startLine;
  1029. this.element.className = "webkit-line-number";
  1030. if (this.linesCount === 1) {
  1031. // Single line chunks are typically created for decorations. Host line number in
  1032. // the sub-element in order to allow flexible border / margin management.
  1033. var innerSpan = document.createElement("span");
  1034. innerSpan.className = "webkit-line-number-inner";
  1035. innerSpan.textContent = startLine + 1;
  1036. var outerSpan = document.createElement("div");
  1037. outerSpan.className = "webkit-line-number-outer";
  1038. outerSpan.appendChild(innerSpan);
  1039. this.element.appendChild(outerSpan);
  1040. } else {
  1041. var lineNumbers = [];
  1042. for (var i = startLine; i < endLine; ++i)
  1043. lineNumbers.push(i + 1);
  1044. this.element.textContent = lineNumbers.join("\n");
  1045. }
  1046. }
  1047. WebInspector.TextEditorGutterChunk.prototype = {
  1048. /**
  1049. * @param {string} decoration
  1050. */
  1051. addDecoration: function(decoration)
  1052. {
  1053. this._chunkedPanel.beginDomUpdates();
  1054. if (typeof decoration === "string")
  1055. this.element.addStyleClass(decoration);
  1056. this._chunkedPanel.endDomUpdates();
  1057. },
  1058. /**
  1059. * @param {string} decoration
  1060. */
  1061. removeDecoration: function(decoration)
  1062. {
  1063. this._chunkedPanel.beginDomUpdates();
  1064. if (typeof decoration === "string")
  1065. this.element.removeStyleClass(decoration);
  1066. this._chunkedPanel.endDomUpdates();
  1067. },
  1068. /**
  1069. * @return {boolean}
  1070. */
  1071. expanded: function()
  1072. {
  1073. return this._expanded;
  1074. },
  1075. expand: function()
  1076. {
  1077. if (this.linesCount === 1)
  1078. this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
  1079. if (this._expanded)
  1080. return;
  1081. this._expanded = true;
  1082. if (this.linesCount === 1)
  1083. return;
  1084. this._chunkedPanel.beginDomUpdates();
  1085. this._expandedLineRows = [];
  1086. var parentElement = this.element.parentElement;
  1087. for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
  1088. var lineRow = this._createRow(i);
  1089. parentElement.insertBefore(lineRow, this.element);
  1090. this._expandedLineRows.push(lineRow);
  1091. }
  1092. parentElement.removeChild(this.element);
  1093. this._chunkedPanel._syncLineHeightListener(this._expandedLineRows[0]);
  1094. this._chunkedPanel.endDomUpdates();
  1095. },
  1096. collapse: function()
  1097. {
  1098. if (this.linesCount === 1)
  1099. this._chunkedPanel._syncDecorationsForLineListener(this.startLine);
  1100. if (!this._expanded)
  1101. return;
  1102. this._expanded = false;
  1103. if (this.linesCount === 1)
  1104. return;
  1105. this._chunkedPanel.beginDomUpdates();
  1106. var elementInserted = false;
  1107. for (var i = 0; i < this._expandedLineRows.length; ++i) {
  1108. var lineRow = this._expandedLineRows[i];
  1109. var parentElement = lineRow.parentElement;
  1110. if (parentElement) {
  1111. if (!elementInserted) {
  1112. elementInserted = true;
  1113. parentElement.insertBefore(this.element, lineRow);
  1114. }
  1115. parentElement.removeChild(lineRow);
  1116. }
  1117. this._chunkedPanel._cachedRows.push(lineRow);
  1118. }
  1119. delete this._expandedLineRows;
  1120. this._chunkedPanel.endDomUpdates();
  1121. },
  1122. /**
  1123. * @return {number}
  1124. */
  1125. get height()
  1126. {
  1127. if (!this._expandedLineRows)
  1128. return this._chunkedPanel.totalHeight(this.element);
  1129. return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
  1130. },
  1131. /**
  1132. * @return {number}
  1133. */
  1134. get offsetTop()
  1135. {
  1136. return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
  1137. },
  1138. /**
  1139. * @param {number} lineNumber
  1140. * @return {Element}
  1141. */
  1142. _createRow: function(lineNumber)
  1143. {
  1144. var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
  1145. lineRow.lineNumber = lineNumber;
  1146. lineRow.className = "webkit-line-number";
  1147. lineRow.textContent = lineNumber + 1;
  1148. return lineRow;
  1149. }
  1150. }
  1151. /**
  1152. * @constructor
  1153. * @extends {WebInspector.TextEditorChunkedPanel}
  1154. * @param {WebInspector.TextEditorDelegate} delegate
  1155. * @param {WebInspector.TextEditorModel} textModel
  1156. * @param {?string} url
  1157. * @param {function()} syncScrollListener
  1158. * @param {function(number)} syncDecorationsForLineListener
  1159. */
  1160. WebInspector.TextEditorMainPanel = function(delegate, textModel, url, syncScrollListener, syncDecorationsForLineListener)
  1161. {
  1162. WebInspector.TextEditorChunkedPanel.call(this, textModel);
  1163. this._delegate = delegate;
  1164. this._syncScrollListener = syncScrollListener;
  1165. this._syncDecorationsForLineListener = syncDecorationsForLineListener;
  1166. this._url = url;
  1167. this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
  1168. this._readOnly = true;
  1169. this.element.className = "text-editor-contents";
  1170. this.element.tabIndex = 0;
  1171. this._container = document.createElement("div");
  1172. this._container.className = "inner-container";
  1173. this._container.tabIndex = 0;
  1174. this.element.appendChild(this._container);
  1175. this.element.addEventListener("focus", this._handleElementFocus.bind(this), false);
  1176. this.element.addEventListener("textInput", this._handleTextInput.bind(this), false);
  1177. this.element.addEventListener("cut", this._handleCut.bind(this), false);
  1178. this.element.addEventListener("keypress", this._handleKeyPress.bind(this), false);
  1179. this._showWhitespace = WebInspector.experimentsSettings.showWhitespaceInEditor.isEnabled();
  1180. this._container.addEventListener("focus", this._handleFocused.bind(this), false);
  1181. this._highlightDescriptors = [];
  1182. this._tokenHighlighter = new WebInspector.TextEditorMainPanel.TokenHighlighter(this, textModel);
  1183. this._braceMatcher = new WebInspector.TextEditorModel.BraceMatcher(textModel);
  1184. this._braceHighlighter = new WebInspector.TextEditorMainPanel.BraceHighlightController(this, textModel, this._braceMatcher);
  1185. this._smartBraceController = new WebInspector.TextEditorMainPanel.SmartBraceController(this, textModel, this._braceMatcher);
  1186. this._freeCachedElements();
  1187. this.buildChunks();
  1188. this._registerShortcuts();
  1189. }
  1190. WebInspector.TextEditorMainPanel._ConsecutiveWhitespaceChars = {
  1191. 1: " ",
  1192. 2: " ",
  1193. 4: " ",
  1194. 8: " ",
  1195. 16: " "
  1196. };
  1197. WebInspector.TextEditorMainPanel.prototype = {
  1198. /**
  1199. * @param {number} lineNumber
  1200. * @param {number} column
  1201. * @return {?{startColumn: number, endColumn: number, type: string}}
  1202. */
  1203. tokenAtTextPosition: function(lineNumber, column)
  1204. {
  1205. if (lineNumber >= this._textModel.linesCount || lineNumber < 0)
  1206. return null;
  1207. var line = this._textModel.line(lineNumber);
  1208. if (column >= line.length || column < 0)
  1209. return null;
  1210. var highlight = this._textModel.getAttribute(lineNumber, "highlight");
  1211. if (!highlight)
  1212. return this._tokenAtUnhighlightedLine(line, column);
  1213. function compare(value, object)
  1214. {
  1215. if (value >= object.startColumn && value <= object.endColumn)
  1216. return 0;
  1217. return value - object.startColumn;
  1218. }
  1219. var index = binarySearch(column, highlight.ranges, compare);
  1220. if (index >= 0) {
  1221. var range = highlight.ranges[index];
  1222. return {
  1223. startColumn: range.startColumn,
  1224. endColumn: range.endColumn,
  1225. type: range.token
  1226. };
  1227. }
  1228. return null;
  1229. },
  1230. /**
  1231. * @param {number} lineNumber
  1232. * @param {number} column
  1233. * @return {?{x: number, y: number, height: number}}
  1234. */
  1235. cursorPositionToCoordinates: function(lineNumber, column)
  1236. {
  1237. if (lineNumber >= this._textModel.linesCount || lineNumber < 0)
  1238. return null;
  1239. var line = this._textModel.line(lineNumber);
  1240. if (column > line.length || column < 0)
  1241. return null;
  1242. var chunk = this.chunkForLine(lineNumber);
  1243. if (!chunk.expanded())
  1244. return null;
  1245. var lineRow = chunk.expandedLineRow(lineNumber);
  1246. var ranges = [{
  1247. startColumn: column,
  1248. endColumn: column,
  1249. token: "measure-cursor-position"
  1250. }];
  1251. var selection = this.selection();
  1252. this.beginDomUpdates();
  1253. this._renderRanges(lineRow, line, ranges);
  1254. var spans = lineRow.getElementsByClassName("webkit-measure-cursor-position");
  1255. if (WebInspector.debugDefaultTextEditor)
  1256. console.assert(spans.length === 0);
  1257. var totalOffset = spans[0].totalOffset();
  1258. var height = spans[0].offsetHeight;
  1259. this._paintLineRows([lineRow]);
  1260. this.endDomUpdates();
  1261. this._restoreSelection(selection);
  1262. return {
  1263. x: totalOffset.left,
  1264. y: totalOffset.top,
  1265. height: height
  1266. };
  1267. },
  1268. /**
  1269. * @param {number} x
  1270. * @param {number} y
  1271. * @return {?WebInspector.TextRange}
  1272. */
  1273. coordinatesToCursorPosition: function(x, y)
  1274. {
  1275. var element = document.elementFromPoint(x, y);
  1276. if (!element)
  1277. return null;
  1278. var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
  1279. if (!lineRow)
  1280. return null;
  1281. var line = this._textModel.line(lineRow.lineNumber) + " ";
  1282. var ranges = [];
  1283. const prefix = "character-position-";
  1284. for(var i = 0; i < line.length; ++i) {
  1285. ranges.push({
  1286. startColumn: i,
  1287. endColumn: i,
  1288. token: prefix + i
  1289. });
  1290. }
  1291. var selection = this.selection();
  1292. this.beginDomUpdates();
  1293. this._renderRanges(lineRow, line, ranges);
  1294. var charElement = document.elementFromPoint(x, y);
  1295. this._paintLineRows([lineRow]);
  1296. this.endDomUpdates();
  1297. this._restoreSelection(selection);
  1298. var className = charElement.className;
  1299. if (className.indexOf(prefix) < 0)
  1300. return null;
  1301. var column = parseInt(className.substring(className.indexOf(prefix) + prefix.length), 10);
  1302. return WebInspector.TextRange.createFromLocation(lineRow.lineNumber, column);
  1303. },
  1304. /**
  1305. * @param {string} line
  1306. * @param {number} column
  1307. * @return {?{startColumn: number, endColumn: number, type: string}}
  1308. */
  1309. _tokenAtUnhighlightedLine: function(line, column)
  1310. {
  1311. var tokenizer = WebInspector.SourceTokenizer.Registry.getInstance().getTokenizer(this.mimeType);
  1312. tokenizer.condition = tokenizer.createInitialCondition();
  1313. tokenizer.line = line;
  1314. var lastTokenizedColumn = 0;
  1315. while (lastTokenizedColumn < line.length) {
  1316. var newColumn = tokenizer.nextToken(lastTokenizedColumn);
  1317. if (column < newColumn) {
  1318. if (!tokenizer.tokenType)
  1319. return null;
  1320. return {
  1321. startColumn: lastTokenizedColumn,
  1322. endColumn: newColumn - 1,
  1323. type: tokenizer.tokenType
  1324. };
  1325. } else
  1326. lastTokenizedColumn = newColumn;
  1327. }
  1328. return null;
  1329. },
  1330. _registerShortcuts: function()
  1331. {
  1332. var keys = WebInspector.KeyboardShortcut.Keys;
  1333. var modifiers = WebInspector.KeyboardShortcut.Modifiers;
  1334. this._shortcuts = {};
  1335. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, WebInspector.KeyboardShortcut.Modifiers.None)] = this._handleEnterKey.bind(this);
  1336. this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = this._handleUndoRedo.bind(this, false);
  1337. var handleRedo = this._handleUndoRedo.bind(this, true);
  1338. this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo;
  1339. if (!WebInspector.isMac())
  1340. this._shortcuts[WebInspector.KeyboardShortcut.makeKey("y", modifiers.CtrlOrMeta)] = handleRedo;
  1341. var handleTabKey = this._handleTabKeyPress.bind(this, false);
  1342. var handleShiftTabKey = this._handleTabKeyPress.bind(this, true);
  1343. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey;
  1344. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey;
  1345. var homeKey = WebInspector.isMac() ? keys.Right : keys.Home;
  1346. var homeModifier = WebInspector.isMac() ? modifiers.Meta : modifiers.None;
  1347. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(homeKey.code, homeModifier)] = this._handleHomeKey.bind(this, false);
  1348. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(homeKey.code, homeModifier | modifiers.Shift)] = this._handleHomeKey.bind(this, true);
  1349. this._charOverrides = {};
  1350. this._smartBraceController.registerShortcuts(this._shortcuts);
  1351. this._smartBraceController.registerCharOverrides(this._charOverrides);
  1352. },
  1353. _handleKeyPress: function(event)
  1354. {
  1355. var char = String.fromCharCode(event.which);
  1356. var handler = this._charOverrides[char];
  1357. if (handler && handler()) {
  1358. event.consume(true);
  1359. return;
  1360. }
  1361. this._keyDownCode = event.keyCode;
  1362. },
  1363. /**
  1364. * @param {boolean} shift
  1365. */
  1366. _handleHomeKey: function(shift)
  1367. {
  1368. var selection = this.selection();
  1369. var line = this._textModel.line(selection.endLine);
  1370. var firstNonBlankCharacter = 0;
  1371. while (firstNonBlankCharacter < line.length) {
  1372. var char = line.charAt(firstNonBlankCharacter);
  1373. if (char === " " || char === "\t")
  1374. ++firstNonBlankCharacter;
  1375. else
  1376. break;
  1377. }
  1378. if (firstNonBlankCharacter >= line.length || selection.endColumn === firstNonBlankCharacter)
  1379. return false;
  1380. selection.endColumn = firstNonBlankCharacter;
  1381. if (!shift)
  1382. selection = selection.collapseToEnd();
  1383. this._restoreSelection(selection);
  1384. return true;
  1385. },
  1386. /**
  1387. * @param {string} regex
  1388. * @param {string} cssClass
  1389. * @return {Object}
  1390. */
  1391. highlightRegex: function(regex, cssClass)
  1392. {
  1393. var highlightDescriptor = new WebInspector.TextEditorMainPanel.RegexHighlightDescriptor(new RegExp(regex, "g"), cssClass);
  1394. this._highlightDescriptors.push(highlightDescriptor);
  1395. this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
  1396. return highlightDescriptor;
  1397. },
  1398. /**
  1399. * @param {Object} highlightDescriptor
  1400. */
  1401. removeHighlight: function(highlightDescriptor)
  1402. {
  1403. this._highlightDescriptors.remove(highlightDescriptor);
  1404. this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
  1405. },
  1406. /**
  1407. * @param {WebInspector.TextRange} range
  1408. * @param {string} cssClass
  1409. * @return {Object}
  1410. */
  1411. highlightRange: function(range, cssClass)
  1412. {
  1413. var highlightDescriptor = new WebInspector.TextEditorMainPanel.RangeHighlightDescriptor(range, cssClass);
  1414. this._highlightDescriptors.push(highlightDescriptor);
  1415. this._repaintLineRowsAffectedByHighlightDescriptors([highlightDescriptor]);
  1416. return highlightDescriptor;
  1417. },
  1418. /**
  1419. * @param {Array.<WebInspector.TextEditorMainPanel.HighlightDescriptor>} highlightDescriptors
  1420. */
  1421. _repaintLineRowsAffectedByHighlightDescriptors: function(highlightDescriptors)
  1422. {
  1423. var visibleFrom = this.scrollTop();
  1424. var visibleTo = visibleFrom + this.clientHeight();
  1425. var visibleChunks = this.findVisibleChunks(visibleFrom, visibleTo);
  1426. var affectedLineRows = [];
  1427. for (var i = visibleChunks.start; i < visibleChunks.end; ++i) {
  1428. var chunk = this._textChunks[i];
  1429. if (!chunk.expanded())
  1430. continue;
  1431. for (var lineNumber = chunk.startLine; lineNumber < chunk.startLine + chunk.linesCount; ++lineNumber) {
  1432. var lineRow = chunk.expandedLineRow(lineNumber);
  1433. var line = this._textModel.line(lineNumber);
  1434. for(var j = 0; j < highlightDescriptors.length; ++j) {
  1435. if (highlightDescriptors[j].affectsLine(lineNumber, line)) {
  1436. affectedLineRows.push(lineRow);
  1437. break;
  1438. }
  1439. }
  1440. }
  1441. }
  1442. if (affectedLineRows.length === 0)
  1443. return;
  1444. var selection = this.selection();
  1445. this._paintLineRows(affectedLineRows);
  1446. this._restoreSelection(selection);
  1447. },
  1448. resize: function()
  1449. {
  1450. WebInspector.TextEditorChunkedPanel.prototype.resize.call(this);
  1451. this._repaintLineRowsAffectedByHighlightDescriptors(this._highlightDescriptors);
  1452. },
  1453. wasShown: function()
  1454. {
  1455. this._boundSelectionChangeListener = this._handleSelectionChange.bind(this);
  1456. document.addEventListener("selectionchange", this._boundSelectionChangeListener, false);
  1457. this._isShowing = true;
  1458. this._attachMutationObserver();
  1459. },
  1460. willHide: function()
  1461. {
  1462. document.removeEventListener("selectionchange", this._boundSelectionChangeListener, false);
  1463. delete this._boundSelectionChangeListener;
  1464. this._detachMutationObserver();
  1465. this._isShowing = false;
  1466. this._freeCachedElements();
  1467. },
  1468. /**
  1469. * @param {Element} eventTarget
  1470. * @param {WebInspector.ContextMenu} contextMenu
  1471. */
  1472. populateContextMenu: function(eventTarget, contextMenu)
  1473. {
  1474. var target = this._enclosingLineRowOrSelf(eventTarget);
  1475. this._delegate.populateTextAreaContextMenu(contextMenu, target && target.lineNumber);
  1476. },
  1477. /**
  1478. * @param {WebInspector.TextRange} textRange
  1479. */
  1480. setSelection: function(textRange)
  1481. {
  1482. this._lastSelection = textRange;
  1483. if (this.element.isAncestor(document.activeElement))
  1484. this._restoreSelection(textRange);
  1485. },
  1486. _handleFocused: function()
  1487. {
  1488. if (this._lastSelection)
  1489. this.setSelection(this._lastSelection);
  1490. },
  1491. _attachMutationObserver: function()
  1492. {
  1493. if (!this._isShowing)
  1494. return;
  1495. if (this._mutationObserver)
  1496. this._mutationObserver.disconnect();
  1497. this._mutationObserver = new NonLeakingMutationObserver(this._handleMutations.bind(this));
  1498. this._mutationObserver.observe(this._container, { subtree: true, childList: true, characterData: true });
  1499. },
  1500. _detachMutationObserver: function()
  1501. {
  1502. if (!this._isShowing)
  1503. return;
  1504. if (this._mutationObserver) {
  1505. this._mutationObserver.disconnect();
  1506. delete this._mutationObserver;
  1507. }
  1508. },
  1509. /**
  1510. * @param {string} mimeType
  1511. */
  1512. set mimeType(mimeType)
  1513. {
  1514. this._highlighter.mimeType = mimeType;
  1515. },
  1516. get mimeType()
  1517. {
  1518. return this._highlighter.mimeType;
  1519. },
  1520. /**
  1521. * @param {boolean} readOnly
  1522. * @param {boolean} requestFocus
  1523. */
  1524. setReadOnly: function(readOnly, requestFocus)
  1525. {
  1526. if (this._readOnly === readOnly)
  1527. return;
  1528. this.beginDomUpdates();
  1529. this._readOnly = readOnly;
  1530. if (this._readOnly)
  1531. this._container.removeStyleClass("text-editor-editable");
  1532. else {
  1533. this._container.addStyleClass("text-editor-editable");
  1534. if (requestFocus)
  1535. this._updateSelectionOnStartEditing();
  1536. }
  1537. this.endDomUpdates();
  1538. },
  1539. /**
  1540. * @return {boolean}
  1541. */
  1542. readOnly: function()
  1543. {
  1544. return this._readOnly;
  1545. },
  1546. _handleElementFocus: function()
  1547. {
  1548. if (!this._readOnly)
  1549. this._container.focus();
  1550. },
  1551. /**
  1552. * @return {Element}
  1553. */
  1554. defaultFocusedElement: function()
  1555. {
  1556. if (this._readOnly)
  1557. return this.element;
  1558. return this._container;
  1559. },
  1560. _updateSelectionOnStartEditing: function()
  1561. {
  1562. // focus() needs to go first for the case when the last selection was inside the editor and
  1563. // the "Edit" button was clicked. In this case we bail at the check below, but the
  1564. // editor does not receive the focus, thus "Esc" does not cancel editing until at least
  1565. // one change has been made to the editor contents.
  1566. this._container.focus();
  1567. var selection = window.getSelection();
  1568. if (selection.rangeCount) {
  1569. var commonAncestorContainer = selection.getRangeAt(0).commonAncestorContainer;
  1570. if (this._container.isSelfOrAncestor(commonAncestorContainer))
  1571. return;
  1572. }
  1573. selection.removeAllRanges();
  1574. var range = document.createRange();
  1575. range.setStart(this._container, 0);
  1576. range.setEnd(this._container, 0);
  1577. selection.addRange(range);
  1578. },
  1579. /**
  1580. * @param {WebInspector.TextRange} range
  1581. */
  1582. markAndRevealRange: function(range)
  1583. {
  1584. if (this._rangeToMark) {
  1585. var markedLine = this._rangeToMark.startLine;
  1586. delete this._rangeToMark;
  1587. // Remove the marked region immediately.
  1588. this.beginDomUpdates();
  1589. var chunk = this.chunkForLine(markedLine);
  1590. var wasExpanded = chunk.expanded();
  1591. chunk.collapse();
  1592. chunk.updateCollapsedLineRow();
  1593. if (wasExpanded)
  1594. chunk.expand();
  1595. this.endDomUpdates();
  1596. }
  1597. if (range) {
  1598. this._rangeToMark = range;
  1599. this.revealLine(range.startLine);
  1600. var chunk = this.makeLineAChunk(range.startLine);
  1601. this._paintLines(chunk.startLine, chunk.startLine + 1);
  1602. if (this._markedRangeElement)
  1603. this._markedRangeElement.scrollIntoViewIfNeeded();
  1604. }
  1605. delete this._markedRangeElement;
  1606. },
  1607. /**
  1608. * @param {number} lineNumber
  1609. */
  1610. highlightLine: function(lineNumber)
  1611. {
  1612. this.clearLineHighlight();
  1613. this._highlightedLine = lineNumber;
  1614. this.revealLine(lineNumber);
  1615. if (!this._readOnly)
  1616. this._restoreSelection(WebInspector.TextRange.createFromLocation(lineNumber, 0), false);
  1617. this.addDecoration(lineNumber, "webkit-highlighted-line");
  1618. },
  1619. clearLineHighlight: function()
  1620. {
  1621. if (typeof this._highlightedLine === "number") {
  1622. this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
  1623. delete this._highlightedLine;
  1624. }
  1625. },
  1626. _freeCachedElements: function()
  1627. {
  1628. this._cachedSpans = [];
  1629. this._cachedTextNodes = [];
  1630. this._cachedRows = [];
  1631. },
  1632. /**
  1633. * @param {boolean} redo
  1634. * @return {boolean}
  1635. */
  1636. _handleUndoRedo: function(redo)
  1637. {
  1638. if (this.readOnly())
  1639. return false;
  1640. this.beginUpdates();
  1641. var range = redo ? this._textModel.redo() : this._textModel.undo();
  1642. this.endUpdates();
  1643. // Restore location post-repaint.
  1644. if (range)
  1645. this._restoreSelection(range, true);
  1646. return true;
  1647. },
  1648. /**
  1649. * @param {boolean} shiftKey
  1650. * @return {boolean}
  1651. */
  1652. _handleTabKeyPress: function(shiftKey)
  1653. {
  1654. if (this.readOnly())
  1655. return false;
  1656. var selection = this.selection();
  1657. if (!selection)
  1658. return false;
  1659. var range = selection.normalize();
  1660. this.beginUpdates();
  1661. var newRange;
  1662. var rangeWasEmpty = range.isEmpty();
  1663. if (shiftKey)
  1664. newRange = this._textModel.unindentLines(range);
  1665. else {
  1666. if (rangeWasEmpty)
  1667. newRange = this._textModel.editRange(range, WebInspector.settings.textEditorIndent.get());
  1668. else
  1669. newRange = this._textModel.indentLines(range);
  1670. }
  1671. this.endUpdates();
  1672. if (rangeWasEmpty)
  1673. newRange.startColumn = newRange.endColumn;
  1674. this._restoreSelection(newRange, true);
  1675. return true;
  1676. },
  1677. _handleEnterKey: function()
  1678. {
  1679. if (this.readOnly())
  1680. return false;
  1681. var range = this.selection();
  1682. if (!range)
  1683. return false;
  1684. range = range.normalize();
  1685. if (range.endColumn === 0)
  1686. return false;
  1687. var line = this._textModel.line(range.startLine);
  1688. var linePrefix = line.substring(0, range.startColumn);
  1689. var indentMatch = linePrefix.match(/^\s+/);
  1690. var currentIndent = indentMatch ? indentMatch[0] : "";
  1691. var textEditorIndent = WebInspector.settings.textEditorIndent.get();
  1692. var indent = WebInspector.TextEditorModel.endsWithBracketRegex.test(linePrefix) ? currentIndent + textEditorIndent : currentIndent;
  1693. if (!indent)
  1694. return false;
  1695. this.beginDomUpdates();
  1696. var lineBreak = this._textModel.lineBreak;
  1697. var newRange;
  1698. if (range.isEmpty() && line.substr(range.endColumn - 1, 2) === '{}') {
  1699. // {|}
  1700. // becomes
  1701. // {
  1702. // |
  1703. // }
  1704. newRange = this._textModel.editRange(range, lineBreak + indent + lineBreak + currentIndent);
  1705. newRange.endLine--;
  1706. newRange.endColumn += textEditorIndent.length;
  1707. } else
  1708. newRange = this._textModel.editRange(range, lineBreak + indent);
  1709. this.endDomUpdates();
  1710. this._restoreSelection(newRange.collapseToEnd(), true);
  1711. return true;
  1712. },
  1713. /**
  1714. * @param {number} lineNumber
  1715. * @param {number} chunkNumber
  1716. * @param {boolean=} createSuffixChunk
  1717. * @return {Object}
  1718. */
  1719. splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
  1720. {
  1721. var selection = this.selection();
  1722. var chunk = WebInspector.TextEditorChunkedPanel.prototype.splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
  1723. this._restoreSelection(selection);
  1724. return chunk;
  1725. },
  1726. beginDomUpdates: function()
  1727. {
  1728. if (!this._domUpdateCoalescingLevel)
  1729. this._detachMutationObserver();
  1730. WebInspector.TextEditorChunkedPanel.prototype.beginDomUpdates.call(this);
  1731. },
  1732. endDomUpdates: function()
  1733. {
  1734. WebInspector.TextEditorChunkedPanel.prototype.endDomUpdates.call(this);
  1735. if (!this._domUpdateCoalescingLevel)
  1736. this._attachMutationObserver();
  1737. },
  1738. buildChunks: function()
  1739. {
  1740. for (var i = 0; i < this._textModel.linesCount; ++i)
  1741. this._textModel.removeAttribute(i, "highlight");
  1742. WebInspector.TextEditorChunkedPanel.prototype.buildChunks.call(this);
  1743. },
  1744. /**
  1745. * @param {number} startLine
  1746. * @param {number} endLine
  1747. * @return {WebInspector.TextEditorMainChunk}
  1748. */
  1749. createNewChunk: function(startLine, endLine)
  1750. {
  1751. return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
  1752. },
  1753. /**
  1754. * @param {number} fromIndex
  1755. * @param {number} toIndex
  1756. */
  1757. expandChunks: function(fromIndex, toIndex)
  1758. {
  1759. var lastChunk = this._textChunks[toIndex - 1];
  1760. var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
  1761. var selection = this.selection();
  1762. this._muteHighlightListener = true;
  1763. this._highlighter.highlight(lastVisibleLine);
  1764. delete this._muteHighlightListener;
  1765. WebInspector.TextEditorChunkedPanel.prototype.expandChunks.call(this, fromIndex, toIndex);
  1766. this._restoreSelection(selection);
  1767. },
  1768. /**
  1769. * @param {number} fromLine
  1770. * @param {number} toLine
  1771. */
  1772. _highlightDataReady: function(fromLine, toLine)
  1773. {
  1774. if (this._muteHighlightListener)
  1775. return;
  1776. this._paintLines(fromLine, toLine, true /*restoreSelection*/);
  1777. },
  1778. /**
  1779. * @param {number} fromLine
  1780. * @param {number} toLine
  1781. * @param {boolean=} restoreSelection
  1782. */
  1783. _paintLines: function(fromLine, toLine, restoreSelection)
  1784. {
  1785. var lineRows = [];
  1786. var chunk;
  1787. for (var lineNumber = fromLine; lineNumber < toLine; ++lineNumber) {
  1788. if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
  1789. chunk = this.chunkForLine(lineNumber);
  1790. var lineRow = chunk.expandedLineRow(lineNumber);
  1791. if (!lineRow)
  1792. continue;
  1793. lineRows.push(lineRow);
  1794. }
  1795. if (lineRows.length === 0)
  1796. return;
  1797. var selection;
  1798. if (restoreSelection)
  1799. selection = this.selection();
  1800. this._paintLineRows(lineRows);
  1801. if (restoreSelection)
  1802. this._restoreSelection(selection);
  1803. },
  1804. /**
  1805. * @param {Array.<Element>} lineRows
  1806. */
  1807. _paintLineRows: function(lineRows)
  1808. {
  1809. var highlight = {};
  1810. this.beginDomUpdates();
  1811. for(var i = 0; i < this._highlightDescriptors.length; ++i) {
  1812. var highlightDescriptor = this._highlightDescriptors[i];
  1813. this._measureHighlightDescriptor(highlight, lineRows, highlightDescriptor);
  1814. }
  1815. for(var i = 0; i < lineRows.length; ++i)
  1816. this._paintLine(lineRows[i], highlight[lineRows[i].lineNumber]);
  1817. this.endDomUpdates();
  1818. },
  1819. /**
  1820. * @param {Object.<number, Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>>} highlight
  1821. * @param {Array.<Element>} lineRows
  1822. * @param {WebInspector.TextEditorMainPanel.HighlightDescriptor} highlightDescriptor
  1823. */
  1824. _measureHighlightDescriptor: function(highlight, lineRows, highlightDescriptor)
  1825. {
  1826. var rowsToMeasure = [];
  1827. for(var i = 0; i < lineRows.length; ++i) {
  1828. var lineRow = lineRows[i];
  1829. var line = this._textModel.line(lineRow.lineNumber);
  1830. var ranges = highlightDescriptor.rangesForLine(lineRow.lineNumber, line);
  1831. if (ranges.length === 0)
  1832. continue;
  1833. for(var j = 0; j < ranges.length; ++j)
  1834. ranges[j].token = "measure-span";
  1835. this._renderRanges(lineRow, line, ranges);
  1836. rowsToMeasure.push(lineRow);
  1837. }
  1838. for(var i = 0; i < rowsToMeasure.length; ++i) {
  1839. var lineRow = rowsToMeasure[i];
  1840. var lineNumber = lineRow.lineNumber;
  1841. var metrics = this._measureSpans(lineRow);
  1842. if (!highlight[lineNumber])
  1843. highlight[lineNumber] = [];
  1844. highlight[lineNumber].push(new WebInspector.TextEditorMainPanel.LineOverlayHighlight(metrics, highlightDescriptor.cssClass()));
  1845. }
  1846. },
  1847. /**
  1848. * @param {Element} lineRow
  1849. * @return {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>}
  1850. */
  1851. _measureSpans: function(lineRow)
  1852. {
  1853. var spans = lineRow.getElementsByClassName("webkit-measure-span");
  1854. var metrics = [];
  1855. for(var i = 0; i < spans.length; ++i)
  1856. metrics.push(new WebInspector.TextEditorMainPanel.ElementMetrics(spans[i]));
  1857. return metrics;
  1858. },
  1859. /**
  1860. * @param {Element} lineRow
  1861. * @param {WebInspector.TextEditorMainPanel.LineOverlayHighlight} highlight
  1862. */
  1863. _appendOverlayHighlight: function(lineRow, highlight)
  1864. {
  1865. var metrics = highlight.metrics;
  1866. var cssClass = highlight.cssClass;
  1867. for(var i = 0; i < metrics.length; ++i) {
  1868. var highlightSpan = document.createElement("span");
  1869. highlightSpan._isOverlayHighlightElement = true;
  1870. highlightSpan.addStyleClass(cssClass);
  1871. highlightSpan.style.left = metrics[i].left + "px";
  1872. highlightSpan.style.width = metrics[i].width + "px";
  1873. highlightSpan.style.height = metrics[i].height + "px";
  1874. highlightSpan.addStyleClass("text-editor-overlay-highlight");
  1875. lineRow.insertBefore(highlightSpan, lineRow.decorationsElement);
  1876. }
  1877. },
  1878. /**
  1879. * @param {Element} lineRow
  1880. * @param {string} line
  1881. * @param {Array.<{startColumn: number, endColumn: number, token: ?string}>} ranges
  1882. * @param {boolean=} splitWhitespaceSequences
  1883. */
  1884. _renderRanges: function(lineRow, line, ranges, splitWhitespaceSequences)
  1885. {
  1886. var decorationsElement = lineRow.decorationsElement;
  1887. if (!decorationsElement)
  1888. lineRow.removeChildren();
  1889. else {
  1890. while (true) {
  1891. var child = lineRow.firstChild;
  1892. if (!child || child === decorationsElement)
  1893. break;
  1894. lineRow.removeChild(child);
  1895. }
  1896. }
  1897. if (!line)
  1898. lineRow.insertBefore(document.createElement("br"), decorationsElement);
  1899. var plainTextStart = 0;
  1900. for(var i = 0; i < ranges.length; i++) {
  1901. var rangeStart = ranges[i].startColumn;
  1902. var rangeEnd = ranges[i].endColumn;
  1903. if (plainTextStart < rangeStart) {
  1904. this._insertSpanBefore(lineRow, decorationsElement, line.substring(plainTextStart, rangeStart));
  1905. }
  1906. if (splitWhitespaceSequences && ranges[i].token === "whitespace")
  1907. this._renderWhitespaceCharsWithFixedSizeSpans(lineRow, decorationsElement, rangeEnd - rangeStart + 1);
  1908. else
  1909. this._insertSpanBefore(lineRow, decorationsElement, line.substring(rangeStart, rangeEnd + 1), ranges[i].token ? "webkit-" + ranges[i].token : "");
  1910. plainTextStart = rangeEnd + 1;
  1911. }
  1912. if (plainTextStart < line.length) {
  1913. this._insertSpanBefore(lineRow, decorationsElement, line.substring(plainTextStart, line.length));
  1914. }
  1915. },
  1916. /**
  1917. * @param {Element} lineRow
  1918. * @param {Element} decorationsElement
  1919. * @param {number} length
  1920. */
  1921. _renderWhitespaceCharsWithFixedSizeSpans: function(lineRow, decorationsElement, length)
  1922. {
  1923. for (var whitespaceLength = 16; whitespaceLength > 0; whitespaceLength >>= 1) {
  1924. var cssClass = "webkit-whitespace webkit-whitespace-" + whitespaceLength;
  1925. for (; length >= whitespaceLength; length -= whitespaceLength)
  1926. this._insertSpanBefore(lineRow, decorationsElement, WebInspector.TextEditorMainPanel._ConsecutiveWhitespaceChars[whitespaceLength], cssClass);
  1927. }
  1928. },
  1929. /**
  1930. * @param {Element} lineRow
  1931. * @param {Array.<WebInspector.TextEditorMainPanel.LineOverlayHighlight>} overlayHighlight
  1932. */
  1933. _paintLine: function(lineRow, overlayHighlight)
  1934. {
  1935. var lineNumber = lineRow.lineNumber;
  1936. this.beginDomUpdates();
  1937. try {
  1938. var syntaxHighlight = this._textModel.getAttribute(lineNumber, "highlight");
  1939. var line = this._textModel.line(lineNumber);
  1940. var ranges = syntaxHighlight ? syntaxHighlight.ranges : [];
  1941. this._renderRanges(lineRow, line, ranges, this._showWhitespace);
  1942. if (overlayHighlight)
  1943. for(var i = 0; i < overlayHighlight.length; ++i)
  1944. this._appendOverlayHighlight(lineRow, overlayHighlight[i]);
  1945. } finally {
  1946. if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
  1947. this._markedRangeElement = WebInspector.highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
  1948. this.endDomUpdates();
  1949. }
  1950. },
  1951. /**
  1952. * @param {Element} lineRow
  1953. */
  1954. _releaseLinesHighlight: function(lineRow)
  1955. {
  1956. if (!lineRow)
  1957. return;
  1958. if ("spans" in lineRow) {
  1959. var spans = lineRow.spans;
  1960. for (var j = 0; j < spans.length; ++j)
  1961. this._cachedSpans.push(spans[j]);
  1962. delete lineRow.spans;
  1963. }
  1964. if ("textNodes" in lineRow) {
  1965. var textNodes = lineRow.textNodes;
  1966. for (var j = 0; j < textNodes.length; ++j)
  1967. this._cachedTextNodes.push(textNodes[j]);
  1968. delete lineRow.textNodes;
  1969. }
  1970. this._cachedRows.push(lineRow);
  1971. },
  1972. /**
  1973. * @param {?Node=} lastUndamagedLineRow
  1974. * @return {WebInspector.TextRange}
  1975. */
  1976. selection: function(lastUndamagedLineRow)
  1977. {
  1978. var selection = window.getSelection();
  1979. if (!selection.rangeCount)
  1980. return null;
  1981. // Selection may be outside of the editor.
  1982. if (!this._container.isAncestor(selection.anchorNode) || !this._container.isAncestor(selection.focusNode))
  1983. return null;
  1984. // Selection may be inside one of decorations.
  1985. if (selection.focusNode.enclosingNodeOrSelfWithClass("webkit-line-decorations", this._container))
  1986. return null;
  1987. var start = this._selectionToPosition(selection.anchorNode, selection.anchorOffset, lastUndamagedLineRow);
  1988. var end = selection.isCollapsed ? start : this._selectionToPosition(selection.focusNode, selection.focusOffset, lastUndamagedLineRow);
  1989. return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
  1990. },
  1991. lastSelection: function()
  1992. {
  1993. return this._lastSelection;
  1994. },
  1995. /**
  1996. * @param {boolean=} scrollIntoView
  1997. */
  1998. _restoreSelection: function(range, scrollIntoView)
  1999. {
  2000. if (!range)
  2001. return;
  2002. var start = this._positionToSelection(range.startLine, range.startColumn);
  2003. var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
  2004. window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
  2005. if (scrollIntoView) {
  2006. for (var node = end.container; node; node = node.parentElement) {
  2007. if (node.scrollIntoViewIfNeeded) {
  2008. node.scrollIntoViewIfNeeded();
  2009. break;
  2010. }
  2011. }
  2012. }
  2013. this._lastSelection = range;
  2014. },
  2015. /**
  2016. * @param {Node} container
  2017. * @param {number} offset
  2018. * @param {?Node=} lastUndamagedLineRow
  2019. * @return {{line: number, column: number}}
  2020. */
  2021. _selectionToPosition: function(container, offset, lastUndamagedLineRow)
  2022. {
  2023. if (container === this._container && offset === 0)
  2024. return { line: 0, column: 0 };
  2025. if (container === this._container && offset === 1)
  2026. return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
  2027. // This method can be called on the damaged DOM (when DOM does not match model).
  2028. // We need to start counting lines from the first undamaged line if it is given.
  2029. var lineNumber;
  2030. var column = 0;
  2031. var node;
  2032. var scopeNode;
  2033. if (lastUndamagedLineRow === null) {
  2034. // Last undamaged row is given, but is null - force traverse from the beginning
  2035. node = this._container.firstChild;
  2036. scopeNode = this._container;
  2037. lineNumber = 0;
  2038. } else {
  2039. var lineRow = this._enclosingLineRowOrSelf(container);
  2040. if (!lastUndamagedLineRow || (typeof lineRow.lineNumber === "number" && lineRow.lineNumber <= lastUndamagedLineRow.lineNumber)) {
  2041. // DOM is consistent (or we belong to the first damaged row)- lookup the row we belong to and start with it.
  2042. node = lineRow;
  2043. scopeNode = node;
  2044. lineNumber = node.lineNumber;
  2045. } else {
  2046. // Start with the node following undamaged row. It corresponds to lineNumber + 1.
  2047. node = lastUndamagedLineRow.nextSibling;
  2048. scopeNode = this._container;
  2049. lineNumber = lastUndamagedLineRow.lineNumber + 1;
  2050. }
  2051. }
  2052. // Fast return the line start.
  2053. if (container === node && offset === 0)
  2054. return { line: lineNumber, column: 0 };
  2055. // Traverse text and increment lineNumber / column.
  2056. for (; node && node !== container; node = node.traverseNextNode(scopeNode)) {
  2057. if (node.nodeName.toLowerCase() === "br") {
  2058. lineNumber++;
  2059. column = 0;
  2060. } else if (node.nodeType === Node.TEXT_NODE) {
  2061. var text = node.textContent;
  2062. for (var i = 0; i < text.length; ++i) {
  2063. if (text.charAt(i) === "\n") {
  2064. lineNumber++;
  2065. column = 0;
  2066. } else
  2067. column++;
  2068. }
  2069. }
  2070. }
  2071. // We reached our container node, traverse within itself until we reach given offset.
  2072. if (node === container && offset) {
  2073. var text = node.textContent;
  2074. // In case offset == 1 and lineRow is a chunk div, we need to traverse it all.
  2075. var textOffset = (node._chunk && offset === 1) ? text.length : offset;
  2076. for (var i = 0; i < textOffset; ++i) {
  2077. if (text.charAt(i) === "\n") {
  2078. lineNumber++;
  2079. column = 0;
  2080. } else
  2081. column++;
  2082. }
  2083. }
  2084. return { line: lineNumber, column: column };
  2085. },
  2086. /**
  2087. * @param {number} line
  2088. * @param {number} column
  2089. * @return {{container: Element, offset: number}}
  2090. */
  2091. _positionToSelection: function(line, column)
  2092. {
  2093. var chunk = this.chunkForLine(line);
  2094. // One-lined collapsed chunks may still stay highlighted.
  2095. var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.expandedLineRow(line);
  2096. if (lineRow)
  2097. var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
  2098. else {
  2099. var offset = column;
  2100. for (var i = chunk.startLine; i < line && i < this._textModel.linesCount; ++i)
  2101. offset += this._textModel.lineLength(i) + 1; // \n
  2102. lineRow = chunk.element;
  2103. if (lineRow.firstChild)
  2104. var rangeBoundary = { container: lineRow.firstChild, offset: offset };
  2105. else
  2106. var rangeBoundary = { container: lineRow, offset: 0 };
  2107. }
  2108. return rangeBoundary;
  2109. },
  2110. /**
  2111. * @param {Node} element
  2112. * @return {?Node}
  2113. */
  2114. _enclosingLineRowOrSelf: function(element)
  2115. {
  2116. var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
  2117. if (lineRow)
  2118. return lineRow;
  2119. for (lineRow = element; lineRow; lineRow = lineRow.parentElement) {
  2120. if (lineRow.parentElement === this._container)
  2121. return lineRow;
  2122. }
  2123. return null;
  2124. },
  2125. /**
  2126. * @param {Element} element
  2127. * @param {Element} oldChild
  2128. * @param {string} content
  2129. * @param {string=} className
  2130. */
  2131. _insertSpanBefore: function(element, oldChild, content, className)
  2132. {
  2133. if (className === "html-resource-link" || className === "html-external-link") {
  2134. element.insertBefore(this._createLink(content, className === "html-external-link"), oldChild);
  2135. return;
  2136. }
  2137. var span = this._cachedSpans.pop() || document.createElement("span");
  2138. if (!className)
  2139. span.removeAttribute("class");
  2140. else
  2141. span.className = className;
  2142. if (WebInspector.FALSE) // For paint debugging.
  2143. span.addStyleClass("debug-fadeout");
  2144. span.textContent = content;
  2145. element.insertBefore(span, oldChild);
  2146. if (!("spans" in element))
  2147. element.spans = [];
  2148. element.spans.push(span);
  2149. },
  2150. /**
  2151. * @param {Element} element
  2152. * @param {Element} oldChild
  2153. * @param {string} text
  2154. */
  2155. _insertTextNodeBefore: function(element, oldChild, text)
  2156. {
  2157. var textNode = this._cachedTextNodes.pop();
  2158. if (textNode)
  2159. textNode.nodeValue = text;
  2160. else
  2161. textNode = document.createTextNode(text);
  2162. element.insertBefore(textNode, oldChild);
  2163. if (!("textNodes" in element))
  2164. element.textNodes = [];
  2165. element.textNodes.push(textNode);
  2166. },
  2167. /**
  2168. * @param {string} content
  2169. * @param {boolean} isExternal
  2170. * @return {Element}
  2171. */
  2172. _createLink: function(content, isExternal)
  2173. {
  2174. var quote = content.charAt(0);
  2175. if (content.length > 1 && (quote === "\"" || quote === "'"))
  2176. content = content.substring(1, content.length - 1);
  2177. else
  2178. quote = null;
  2179. var span = document.createElement("span");
  2180. span.className = "webkit-html-attribute-value";
  2181. if (quote)
  2182. span.appendChild(document.createTextNode(quote));
  2183. span.appendChild(this._delegate.createLink(content, isExternal));
  2184. if (quote)
  2185. span.appendChild(document.createTextNode(quote));
  2186. return span;
  2187. },
  2188. /**
  2189. * @param {Array.<WebKitMutation>} mutations
  2190. */
  2191. _handleMutations: function(mutations)
  2192. {
  2193. if (this._readOnly) {
  2194. delete this._keyDownCode;
  2195. return;
  2196. }
  2197. // Annihilate noop BR addition + removal that takes place upon line removal.
  2198. var filteredMutations = mutations.slice();
  2199. var addedBRs = new Map();
  2200. for (var i = 0; i < mutations.length; ++i) {
  2201. var mutation = mutations[i];
  2202. if (mutation.type !== "childList")
  2203. continue;
  2204. if (mutation.addedNodes.length === 1 && mutation.addedNodes[0].nodeName === "BR")
  2205. addedBRs.put(mutation.addedNodes[0], mutation);
  2206. else if (mutation.removedNodes.length === 1 && mutation.removedNodes[0].nodeName === "BR") {
  2207. var noopMutation = addedBRs.get(mutation.removedNodes[0]);
  2208. if (noopMutation) {
  2209. filteredMutations.remove(mutation);
  2210. filteredMutations.remove(noopMutation);
  2211. }
  2212. }
  2213. }
  2214. var dirtyLines;
  2215. for (var i = 0; i < filteredMutations.length; ++i) {
  2216. var mutation = filteredMutations[i];
  2217. var changedNodes = [];
  2218. if (mutation.type === "childList" && mutation.addedNodes.length)
  2219. changedNodes = Array.prototype.slice.call(mutation.addedNodes);
  2220. else if (mutation.type === "childList" && mutation.removedNodes.length)
  2221. changedNodes = Array.prototype.slice.call(mutation.removedNodes);
  2222. changedNodes.push(mutation.target);
  2223. for (var j = 0; j < changedNodes.length; ++j) {
  2224. var lines = this._collectDirtyLines(mutation, changedNodes[j]);
  2225. if (!lines)
  2226. continue;
  2227. if (!dirtyLines) {
  2228. dirtyLines = lines;
  2229. continue;
  2230. }
  2231. dirtyLines.start = Math.min(dirtyLines.start, lines.start);
  2232. dirtyLines.end = Math.max(dirtyLines.end, lines.end);
  2233. }
  2234. }
  2235. if (dirtyLines) {
  2236. delete this._rangeToMark;
  2237. this._applyDomUpdates(dirtyLines);
  2238. }
  2239. this._assertDOMMatchesTextModel();
  2240. delete this._keyDownCode;
  2241. },
  2242. /**
  2243. * @param {WebKitMutation} mutation
  2244. * @param {Node} target
  2245. * @return {?Object}
  2246. */
  2247. _collectDirtyLines: function(mutation, target)
  2248. {
  2249. var lineRow = this._enclosingLineRowOrSelf(target);
  2250. if (!lineRow)
  2251. return null;
  2252. if (lineRow.decorationsElement && lineRow.decorationsElement.isSelfOrAncestor(target)) {
  2253. if (this._syncDecorationsForLineListener)
  2254. this._syncDecorationsForLineListener(lineRow.lineNumber);
  2255. return null;
  2256. }
  2257. if (typeof lineRow.lineNumber !== "number")
  2258. return null;
  2259. var startLine = lineRow.lineNumber;
  2260. var endLine = lineRow._chunk ? lineRow._chunk.endLine - 1 : lineRow.lineNumber;
  2261. return { start: startLine, end: endLine };
  2262. },
  2263. /**
  2264. * @param {Object} dirtyLines
  2265. */
  2266. _applyDomUpdates: function(dirtyLines)
  2267. {
  2268. var lastUndamagedLineNumber = dirtyLines.start - 1; // Can be -1
  2269. var firstUndamagedLineNumber = dirtyLines.end + 1; // Can be this._textModel.linesCount
  2270. var lastUndamagedLineChunk = lastUndamagedLineNumber >= 0 ? this._textChunks[this.chunkNumberForLine(lastUndamagedLineNumber)] : null;
  2271. var firstUndamagedLineChunk = firstUndamagedLineNumber < this._textModel.linesCount ? this._textChunks[this.chunkNumberForLine(firstUndamagedLineNumber)] : null;
  2272. var collectLinesFromNode = lastUndamagedLineChunk ? lastUndamagedLineChunk.lineRowContainingLine(lastUndamagedLineNumber) : null;
  2273. var collectLinesToNode = firstUndamagedLineChunk ? firstUndamagedLineChunk.lineRowContainingLine(firstUndamagedLineNumber) : null;
  2274. var lines = this._collectLinesFromDOM(collectLinesFromNode, collectLinesToNode);
  2275. var startLine = dirtyLines.start;
  2276. var endLine = dirtyLines.end;
  2277. var originalSelection = this._lastSelection;
  2278. var editInfo = this._guessEditRangeBasedOnSelection(startLine, endLine, lines);
  2279. if (!editInfo) {
  2280. if (WebInspector.debugDefaultTextEditor)
  2281. console.warn("Falling back to expensive edit");
  2282. var range = new WebInspector.TextRange(startLine, 0, endLine, this._textModel.lineLength(endLine));
  2283. if (!lines.length) {
  2284. // Entire damaged area has collapsed. Replace everything between start and end lines with nothing.
  2285. editInfo = new WebInspector.DefaultTextEditor.EditInfo(this._textModel.growRangeRight(range), "");
  2286. } else
  2287. editInfo = new WebInspector.DefaultTextEditor.EditInfo(range, lines.join("\n"));
  2288. }
  2289. var selection = this.selection(collectLinesFromNode);
  2290. // Unindent after block
  2291. if (editInfo.text === "}" && editInfo.range.isEmpty() && selection.isEmpty() && !this._textModel.line(editInfo.range.endLine).trim()) {
  2292. var offset = this._closingBlockOffset(editInfo.range);
  2293. if (offset >= 0) {
  2294. editInfo.range.startColumn = offset;
  2295. selection.startColumn = offset + 1;
  2296. selection.endColumn = offset + 1;
  2297. }
  2298. }
  2299. this._textModel.editRange(editInfo.range, editInfo.text, originalSelection);
  2300. this._restoreSelection(selection);
  2301. },
  2302. /**
  2303. * @param {number} startLine
  2304. * @param {number} endLine
  2305. * @param {Array.<string>} lines
  2306. * @return {?WebInspector.DefaultTextEditor.EditInfo}
  2307. */
  2308. _guessEditRangeBasedOnSelection: function(startLine, endLine, lines)
  2309. {
  2310. // Analyze input data
  2311. var textInputData = this._textInputData;
  2312. delete this._textInputData;
  2313. var isBackspace = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Backspace.code;
  2314. var isDelete = this._keyDownCode === WebInspector.KeyboardShortcut.Keys.Delete.code;
  2315. if (!textInputData && (isDelete || isBackspace))
  2316. textInputData = "";
  2317. // Return if there is no input data or selection
  2318. if (typeof textInputData === "undefined" || !this._lastSelection)
  2319. return null;
  2320. // Adjust selection based on the keyboard actions (grow for backspace, etc.).
  2321. textInputData = textInputData || "";
  2322. var range = this._lastSelection.normalize();
  2323. if (isBackspace && range.isEmpty())
  2324. range = this._textModel.growRangeLeft(range);
  2325. else if (isDelete && range.isEmpty())
  2326. range = this._textModel.growRangeRight(range);
  2327. // Test that selection intersects damaged lines
  2328. if (startLine > range.endLine || endLine < range.startLine)
  2329. return null;
  2330. var replacementLineCount = textInputData.split("\n").length - 1;
  2331. var lineCountDelta = replacementLineCount - range.linesCount;
  2332. if (startLine + lines.length - endLine - 1 !== lineCountDelta)
  2333. return null;
  2334. // Clone text model of the size that fits both: selection before edit and the damaged lines after edit.
  2335. var cloneFromLine = Math.min(range.startLine, startLine);
  2336. var postLastLine = startLine + lines.length + lineCountDelta;
  2337. var cloneToLine = Math.min(Math.max(postLastLine, range.endLine) + 1, this._textModel.linesCount);
  2338. var domModel = this._textModel.slice(cloneFromLine, cloneToLine);
  2339. domModel.editRange(range.shift(-cloneFromLine), textInputData);
  2340. // Then we'll test if this new model matches the DOM lines.
  2341. for (var i = 0; i < lines.length; ++i) {
  2342. if (domModel.line(i + startLine - cloneFromLine) !== lines[i])
  2343. return null;
  2344. }
  2345. return new WebInspector.DefaultTextEditor.EditInfo(range, textInputData);
  2346. },
  2347. _assertDOMMatchesTextModel: function()
  2348. {
  2349. if (!WebInspector.debugDefaultTextEditor)
  2350. return;
  2351. console.assert(this.element.innerText === this._textModel.text() + "\n", "DOM does not match model.");
  2352. for (var lineRow = this._container.firstChild; lineRow; lineRow = lineRow.nextSibling) {
  2353. var lineNumber = lineRow.lineNumber;
  2354. if (typeof lineNumber !== "number") {
  2355. console.warn("No line number on line row");
  2356. continue;
  2357. }
  2358. if (lineRow._chunk) {
  2359. var chunk = lineRow._chunk;
  2360. console.assert(lineNumber === chunk.startLine);
  2361. var chunkText = this._textModel.copyRange(new WebInspector.TextRange(chunk.startLine, 0, chunk.endLine - 1, this._textModel.lineLength(chunk.endLine - 1)));
  2362. if (chunkText !== lineRow.textContent)
  2363. console.warn("Chunk is not matching: %d %O", lineNumber, lineRow);
  2364. } else if (this._textModel.line(lineNumber) !== lineRow.textContent)
  2365. console.warn("Line is not matching: %d %O", lineNumber, lineRow);
  2366. }
  2367. },
  2368. /**
  2369. * @param {WebInspector.TextRange} oldRange
  2370. * @return {number}
  2371. */
  2372. _closingBlockOffset: function(oldRange)
  2373. {
  2374. var leftBrace = this._braceMatcher.findLeftCandidate(oldRange.startLine, oldRange.startColumn);
  2375. if (!leftBrace || leftBrace.token !== "block-start")
  2376. return -1;
  2377. var lineContent = this._textModel.line(leftBrace.lineNumber);
  2378. return lineContent.length - lineContent.trimLeft().length;
  2379. },
  2380. /**
  2381. * @param {WebInspector.TextRange} oldRange
  2382. * @param {WebInspector.TextRange} newRange
  2383. */
  2384. textChanged: function(oldRange, newRange)
  2385. {
  2386. this.beginDomUpdates();
  2387. this._removeDecorationsInRange(oldRange);
  2388. this._updateChunksForRanges(oldRange, newRange);
  2389. this._updateHighlightsForRange(newRange);
  2390. this.endDomUpdates();
  2391. },
  2392. /**
  2393. * @param {WebInspector.TextRange} range
  2394. */
  2395. _removeDecorationsInRange: function(range)
  2396. {
  2397. for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
  2398. var chunk = this._textChunks[i];
  2399. if (chunk.startLine > range.endLine)
  2400. break;
  2401. chunk.removeAllDecorations();
  2402. }
  2403. },
  2404. /**
  2405. * @param {WebInspector.TextRange} oldRange
  2406. * @param {WebInspector.TextRange} newRange
  2407. */
  2408. _updateChunksForRanges: function(oldRange, newRange)
  2409. {
  2410. var firstDamagedChunkNumber = this.chunkNumberForLine(oldRange.startLine);
  2411. var lastDamagedChunkNumber = firstDamagedChunkNumber;
  2412. while (lastDamagedChunkNumber + 1 < this._textChunks.length) {
  2413. if (this._textChunks[lastDamagedChunkNumber + 1].startLine > oldRange.endLine)
  2414. break;
  2415. ++lastDamagedChunkNumber;
  2416. }
  2417. var firstDamagedChunk = this._textChunks[firstDamagedChunkNumber];
  2418. var lastDamagedChunk = this._textChunks[lastDamagedChunkNumber];
  2419. var linesDiff = newRange.linesCount - oldRange.linesCount;
  2420. // First, detect chunks that have not been modified and simply shift them.
  2421. if (linesDiff) {
  2422. for (var chunkNumber = lastDamagedChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
  2423. this._textChunks[chunkNumber].startLine += linesDiff;
  2424. }
  2425. // Remove damaged chunks from DOM and from textChunks model.
  2426. var lastUndamagedChunk = firstDamagedChunkNumber > 0 ? this._textChunks[firstDamagedChunkNumber - 1] : null;
  2427. var firstUndamagedChunk = lastDamagedChunkNumber + 1 < this._textChunks.length ? this._textChunks[lastDamagedChunkNumber + 1] : null;
  2428. var removeDOMFromNode = lastUndamagedChunk ? lastUndamagedChunk.lastElement().nextSibling : this._container.firstChild;
  2429. var removeDOMToNode = firstUndamagedChunk ? firstUndamagedChunk.firstElement() : null;
  2430. // Fast case - patch single expanded chunk that did not grow / shrink during edit.
  2431. if (!linesDiff && firstDamagedChunk === lastDamagedChunk && firstDamagedChunk._expandedLineRows) {
  2432. var lastUndamagedLineRow = lastDamagedChunk.expandedLineRow(oldRange.startLine - 1);
  2433. var firstUndamagedLineRow = firstDamagedChunk.expandedLineRow(oldRange.endLine + 1);
  2434. var localRemoveDOMFromNode = lastUndamagedLineRow ? lastUndamagedLineRow.nextSibling : removeDOMFromNode;
  2435. var localRemoveDOMToNode = firstUndamagedLineRow || removeDOMToNode;
  2436. removeSubsequentNodes(localRemoveDOMFromNode, localRemoveDOMToNode);
  2437. for (var i = newRange.startLine; i < newRange.endLine + 1; ++i) {
  2438. var row = firstDamagedChunk._createRow(i);
  2439. firstDamagedChunk._expandedLineRows[i - firstDamagedChunk.startLine] = row;
  2440. this._container.insertBefore(row, localRemoveDOMToNode);
  2441. }
  2442. firstDamagedChunk.updateCollapsedLineRow();
  2443. this._assertDOMMatchesTextModel();
  2444. return;
  2445. }
  2446. removeSubsequentNodes(removeDOMFromNode, removeDOMToNode);
  2447. this._textChunks.splice(firstDamagedChunkNumber, lastDamagedChunkNumber - firstDamagedChunkNumber + 1);
  2448. // Compute damaged chunks span
  2449. var startLine = firstDamagedChunk.startLine;
  2450. var endLine = lastDamagedChunk.endLine + linesDiff;
  2451. var lineSpan = endLine - startLine;
  2452. // Re-create chunks for damaged area.
  2453. var insertionIndex = firstDamagedChunkNumber;
  2454. var chunkSize = Math.ceil(lineSpan / Math.ceil(lineSpan / this._defaultChunkSize));
  2455. for (var i = startLine; i < endLine; i += chunkSize) {
  2456. var chunk = this.createNewChunk(i, Math.min(endLine, i + chunkSize));
  2457. this._textChunks.splice(insertionIndex++, 0, chunk);
  2458. this._container.insertBefore(chunk.element, removeDOMToNode);
  2459. }
  2460. this._assertDOMMatchesTextModel();
  2461. },
  2462. /**
  2463. * @param {WebInspector.TextRange} range
  2464. */
  2465. _updateHighlightsForRange: function(range)
  2466. {
  2467. var visibleFrom = this.scrollTop();
  2468. var visibleTo = visibleFrom + this.clientHeight();
  2469. var result = this.findVisibleChunks(visibleFrom, visibleTo);
  2470. var chunk = this._textChunks[result.end - 1];
  2471. var lastVisibleLine = chunk.startLine + chunk.linesCount;
  2472. lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
  2473. lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
  2474. var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
  2475. if (!updated) {
  2476. // Highlights for the chunks below are invalid, so just collapse them.
  2477. for (var i = this.chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
  2478. this._textChunks[i].collapse();
  2479. }
  2480. this.repaintAll();
  2481. },
  2482. /**
  2483. * @param {Node} from
  2484. * @param {Node} to
  2485. * @return {Array.<string>}
  2486. */
  2487. _collectLinesFromDOM: function(from, to)
  2488. {
  2489. var textContents = [];
  2490. var hasContent = false;
  2491. for (var node = from ? from.nextSibling : this._container; node && node !== to; node = node.traverseNextNode(this._container)) {
  2492. // Skip all children of the decoration container and overlay highlight spans.
  2493. while (node && node !== to && (node._isDecorationsElement || node._isOverlayHighlightElement))
  2494. node = node.nextSibling;
  2495. if (!node || node === to)
  2496. break;
  2497. hasContent = true;
  2498. if (node.nodeName.toLowerCase() === "br")
  2499. textContents.push("\n");
  2500. else if (node.nodeType === Node.TEXT_NODE)
  2501. textContents.push(node.textContent);
  2502. }
  2503. if (!hasContent)
  2504. return [];
  2505. var textContent = textContents.join("");
  2506. // The last \n (if any) does not "count" in a DIV.
  2507. textContent = textContent.replace(/\n$/, "");
  2508. return textContent.split("\n");
  2509. },
  2510. /**
  2511. * @param {Event} event
  2512. */
  2513. _handleSelectionChange: function(event)
  2514. {
  2515. var textRange = this.selection();
  2516. if (textRange)
  2517. this._lastSelection = textRange;
  2518. this._tokenHighlighter.handleSelectionChange(textRange);
  2519. this._braceHighlighter.handleSelectionChange(textRange);
  2520. this._delegate.selectionChanged(textRange);
  2521. },
  2522. /**
  2523. * @param {Event} event
  2524. */
  2525. _handleTextInput: function(event)
  2526. {
  2527. this._textInputData = event.data;
  2528. },
  2529. /**
  2530. * @param {number} shortcutKey
  2531. * @param {Event} event
  2532. */
  2533. handleKeyDown: function(shortcutKey, event)
  2534. {
  2535. var handler = this._shortcuts[shortcutKey];
  2536. if (handler && handler()) {
  2537. event.consume(true);
  2538. return;
  2539. }
  2540. this._keyDownCode = event.keyCode;
  2541. },
  2542. /**
  2543. * @param {Event} event
  2544. */
  2545. _handleCut: function(event)
  2546. {
  2547. this._keyDownCode = WebInspector.KeyboardShortcut.Keys.Delete.code;
  2548. },
  2549. /**
  2550. * @param {number} scrollTop
  2551. * @param {number} clientHeight
  2552. * @param {number} chunkSize
  2553. */
  2554. overrideViewportForTest: function(scrollTop, clientHeight, chunkSize)
  2555. {
  2556. this._scrollTopOverrideForTest = scrollTop;
  2557. this._clientHeightOverrideForTest = clientHeight;
  2558. this._defaultChunkSize = chunkSize;
  2559. },
  2560. __proto__: WebInspector.TextEditorChunkedPanel.prototype
  2561. }
  2562. /**
  2563. * @interface
  2564. */
  2565. WebInspector.TextEditorMainPanel.HighlightDescriptor = function() { }
  2566. WebInspector.TextEditorMainPanel.HighlightDescriptor.prototype = {
  2567. /**
  2568. * @param {number} lineNumber
  2569. * @param {string} line
  2570. * @return {boolean}
  2571. */
  2572. affectsLine: function(lineNumber, line) { return false; },
  2573. /**
  2574. * @param {number} lineNumber
  2575. * @param {string} line
  2576. * @return {Array.<{startColumn: number, endColumn: number}>}
  2577. */
  2578. rangesForLine: function(lineNumber, line) { return []; },
  2579. /**
  2580. * @return {string}
  2581. */
  2582. cssClass: function() { return ""; },
  2583. }
  2584. /**
  2585. * @constructor
  2586. * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor}
  2587. */
  2588. WebInspector.TextEditorMainPanel.RegexHighlightDescriptor = function(regex, cssClass)
  2589. {
  2590. this._cssClass = cssClass;
  2591. this._regex = regex;
  2592. }
  2593. WebInspector.TextEditorMainPanel.RegexHighlightDescriptor.prototype = {
  2594. /**
  2595. * @param {number} lineNumber
  2596. * @param {string} line
  2597. * @return {boolean}
  2598. */
  2599. affectsLine: function(lineNumber, line)
  2600. {
  2601. this._regex.lastIndex = 0;
  2602. return this._regex.test(line);
  2603. },
  2604. /**
  2605. * @param {number} lineNumber
  2606. * @param {string} line
  2607. * @return {Array.<{startColumn: number, endColumn: number}>}
  2608. */
  2609. rangesForLine: function(lineNumber, line)
  2610. {
  2611. var ranges = [];
  2612. var regexResult;
  2613. this._regex.lastIndex = 0;
  2614. while (regexResult = this._regex.exec(line)) {
  2615. ranges.push({
  2616. startColumn: regexResult.index,
  2617. endColumn: regexResult.index + regexResult[0].length - 1
  2618. });
  2619. }
  2620. return ranges;
  2621. },
  2622. /**
  2623. * @return {string}
  2624. */
  2625. cssClass: function()
  2626. {
  2627. return this._cssClass;
  2628. }
  2629. }
  2630. /**
  2631. * @constructor
  2632. * @implements {WebInspector.TextEditorMainPanel.HighlightDescriptor}
  2633. * @param {WebInspector.TextRange} range
  2634. * @param {string} cssClass
  2635. */
  2636. WebInspector.TextEditorMainPanel.RangeHighlightDescriptor = function(range, cssClass)
  2637. {
  2638. this._cssClass = cssClass;
  2639. this._range = range;
  2640. }
  2641. WebInspector.TextEditorMainPanel.RangeHighlightDescriptor.prototype = {
  2642. /**
  2643. * @param {number} lineNumber
  2644. * @param {string} line
  2645. * @return {boolean}
  2646. */
  2647. affectsLine: function(lineNumber, line)
  2648. {
  2649. return this._range.startLine <= lineNumber && lineNumber <= this._range.endLine && line.length > 0;
  2650. },
  2651. /**
  2652. * @param {number} lineNumber
  2653. * @param {string} line
  2654. * @return {Array.<{startColumn: number, endColumn: number}>}
  2655. */
  2656. rangesForLine: function(lineNumber, line)
  2657. {
  2658. if (!this.affectsLine(lineNumber, line))
  2659. return [];
  2660. var startColumn = lineNumber === this._range.startLine ? this._range.startColumn : 0;
  2661. var endColumn = lineNumber === this._range.endLine ? Math.min(this._range.endColumn, line.length) : line.length;
  2662. return [{
  2663. startColumn: startColumn,
  2664. endColumn: endColumn
  2665. }];
  2666. },
  2667. /**
  2668. * @return {string}
  2669. */
  2670. cssClass: function()
  2671. {
  2672. return this._cssClass;
  2673. }
  2674. }
  2675. /**
  2676. * @constructor
  2677. * @param {Element} element
  2678. */
  2679. WebInspector.TextEditorMainPanel.ElementMetrics = function(element)
  2680. {
  2681. this.width = element.offsetWidth;
  2682. this.height = element.offsetHeight;
  2683. this.left = element.offsetLeft;
  2684. }
  2685. /**
  2686. * @constructor
  2687. * @param {Array.<WebInspector.TextEditorMainPanel.ElementMetrics>} metrics
  2688. * @param {string} cssClass
  2689. */
  2690. WebInspector.TextEditorMainPanel.LineOverlayHighlight = function(metrics, cssClass)
  2691. {
  2692. this.metrics = metrics;
  2693. this.cssClass = cssClass;
  2694. }
  2695. /**
  2696. * @constructor
  2697. * @param {WebInspector.TextEditorChunkedPanel} chunkedPanel
  2698. * @param {number} startLine
  2699. * @param {number} endLine
  2700. */
  2701. WebInspector.TextEditorMainChunk = function(chunkedPanel, startLine, endLine)
  2702. {
  2703. this._chunkedPanel = chunkedPanel;
  2704. this._textModel = chunkedPanel._textModel;
  2705. this.element = document.createElement("div");
  2706. this.element.lineNumber = startLine;
  2707. this.element.className = "webkit-line-content";
  2708. this.element._chunk = this;
  2709. this._startLine = startLine;
  2710. endLine = Math.min(this._textModel.linesCount, endLine);
  2711. this.linesCount = endLine - startLine;
  2712. this._expanded = false;
  2713. this.updateCollapsedLineRow();
  2714. }
  2715. WebInspector.TextEditorMainChunk.prototype = {
  2716. /**
  2717. * @param {Element|string} decoration
  2718. */
  2719. addDecoration: function(decoration)
  2720. {
  2721. this._chunkedPanel.beginDomUpdates();
  2722. if (typeof decoration === "string")
  2723. this.element.addStyleClass(decoration);
  2724. else {
  2725. if (!this.element.decorationsElement) {
  2726. this.element.decorationsElement = document.createElement("div");
  2727. this.element.decorationsElement.className = "webkit-line-decorations";
  2728. this.element.decorationsElement._isDecorationsElement = true;
  2729. this.element.appendChild(this.element.decorationsElement);
  2730. }
  2731. this.element.decorationsElement.appendChild(decoration);
  2732. }
  2733. this._chunkedPanel.endDomUpdates();
  2734. },
  2735. /**
  2736. * @param {string|Element} decoration
  2737. */
  2738. removeDecoration: function(decoration)
  2739. {
  2740. this._chunkedPanel.beginDomUpdates();
  2741. if (typeof decoration === "string")
  2742. this.element.removeStyleClass(decoration);
  2743. else if (this.element.decorationsElement)
  2744. this.element.decorationsElement.removeChild(decoration);
  2745. this._chunkedPanel.endDomUpdates();
  2746. },
  2747. removeAllDecorations: function()
  2748. {
  2749. this._chunkedPanel.beginDomUpdates();
  2750. this.element.className = "webkit-line-content";
  2751. if (this.element.decorationsElement) {
  2752. if (this.element.decorationsElement.parentElement)
  2753. this.element.removeChild(this.element.decorationsElement);
  2754. delete this.element.decorationsElement;
  2755. }
  2756. this._chunkedPanel.endDomUpdates();
  2757. },
  2758. /**
  2759. * @return {boolean}
  2760. */
  2761. isDecorated: function()
  2762. {
  2763. return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
  2764. },
  2765. /**
  2766. * @return {number}
  2767. */
  2768. get startLine()
  2769. {
  2770. return this._startLine;
  2771. },
  2772. /**
  2773. * @return {number}
  2774. */
  2775. get endLine()
  2776. {
  2777. return this._startLine + this.linesCount;
  2778. },
  2779. set startLine(startLine)
  2780. {
  2781. this._startLine = startLine;
  2782. this.element.lineNumber = startLine;
  2783. if (this._expandedLineRows) {
  2784. for (var i = 0; i < this._expandedLineRows.length; ++i)
  2785. this._expandedLineRows[i].lineNumber = startLine + i;
  2786. }
  2787. },
  2788. /**
  2789. * @return {boolean}
  2790. */
  2791. expanded: function()
  2792. {
  2793. return this._expanded;
  2794. },
  2795. expand: function()
  2796. {
  2797. if (this._expanded)
  2798. return;
  2799. this._expanded = true;
  2800. if (this.linesCount === 1) {
  2801. this._chunkedPanel._paintLines(this.startLine, this.startLine + 1);
  2802. return;
  2803. }
  2804. this._chunkedPanel.beginDomUpdates();
  2805. this._expandedLineRows = [];
  2806. var parentElement = this.element.parentElement;
  2807. for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
  2808. var lineRow = this._createRow(i);
  2809. parentElement.insertBefore(lineRow, this.element);
  2810. this._expandedLineRows.push(lineRow);
  2811. }
  2812. parentElement.removeChild(this.element);
  2813. this._chunkedPanel._paintLines(this.startLine, this.startLine + this.linesCount);
  2814. this._chunkedPanel.endDomUpdates();
  2815. },
  2816. collapse: function()
  2817. {
  2818. if (!this._expanded)
  2819. return;
  2820. this._expanded = false;
  2821. if (this.linesCount === 1)
  2822. return;
  2823. this._chunkedPanel.beginDomUpdates();
  2824. var elementInserted = false;
  2825. for (var i = 0; i < this._expandedLineRows.length; ++i) {
  2826. var lineRow = this._expandedLineRows[i];
  2827. var parentElement = lineRow.parentElement;
  2828. if (parentElement) {
  2829. if (!elementInserted) {
  2830. elementInserted = true;
  2831. parentElement.insertBefore(this.element, lineRow);
  2832. }
  2833. parentElement.removeChild(lineRow);
  2834. }
  2835. this._chunkedPanel._releaseLinesHighlight(lineRow);
  2836. }
  2837. delete this._expandedLineRows;
  2838. this._chunkedPanel.endDomUpdates();
  2839. },
  2840. /**
  2841. * @return {number}
  2842. */
  2843. get height()
  2844. {
  2845. if (!this._expandedLineRows)
  2846. return this._chunkedPanel.totalHeight(this.element);
  2847. return this._chunkedPanel.totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
  2848. },
  2849. /**
  2850. * @return {number}
  2851. */
  2852. get offsetTop()
  2853. {
  2854. return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
  2855. },
  2856. /**
  2857. * @param {number} lineNumber
  2858. * @return {Element}
  2859. */
  2860. _createRow: function(lineNumber)
  2861. {
  2862. var lineRow = this._chunkedPanel._cachedRows.pop() || document.createElement("div");
  2863. lineRow.lineNumber = lineNumber;
  2864. lineRow.className = "webkit-line-content";
  2865. lineRow.textContent = this._textModel.line(lineNumber);
  2866. if (!lineRow.textContent)
  2867. lineRow.appendChild(document.createElement("br"));
  2868. return lineRow;
  2869. },
  2870. /**
  2871. * Called on potentially damaged / inconsistent chunk
  2872. * @param {number} lineNumber
  2873. * @return {?Node}
  2874. */
  2875. lineRowContainingLine: function(lineNumber)
  2876. {
  2877. if (!this._expanded)
  2878. return this.element;
  2879. return this.expandedLineRow(lineNumber);
  2880. },
  2881. /**
  2882. * @param {number} lineNumber
  2883. * @return {Element}
  2884. */
  2885. expandedLineRow: function(lineNumber)
  2886. {
  2887. if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
  2888. return null;
  2889. if (!this._expandedLineRows)
  2890. return this.element;
  2891. return this._expandedLineRows[lineNumber - this.startLine];
  2892. },
  2893. updateCollapsedLineRow: function()
  2894. {
  2895. if (this.linesCount === 1 && this._expanded)
  2896. return;
  2897. var lines = [];
  2898. for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
  2899. lines.push(this._textModel.line(i));
  2900. if (WebInspector.FALSE)
  2901. console.log("Rebuilding chunk with " + lines.length + " lines");
  2902. this.element.removeChildren();
  2903. this.element.textContent = lines.join("\n");
  2904. // The last empty line will get swallowed otherwise.
  2905. if (!lines[lines.length - 1])
  2906. this.element.appendChild(document.createElement("br"));
  2907. },
  2908. firstElement: function()
  2909. {
  2910. return this._expandedLineRows ? this._expandedLineRows[0] : this.element;
  2911. },
  2912. /**
  2913. * @return {Element}
  2914. */
  2915. lastElement: function()
  2916. {
  2917. return this._expandedLineRows ? this._expandedLineRows[this._expandedLineRows.length - 1] : this.element;
  2918. }
  2919. }
  2920. /**
  2921. * @constructor
  2922. * @param {WebInspector.TextEditorMainPanel} mainPanel
  2923. * @param {WebInspector.TextEditorModel} textModel
  2924. */
  2925. WebInspector.TextEditorMainPanel.TokenHighlighter = function(mainPanel, textModel)
  2926. {
  2927. this._mainPanel = mainPanel;
  2928. this._textModel = textModel;
  2929. }
  2930. WebInspector.TextEditorMainPanel.TokenHighlighter.prototype = {
  2931. /**
  2932. * @param {WebInspector.TextRange} range
  2933. */
  2934. handleSelectionChange: function(range)
  2935. {
  2936. if (!range) {
  2937. this._removeHighlight();
  2938. return;
  2939. }
  2940. if (range.startLine !== range.endLine) {
  2941. this._removeHighlight();
  2942. return;
  2943. }
  2944. range = range.normalize();
  2945. var selectedText = this._textModel.copyRange(range);
  2946. if (selectedText === this._selectedWord)
  2947. return;
  2948. if (selectedText === "") {
  2949. this._removeHighlight();
  2950. return;
  2951. }
  2952. if (this._isWord(range, selectedText))
  2953. this._highlight(selectedText);
  2954. else
  2955. this._removeHighlight();
  2956. },
  2957. /**
  2958. * @param {string} word
  2959. */
  2960. _regexString: function(word)
  2961. {
  2962. return "\\b" + word + "\\b";
  2963. },
  2964. /**
  2965. * @param {string} selectedWord
  2966. */
  2967. _highlight: function(selectedWord)
  2968. {
  2969. this._removeHighlight();
  2970. this._selectedWord = selectedWord;
  2971. this._highlightDescriptor = this._mainPanel.highlightRegex(this._regexString(selectedWord), "text-editor-token-highlight")
  2972. },
  2973. _removeHighlight: function()
  2974. {
  2975. if (this._selectedWord) {
  2976. this._mainPanel.removeHighlight(this._highlightDescriptor);
  2977. delete this._selectedWord;
  2978. delete this._highlightDescriptor;
  2979. }
  2980. },
  2981. /**
  2982. * @param {WebInspector.TextRange} range
  2983. * @param {string} selectedText
  2984. * @return {boolean}
  2985. */
  2986. _isWord: function(range, selectedText)
  2987. {
  2988. var line = this._textModel.line(range.startLine);
  2989. var leftBound = range.startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(range.startColumn - 1));
  2990. var rightBound = range.endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(range.endColumn));
  2991. return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText);
  2992. }
  2993. }
  2994. /**
  2995. * @constructor
  2996. * @param {WebInspector.TextEditorModel} textModel
  2997. * @param {WebInspector.TextEditor} textEditor
  2998. */
  2999. WebInspector.DefaultTextEditor.WordMovementController = function(textEditor, textModel)
  3000. {
  3001. this._textModel = textModel;
  3002. this._textEditor = textEditor;
  3003. }
  3004. WebInspector.DefaultTextEditor.WordMovementController.prototype = {
  3005. /**
  3006. * @param {Object.<number, function()>} shortcuts
  3007. */
  3008. _registerShortcuts: function(shortcuts)
  3009. {
  3010. var keys = WebInspector.KeyboardShortcut.Keys;
  3011. var modifiers = WebInspector.KeyboardShortcut.Modifiers;
  3012. const wordJumpModifier = WebInspector.isMac() ? modifiers.Alt : modifiers.Ctrl;
  3013. shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, wordJumpModifier)] = this._handleCtrlBackspace.bind(this);
  3014. shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "left");
  3015. shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, wordJumpModifier)] = this._handleCtrlArrow.bind(this, "right");
  3016. shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Left.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "left");
  3017. shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Right.code, modifiers.Shift | wordJumpModifier)] = this._handleCtrlShiftArrow.bind(this, "right");
  3018. },
  3019. /**
  3020. * @param {WebInspector.TextRange} selection
  3021. * @param {string} direction
  3022. * @return {WebInspector.TextRange}
  3023. */
  3024. _rangeForCtrlArrowMove: function(selection, direction)
  3025. {
  3026. const isStopChar = WebInspector.TextUtils.isStopChar;
  3027. const isSpaceChar = WebInspector.TextUtils.isSpaceChar;
  3028. var lineNumber = selection.endLine;
  3029. var column = selection.endColumn;
  3030. if (direction === "left")
  3031. --column;
  3032. if (column === -1 && direction === "left") {
  3033. if (lineNumber > 0)
  3034. return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber - 1, this._textModel.line(lineNumber - 1).length);
  3035. else
  3036. return selection.clone();
  3037. }
  3038. var line = this._textModel.line(lineNumber);
  3039. if (column === line.length && direction === "right") {
  3040. if (lineNumber + 1 < this._textModel.linesCount)
  3041. return new WebInspector.TextRange(selection.startLine, selection.startColumn, selection.endLine + 1, 0);
  3042. else
  3043. return selection.clone();
  3044. }
  3045. var delta = direction === "left" ? -1 : +1;
  3046. var directionDependentEndColumnOffset = (delta + 1) / 2;
  3047. if (isSpaceChar(line.charAt(column))) {
  3048. while(column + delta >= 0 && column + delta < line.length && isSpaceChar(line.charAt(column + delta)))
  3049. column += delta;
  3050. if (column + delta < 0 || column + delta === line.length)
  3051. return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber, column + directionDependentEndColumnOffset);
  3052. else
  3053. column += delta;
  3054. }
  3055. var group = isStopChar(line.charAt(column));
  3056. while(column + delta >= 0 && column + delta < line.length && isStopChar(line.charAt(column + delta)) === group && !isSpaceChar(line.charAt(column + delta)))
  3057. column += delta;
  3058. return new WebInspector.TextRange(selection.startLine, selection.startColumn, lineNumber, column + directionDependentEndColumnOffset);
  3059. },
  3060. /**
  3061. * @param {string} direction
  3062. * @return {boolean}
  3063. */
  3064. _handleCtrlArrow: function(direction)
  3065. {
  3066. var newSelection = this._rangeForCtrlArrowMove(this._textEditor.selection(), direction);
  3067. this._textEditor.setSelection(newSelection.collapseToEnd());
  3068. return true;
  3069. },
  3070. /**
  3071. * @param {string} direction
  3072. * @return {boolean}
  3073. */
  3074. _handleCtrlShiftArrow: function(direction)
  3075. {
  3076. this._textEditor.setSelection(this._rangeForCtrlArrowMove(this._textEditor.selection(), direction));
  3077. return true;
  3078. },
  3079. /**
  3080. * @return {boolean}
  3081. */
  3082. _handleCtrlBackspace: function()
  3083. {
  3084. var selection = this._textEditor.selection();
  3085. if (!selection.isEmpty())
  3086. return false;
  3087. var newSelection = this._rangeForCtrlArrowMove(selection, "left");
  3088. this._textModel.editRange(newSelection.normalize(), "", selection);
  3089. this._textEditor.setSelection(newSelection.collapseToEnd());
  3090. return true;
  3091. }
  3092. }
  3093. /**
  3094. * @constructor
  3095. * @param {WebInspector.TextEditorMainPanel} textEditor
  3096. * @param {WebInspector.TextEditorModel} textModel
  3097. * @param {WebInspector.TextEditorModel.BraceMatcher} braceMatcher
  3098. */
  3099. WebInspector.TextEditorMainPanel.BraceHighlightController = function(textEditor, textModel, braceMatcher)
  3100. {
  3101. this._textEditor = textEditor;
  3102. this._textModel = textModel;
  3103. this._braceMatcher = braceMatcher;
  3104. this._highlightDescriptors = [];
  3105. }
  3106. WebInspector.TextEditorMainPanel.BraceHighlightController.prototype = {
  3107. /**
  3108. * @param {string} line
  3109. * @param {number} column
  3110. * @return {number}
  3111. */
  3112. activeBraceColumnForCursorPosition: function(line, column)
  3113. {
  3114. var char = line.charAt(column);
  3115. if (WebInspector.TextUtils.isOpeningBraceChar(char))
  3116. return column;
  3117. var previousChar = line.charAt(column - 1);
  3118. if (WebInspector.TextUtils.isBraceChar(previousChar))
  3119. return column - 1;
  3120. if (WebInspector.TextUtils.isBraceChar(char))
  3121. return column;
  3122. else
  3123. return -1;
  3124. },
  3125. /**
  3126. * @param {WebInspector.TextRange} selectionRange
  3127. */
  3128. handleSelectionChange: function(selectionRange)
  3129. {
  3130. if (!selectionRange || !selectionRange.isEmpty()) {
  3131. this._removeHighlight();
  3132. return;
  3133. }
  3134. if (this._highlightedRange && this._highlightedRange.compareTo(selectionRange) === 0)
  3135. return;
  3136. this._removeHighlight();
  3137. var lineNumber = selectionRange.startLine;
  3138. var column = selectionRange.startColumn;
  3139. var line = this._textModel.line(lineNumber);
  3140. column = this.activeBraceColumnForCursorPosition(line, column);
  3141. if (column < 0)
  3142. return;
  3143. var enclosingBraces = this._braceMatcher.enclosingBraces(lineNumber, column);
  3144. if (!enclosingBraces)
  3145. return;
  3146. this._highlightedRange = selectionRange;
  3147. this._highlightDescriptors.push(this._textEditor.highlightRange(WebInspector.TextRange.createFromLocation(enclosingBraces.leftBrace.lineNumber, enclosingBraces.leftBrace.column), "text-editor-brace-match"));
  3148. this._highlightDescriptors.push(this._textEditor.highlightRange(WebInspector.TextRange.createFromLocation(enclosingBraces.rightBrace.lineNumber, enclosingBraces.rightBrace.column), "text-editor-brace-match"));
  3149. },
  3150. _removeHighlight: function()
  3151. {
  3152. if (!this._highlightDescriptors.length)
  3153. return;
  3154. for(var i = 0; i < this._highlightDescriptors.length; ++i)
  3155. this._textEditor.removeHighlight(this._highlightDescriptors[i]);
  3156. this._highlightDescriptors = [];
  3157. delete this._highlightedRange;
  3158. }
  3159. }
  3160. /**
  3161. * @constructor
  3162. * @param {WebInspector.TextEditorMainPanel} mainPanel
  3163. * @param {WebInspector.TextEditorModel} textModel
  3164. * @param {WebInspector.TextEditorModel.BraceMatcher} braceMatcher
  3165. */
  3166. WebInspector.TextEditorMainPanel.SmartBraceController = function(mainPanel, textModel, braceMatcher)
  3167. {
  3168. this._mainPanel = mainPanel;
  3169. this._textModel = textModel;
  3170. this._braceMatcher = braceMatcher
  3171. }
  3172. WebInspector.TextEditorMainPanel.SmartBraceController.prototype = {
  3173. /**
  3174. * @param {Object.<number, function()>} shortcuts
  3175. */
  3176. registerShortcuts: function(shortcuts)
  3177. {
  3178. if (!WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled())
  3179. return;
  3180. var keys = WebInspector.KeyboardShortcut.Keys;
  3181. var modifiers = WebInspector.KeyboardShortcut.Modifiers;
  3182. shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Backspace.code, modifiers.None)] = this._handleBackspace.bind(this);
  3183. },
  3184. /**
  3185. * @param {Object.<string, function()>} charOverrides
  3186. */
  3187. registerCharOverrides: function(charOverrides)
  3188. {
  3189. if (!WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled())
  3190. return;
  3191. charOverrides["("] = this._handleBracePairInsertion.bind(this, "()");
  3192. charOverrides[")"] = this._handleClosingBraceOverride.bind(this, ")");
  3193. charOverrides["{"] = this._handleBracePairInsertion.bind(this, "{}");
  3194. charOverrides["}"] = this._handleClosingBraceOverride.bind(this, "}");
  3195. },
  3196. _handleBackspace: function()
  3197. {
  3198. var selection = this._mainPanel.lastSelection();
  3199. if (!selection || !selection.isEmpty())
  3200. return false;
  3201. var column = selection.startColumn;
  3202. if (column == 0)
  3203. return false;
  3204. var lineNumber = selection.startLine;
  3205. var line = this._textModel.line(lineNumber);
  3206. if (column === line.length)
  3207. return false;
  3208. var pair = line.substr(column - 1, 2);
  3209. if (pair === "()" || pair === "{}") {
  3210. this._textModel.editRange(new WebInspector.TextRange(lineNumber, column - 1, lineNumber, column + 1), "");
  3211. this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, column - 1));
  3212. return true;
  3213. } else
  3214. return false;
  3215. },
  3216. /**
  3217. * @param {string} bracePair
  3218. * @return {boolean}
  3219. */
  3220. _handleBracePairInsertion: function(bracePair)
  3221. {
  3222. var selection = this._mainPanel.lastSelection().normalize();
  3223. if (selection.isEmpty()) {
  3224. var lineNumber = selection.startLine;
  3225. var column = selection.startColumn;
  3226. var line = this._textModel.line(lineNumber);
  3227. if (column < line.length) {
  3228. var char = line.charAt(column);
  3229. if (WebInspector.TextUtils.isWordChar(char) || (!WebInspector.TextUtils.isBraceChar(char) && WebInspector.TextUtils.isStopChar(char)))
  3230. return false;
  3231. }
  3232. }
  3233. this._textModel.editRange(selection, bracePair);
  3234. this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(selection.startLine, selection.startColumn + 1));
  3235. return true;
  3236. },
  3237. /**
  3238. * @param {string} brace
  3239. * @return {boolean}
  3240. */
  3241. _handleClosingBraceOverride: function(brace)
  3242. {
  3243. var selection = this._mainPanel.lastSelection().normalize();
  3244. if (!selection || !selection.isEmpty())
  3245. return false;
  3246. var lineNumber = selection.startLine;
  3247. var column = selection.startColumn;
  3248. var line = this._textModel.line(lineNumber);
  3249. if (line.charAt(column) !== brace)
  3250. return false;
  3251. var braces = this._braceMatcher.enclosingBraces(lineNumber, column);
  3252. if (braces && braces.rightBrace.lineNumber === lineNumber && braces.rightBrace.column === column) {
  3253. this._mainPanel.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, column + 1));
  3254. return true;
  3255. } else
  3256. return false;
  3257. },
  3258. }
  3259. WebInspector.debugDefaultTextEditor = false;