PageRenderTime 69ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/WebCore/inspector/front-end/TextViewer.js

https://bitbucket.org/sevear/webkit-ios
JavaScript | 2000 lines | 1608 code | 308 blank | 84 comment | 325 complexity | 3f48458149bf889163c9b4564543f5e8 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.0, LGPL-2.1
  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. WebInspector.TextViewer = function(textModel, platform, url, delegate)
  32. {
  33. WebInspector.View.call(this);
  34. this._textModel = textModel;
  35. this._textModel.changeListener = this._textChanged.bind(this);
  36. this._textModel.resetUndoStack();
  37. this._delegate = delegate;
  38. this.element.className = "text-editor monospace";
  39. var enterTextChangeMode = this._enterInternalTextChangeMode.bind(this);
  40. var exitTextChangeMode = this._exitInternalTextChangeMode.bind(this);
  41. var syncScrollListener = this._syncScroll.bind(this);
  42. var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this);
  43. this._mainPanel = new WebInspector.TextEditorMainPanel(this._textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode);
  44. this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener);
  45. this.element.appendChild(this._mainPanel.element);
  46. this.element.appendChild(this._gutterPanel.element);
  47. // Forward mouse wheel events from the unscrollable gutter to the main panel.
  48. this._gutterPanel.element.addEventListener("mousewheel", function(e) {
  49. this._mainPanel.element.dispatchEvent(e);
  50. }.bind(this), false);
  51. this.element.addEventListener("dblclick", this._doubleClick.bind(this), true);
  52. this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false);
  53. this.element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
  54. this._registerShortcuts();
  55. }
  56. WebInspector.TextViewer.prototype = {
  57. set mimeType(mimeType)
  58. {
  59. this._mainPanel.mimeType = mimeType;
  60. },
  61. set readOnly(readOnly)
  62. {
  63. if (this._mainPanel.readOnly === readOnly)
  64. return;
  65. this._mainPanel.readOnly = readOnly;
  66. },
  67. get readOnly()
  68. {
  69. return this._mainPanel.readOnly;
  70. },
  71. get textModel()
  72. {
  73. return this._textModel;
  74. },
  75. revealLine: function(lineNumber)
  76. {
  77. this._mainPanel.revealLine(lineNumber);
  78. },
  79. addDecoration: function(lineNumber, decoration)
  80. {
  81. this._mainPanel.addDecoration(lineNumber, decoration);
  82. this._gutterPanel.addDecoration(lineNumber, decoration);
  83. },
  84. removeDecoration: function(lineNumber, decoration)
  85. {
  86. this._mainPanel.removeDecoration(lineNumber, decoration);
  87. this._gutterPanel.removeDecoration(lineNumber, decoration);
  88. },
  89. markAndRevealRange: function(range)
  90. {
  91. this._mainPanel.markAndRevealRange(range);
  92. },
  93. highlightLine: function(lineNumber)
  94. {
  95. if (typeof lineNumber !== "number" || lineNumber < 0)
  96. return;
  97. lineNumber = Math.min(lineNumber, this._textModel.linesCount - 1);
  98. this._mainPanel.highlightLine(lineNumber);
  99. },
  100. clearLineHighlight: function()
  101. {
  102. this._mainPanel.clearLineHighlight();
  103. },
  104. freeCachedElements: function()
  105. {
  106. this._mainPanel.freeCachedElements();
  107. this._gutterPanel.freeCachedElements();
  108. },
  109. get scrollTop()
  110. {
  111. return this._mainPanel.element.scrollTop;
  112. },
  113. set scrollTop(scrollTop)
  114. {
  115. this._mainPanel.element.scrollTop = scrollTop;
  116. },
  117. get scrollLeft()
  118. {
  119. return this._mainPanel.element.scrollLeft;
  120. },
  121. set scrollLeft(scrollLeft)
  122. {
  123. this._mainPanel.element.scrollLeft = scrollLeft;
  124. },
  125. beginUpdates: function()
  126. {
  127. this._mainPanel.beginUpdates();
  128. this._gutterPanel.beginUpdates();
  129. },
  130. endUpdates: function()
  131. {
  132. this._mainPanel.endUpdates();
  133. this._gutterPanel.endUpdates();
  134. this._updatePanelOffsets();
  135. },
  136. resize: function()
  137. {
  138. this._mainPanel.resize();
  139. this._gutterPanel.resize();
  140. this._updatePanelOffsets();
  141. },
  142. // WebInspector.TextModel listener
  143. _textChanged: function(oldRange, newRange, oldText, newText)
  144. {
  145. if (!this._internalTextChangeMode)
  146. this._textModel.resetUndoStack();
  147. this._mainPanel.textChanged(oldRange, newRange);
  148. this._gutterPanel.textChanged(oldRange, newRange);
  149. this._updatePanelOffsets();
  150. },
  151. _enterInternalTextChangeMode: function()
  152. {
  153. this._internalTextChangeMode = true;
  154. this._delegate.beforeTextChanged();
  155. },
  156. _exitInternalTextChangeMode: function(oldRange, newRange)
  157. {
  158. this._internalTextChangeMode = false;
  159. this._delegate.afterTextChanged(oldRange, newRange);
  160. },
  161. _updatePanelOffsets: function()
  162. {
  163. var lineNumbersWidth = this._gutterPanel.element.offsetWidth;
  164. if (lineNumbersWidth)
  165. this._mainPanel.element.style.setProperty("left", lineNumbersWidth + "px");
  166. else
  167. this._mainPanel.element.style.removeProperty("left"); // Use default value set in CSS.
  168. },
  169. _syncScroll: function()
  170. {
  171. // Async call due to performance reasons.
  172. setTimeout(function() {
  173. var mainElement = this._mainPanel.element;
  174. var gutterElement = this._gutterPanel.element;
  175. // Handle horizontal scroll bar at the bottom of the main panel.
  176. this._gutterPanel.syncClientHeight(mainElement.clientHeight);
  177. gutterElement.scrollTop = mainElement.scrollTop;
  178. }.bind(this), 0);
  179. },
  180. _syncDecorationsForLine: function(lineNumber)
  181. {
  182. if (lineNumber >= this._textModel.linesCount)
  183. return;
  184. var mainChunk = this._mainPanel.chunkForLine(lineNumber);
  185. if (mainChunk.linesCount === 1 && mainChunk.decorated) {
  186. var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber);
  187. var height = mainChunk.height;
  188. if (height)
  189. gutterChunk.element.style.setProperty("height", height + "px");
  190. else
  191. gutterChunk.element.style.removeProperty("height");
  192. } else {
  193. var gutterChunk = this._gutterPanel.chunkForLine(lineNumber);
  194. if (gutterChunk.linesCount === 1)
  195. gutterChunk.element.style.removeProperty("height");
  196. }
  197. },
  198. _doubleClick: function(event)
  199. {
  200. if (!this.readOnly)
  201. return;
  202. var lineRow = event.target.enclosingNodeOrSelfWithClass("webkit-line-content");
  203. if (!lineRow)
  204. return; // Do not trigger editing from line numbers.
  205. this._delegate.doubleClick(lineRow.lineNumber);
  206. window.getSelection().collapseToStart();
  207. },
  208. _registerShortcuts: function()
  209. {
  210. var keys = WebInspector.KeyboardShortcut.Keys;
  211. var modifiers = WebInspector.KeyboardShortcut.Modifiers;
  212. this._shortcuts = {};
  213. var commitEditing = this._commitEditing.bind(this);
  214. var cancelEditing = this._cancelEditing.bind(this);
  215. this._shortcuts[WebInspector.KeyboardShortcut.makeKey("s", modifiers.CtrlOrMeta)] = commitEditing;
  216. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Enter.code, modifiers.CtrlOrMeta)] = commitEditing;
  217. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Esc.code)] = cancelEditing;
  218. var handleUndo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, false);
  219. var handleRedo = this._mainPanel.handleUndoRedo.bind(this._mainPanel, true);
  220. this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.CtrlOrMeta)] = handleUndo;
  221. this._shortcuts[WebInspector.KeyboardShortcut.makeKey("z", modifiers.Shift | modifiers.CtrlOrMeta)] = handleRedo;
  222. var handleTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, false);
  223. var handleShiftTabKey = this._mainPanel.handleTabKeyPress.bind(this._mainPanel, true);
  224. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code)] = handleTabKey;
  225. this._shortcuts[WebInspector.KeyboardShortcut.makeKey(keys.Tab.code, modifiers.Shift)] = handleShiftTabKey;
  226. },
  227. _handleKeyDown: function(e)
  228. {
  229. var shortcutKey = WebInspector.KeyboardShortcut.makeKeyFromEvent(e);
  230. var handler = this._shortcuts[shortcutKey];
  231. if (handler && handler.call(this)) {
  232. e.preventDefault();
  233. e.stopPropagation();
  234. }
  235. },
  236. _contextMenu: function(event)
  237. {
  238. var contextMenu = new WebInspector.ContextMenu();
  239. var target = event.target.enclosingNodeOrSelfWithClass("webkit-line-number");
  240. if (target)
  241. this._delegate.populateLineGutterContextMenu(target.lineNumber, contextMenu);
  242. else {
  243. var selection = this._mainPanel._getSelection();
  244. if (selection && !selection.isEmpty())
  245. return; // Show default context menu for selection.
  246. this._delegate.populateTextAreaContextMenu(contextMenu);
  247. }
  248. var fileName = this._delegate.suggestedFileName();
  249. if (fileName)
  250. contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Save as..." : "Save As..."), InspectorFrontendHost.saveAs.bind(InspectorFrontendHost, fileName, this._textModel.text));
  251. contextMenu.show(event);
  252. },
  253. _commitEditing: function()
  254. {
  255. if (this.readOnly)
  256. return false;
  257. this._delegate.commitEditing();
  258. return true;
  259. },
  260. _cancelEditing: function()
  261. {
  262. if (this.readOnly)
  263. return false;
  264. this._delegate.cancelEditing();
  265. return true;
  266. }
  267. }
  268. WebInspector.TextViewer.prototype.__proto__ = WebInspector.View.prototype;
  269. WebInspector.TextViewerDelegate = function()
  270. {
  271. }
  272. WebInspector.TextViewerDelegate.prototype = {
  273. doubleClick: function(lineNumber)
  274. {
  275. // Should be implemented by subclasses.
  276. },
  277. beforeTextChanged: function()
  278. {
  279. // Should be implemented by subclasses.
  280. },
  281. afterTextChanged: function(oldRange, newRange)
  282. {
  283. // Should be implemented by subclasses.
  284. },
  285. commitEditing: function()
  286. {
  287. // Should be implemented by subclasses.
  288. },
  289. cancelEditing: function()
  290. {
  291. // Should be implemented by subclasses.
  292. },
  293. populateLineGutterContextMenu: function(contextMenu)
  294. {
  295. // Should be implemented by subclasses.
  296. },
  297. populateTextAreaContextMenu: function(contextMenu)
  298. {
  299. // Should be implemented by subclasses.
  300. },
  301. suggestedFileName: function()
  302. {
  303. // Should be implemented by subclasses.
  304. }
  305. }
  306. WebInspector.TextViewerDelegate.prototype.__proto__ = WebInspector.Object.prototype;
  307. WebInspector.TextEditorChunkedPanel = function(textModel)
  308. {
  309. this._textModel = textModel;
  310. this._defaultChunkSize = 50;
  311. this._paintCoalescingLevel = 0;
  312. this._domUpdateCoalescingLevel = 0;
  313. }
  314. WebInspector.TextEditorChunkedPanel.prototype = {
  315. get textModel()
  316. {
  317. return this._textModel;
  318. },
  319. revealLine: function(lineNumber)
  320. {
  321. if (lineNumber >= this._textModel.linesCount)
  322. return;
  323. var chunk = this.makeLineAChunk(lineNumber);
  324. chunk.element.scrollIntoViewIfNeeded();
  325. },
  326. addDecoration: function(lineNumber, decoration)
  327. {
  328. if (lineNumber >= this._textModel.linesCount)
  329. return;
  330. var chunk = this.makeLineAChunk(lineNumber);
  331. chunk.addDecoration(decoration);
  332. },
  333. removeDecoration: function(lineNumber, decoration)
  334. {
  335. if (lineNumber >= this._textModel.linesCount)
  336. return;
  337. var chunk = this.chunkForLine(lineNumber);
  338. chunk.removeDecoration(decoration);
  339. },
  340. _buildChunks: function()
  341. {
  342. this.beginDomUpdates();
  343. this._container.removeChildren();
  344. this._textChunks = [];
  345. for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) {
  346. var chunk = this._createNewChunk(i, i + this._defaultChunkSize);
  347. this._textChunks.push(chunk);
  348. this._container.appendChild(chunk.element);
  349. }
  350. this._repaintAll();
  351. this.endDomUpdates();
  352. },
  353. makeLineAChunk: function(lineNumber)
  354. {
  355. var chunkNumber = this._chunkNumberForLine(lineNumber);
  356. var oldChunk = this._textChunks[chunkNumber];
  357. if (!oldChunk) {
  358. console.error("No chunk for line number: " + lineNumber);
  359. return;
  360. }
  361. if (oldChunk.linesCount === 1)
  362. return oldChunk;
  363. return this._splitChunkOnALine(lineNumber, chunkNumber, true);
  364. },
  365. _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
  366. {
  367. this.beginDomUpdates();
  368. var oldChunk = this._textChunks[chunkNumber];
  369. var wasExpanded = oldChunk.expanded;
  370. oldChunk.expanded = false;
  371. var insertIndex = chunkNumber + 1;
  372. // Prefix chunk.
  373. if (lineNumber > oldChunk.startLine) {
  374. var prefixChunk = this._createNewChunk(oldChunk.startLine, lineNumber);
  375. prefixChunk.readOnly = oldChunk.readOnly;
  376. this._textChunks.splice(insertIndex++, 0, prefixChunk);
  377. this._container.insertBefore(prefixChunk.element, oldChunk.element);
  378. }
  379. // Line chunk.
  380. var endLine = createSuffixChunk ? lineNumber + 1 : oldChunk.startLine + oldChunk.linesCount;
  381. var lineChunk = this._createNewChunk(lineNumber, endLine);
  382. lineChunk.readOnly = oldChunk.readOnly;
  383. this._textChunks.splice(insertIndex++, 0, lineChunk);
  384. this._container.insertBefore(lineChunk.element, oldChunk.element);
  385. // Suffix chunk.
  386. if (oldChunk.startLine + oldChunk.linesCount > endLine) {
  387. var suffixChunk = this._createNewChunk(endLine, oldChunk.startLine + oldChunk.linesCount);
  388. suffixChunk.readOnly = oldChunk.readOnly;
  389. this._textChunks.splice(insertIndex, 0, suffixChunk);
  390. this._container.insertBefore(suffixChunk.element, oldChunk.element);
  391. }
  392. // Remove enclosing chunk.
  393. this._textChunks.splice(chunkNumber, 1);
  394. this._container.removeChild(oldChunk.element);
  395. if (wasExpanded) {
  396. if (prefixChunk)
  397. prefixChunk.expanded = true;
  398. lineChunk.expanded = true;
  399. if (suffixChunk)
  400. suffixChunk.expanded = true;
  401. }
  402. this.endDomUpdates();
  403. return lineChunk;
  404. },
  405. _scroll: function()
  406. {
  407. // FIXME: Replace the "2" with the padding-left value from CSS.
  408. if (this.element.scrollLeft <= 2)
  409. this.element.scrollLeft = 0;
  410. this._scheduleRepaintAll();
  411. if (this._syncScrollListener)
  412. this._syncScrollListener();
  413. },
  414. _scheduleRepaintAll: function()
  415. {
  416. if (this._repaintAllTimer)
  417. clearTimeout(this._repaintAllTimer);
  418. this._repaintAllTimer = setTimeout(this._repaintAll.bind(this), 50);
  419. },
  420. beginUpdates: function()
  421. {
  422. this._paintCoalescingLevel++;
  423. },
  424. endUpdates: function()
  425. {
  426. this._paintCoalescingLevel--;
  427. if (!this._paintCoalescingLevel)
  428. this._repaintAll();
  429. },
  430. beginDomUpdates: function()
  431. {
  432. this._domUpdateCoalescingLevel++;
  433. },
  434. endDomUpdates: function()
  435. {
  436. this._domUpdateCoalescingLevel--;
  437. },
  438. _chunkNumberForLine: function(lineNumber)
  439. {
  440. function compareLineNumbers(value, chunk)
  441. {
  442. return value < chunk.startLine ? -1 : 1;
  443. }
  444. var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers);
  445. return insertBefore - 1;
  446. },
  447. chunkForLine: function(lineNumber)
  448. {
  449. return this._textChunks[this._chunkNumberForLine(lineNumber)];
  450. },
  451. _findFirstVisibleChunkNumber: function(visibleFrom)
  452. {
  453. function compareOffsetTops(value, chunk)
  454. {
  455. return value < chunk.offsetTop ? -1 : 1;
  456. }
  457. var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops);
  458. return insertBefore - 1;
  459. },
  460. _findVisibleChunks: function(visibleFrom, visibleTo)
  461. {
  462. var from = this._findFirstVisibleChunkNumber(visibleFrom);
  463. for (var to = from + 1; to < this._textChunks.length; ++to) {
  464. if (this._textChunks[to].offsetTop >= visibleTo)
  465. break;
  466. }
  467. return { start: from, end: to };
  468. },
  469. _findFirstVisibleLineNumber: function(visibleFrom)
  470. {
  471. var chunk = this._textChunks[this._findFirstVisibleChunkNumber(visibleFrom)];
  472. if (!chunk.expanded)
  473. return chunk.startLine;
  474. var lineNumbers = [];
  475. for (var i = 0; i < chunk.linesCount; ++i) {
  476. lineNumbers.push(chunk.startLine + i);
  477. }
  478. function compareLineRowOffsetTops(value, lineNumber)
  479. {
  480. var lineRow = chunk.getExpandedLineRow(lineNumber);
  481. return value < lineRow.offsetTop ? -1 : 1;
  482. }
  483. var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, lineNumbers, compareLineRowOffsetTops);
  484. return lineNumbers[insertBefore - 1];
  485. },
  486. _repaintAll: function()
  487. {
  488. delete this._repaintAllTimer;
  489. if (this._paintCoalescingLevel || this._dirtyLines)
  490. return;
  491. var visibleFrom = this.element.scrollTop;
  492. var visibleTo = this.element.scrollTop + this.element.clientHeight;
  493. if (visibleTo) {
  494. var result = this._findVisibleChunks(visibleFrom, visibleTo);
  495. this._expandChunks(result.start, result.end);
  496. }
  497. },
  498. _expandChunks: function(fromIndex, toIndex)
  499. {
  500. // First collapse chunks to collect the DOM elements into a cache to reuse them later.
  501. for (var i = 0; i < fromIndex; ++i)
  502. this._textChunks[i].expanded = false;
  503. for (var i = toIndex; i < this._textChunks.length; ++i)
  504. this._textChunks[i].expanded = false;
  505. for (var i = fromIndex; i < toIndex; ++i)
  506. this._textChunks[i].expanded = true;
  507. },
  508. _totalHeight: function(firstElement, lastElement)
  509. {
  510. lastElement = (lastElement || firstElement).nextElementSibling;
  511. if (lastElement)
  512. return lastElement.offsetTop - firstElement.offsetTop;
  513. var offsetParent = firstElement.offsetParent;
  514. if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight)
  515. return offsetParent.scrollHeight - firstElement.offsetTop;
  516. var total = 0;
  517. while (firstElement && firstElement !== lastElement) {
  518. total += firstElement.offsetHeight;
  519. firstElement = firstElement.nextElementSibling;
  520. }
  521. return total;
  522. },
  523. resize: function()
  524. {
  525. this._repaintAll();
  526. }
  527. }
  528. WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineListener)
  529. {
  530. WebInspector.TextEditorChunkedPanel.call(this, textModel);
  531. this._syncDecorationsForLineListener = syncDecorationsForLineListener;
  532. this.element = document.createElement("div");
  533. this.element.className = "text-editor-lines";
  534. this._container = document.createElement("div");
  535. this._container.className = "inner-container";
  536. this.element.appendChild(this._container);
  537. this.element.addEventListener("scroll", this._scroll.bind(this), false);
  538. this.freeCachedElements();
  539. this._buildChunks();
  540. }
  541. WebInspector.TextEditorGutterPanel.prototype = {
  542. freeCachedElements: function()
  543. {
  544. this._cachedRows = [];
  545. },
  546. _createNewChunk: function(startLine, endLine)
  547. {
  548. return new WebInspector.TextEditorGutterChunk(this, startLine, endLine);
  549. },
  550. textChanged: function(oldRange, newRange)
  551. {
  552. this.beginDomUpdates();
  553. var linesDiff = newRange.linesCount - oldRange.linesCount;
  554. if (linesDiff) {
  555. // Remove old chunks (if needed).
  556. for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0 ; --chunkNumber) {
  557. var chunk = this._textChunks[chunkNumber];
  558. if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount)
  559. break;
  560. chunk.expanded = false;
  561. this._container.removeChild(chunk.element);
  562. }
  563. this._textChunks.length = chunkNumber + 1;
  564. // Add new chunks (if needed).
  565. var totalLines = 0;
  566. if (this._textChunks.length) {
  567. var lastChunk = this._textChunks[this._textChunks.length - 1];
  568. totalLines = lastChunk.startLine + lastChunk.linesCount;
  569. }
  570. for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) {
  571. var chunk = this._createNewChunk(i, i + this._defaultChunkSize);
  572. this._textChunks.push(chunk);
  573. this._container.appendChild(chunk.element);
  574. }
  575. this._repaintAll();
  576. } else {
  577. // Decorations may have been removed, so we may have to sync those lines.
  578. var chunkNumber = this._chunkNumberForLine(newRange.startLine);
  579. var chunk = this._textChunks[chunkNumber];
  580. while (chunk && chunk.startLine <= newRange.endLine) {
  581. if (chunk.linesCount === 1)
  582. this._syncDecorationsForLineListener(chunk.startLine);
  583. chunk = this._textChunks[++chunkNumber];
  584. }
  585. }
  586. this.endDomUpdates();
  587. },
  588. syncClientHeight: function(clientHeight)
  589. {
  590. if (this.element.offsetHeight > clientHeight)
  591. this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px");
  592. else
  593. this._container.style.removeProperty("padding-bottom");
  594. }
  595. }
  596. WebInspector.TextEditorGutterPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype;
  597. WebInspector.TextEditorGutterChunk = function(textViewer, startLine, endLine)
  598. {
  599. this._textViewer = textViewer;
  600. this._textModel = textViewer._textModel;
  601. this.startLine = startLine;
  602. endLine = Math.min(this._textModel.linesCount, endLine);
  603. this.linesCount = endLine - startLine;
  604. this._expanded = false;
  605. this.element = document.createElement("div");
  606. this.element.lineNumber = startLine;
  607. this.element.className = "webkit-line-number";
  608. if (this.linesCount === 1) {
  609. // Single line chunks are typically created for decorations. Host line number in
  610. // the sub-element in order to allow flexible border / margin management.
  611. var innerSpan = document.createElement("span");
  612. innerSpan.className = "webkit-line-number-inner";
  613. innerSpan.textContent = startLine + 1;
  614. var outerSpan = document.createElement("div");
  615. outerSpan.className = "webkit-line-number-outer";
  616. outerSpan.appendChild(innerSpan);
  617. this.element.appendChild(outerSpan);
  618. } else {
  619. var lineNumbers = [];
  620. for (var i = startLine; i < endLine; ++i)
  621. lineNumbers.push(i + 1);
  622. this.element.textContent = lineNumbers.join("\n");
  623. }
  624. }
  625. WebInspector.TextEditorGutterChunk.prototype = {
  626. addDecoration: function(decoration)
  627. {
  628. this._textViewer.beginDomUpdates();
  629. if (typeof decoration === "string")
  630. this.element.addStyleClass(decoration);
  631. this._textViewer.endDomUpdates();
  632. },
  633. removeDecoration: function(decoration)
  634. {
  635. this._textViewer.beginDomUpdates();
  636. if (typeof decoration === "string")
  637. this.element.removeStyleClass(decoration);
  638. this._textViewer.endDomUpdates();
  639. },
  640. get expanded()
  641. {
  642. return this._expanded;
  643. },
  644. set expanded(expanded)
  645. {
  646. if (this.linesCount === 1)
  647. this._textViewer._syncDecorationsForLineListener(this.startLine);
  648. if (this._expanded === expanded)
  649. return;
  650. this._expanded = expanded;
  651. if (this.linesCount === 1)
  652. return;
  653. this._textViewer.beginDomUpdates();
  654. if (expanded) {
  655. this._expandedLineRows = [];
  656. var parentElement = this.element.parentElement;
  657. for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
  658. var lineRow = this._createRow(i);
  659. parentElement.insertBefore(lineRow, this.element);
  660. this._expandedLineRows.push(lineRow);
  661. }
  662. parentElement.removeChild(this.element);
  663. } else {
  664. var elementInserted = false;
  665. for (var i = 0; i < this._expandedLineRows.length; ++i) {
  666. var lineRow = this._expandedLineRows[i];
  667. var parentElement = lineRow.parentElement;
  668. if (parentElement) {
  669. if (!elementInserted) {
  670. elementInserted = true;
  671. parentElement.insertBefore(this.element, lineRow);
  672. }
  673. parentElement.removeChild(lineRow);
  674. }
  675. this._textViewer._cachedRows.push(lineRow);
  676. }
  677. delete this._expandedLineRows;
  678. }
  679. this._textViewer.endDomUpdates();
  680. },
  681. get height()
  682. {
  683. if (!this._expandedLineRows)
  684. return this._textViewer._totalHeight(this.element);
  685. return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
  686. },
  687. get offsetTop()
  688. {
  689. return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
  690. },
  691. _createRow: function(lineNumber)
  692. {
  693. var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div");
  694. lineRow.lineNumber = lineNumber;
  695. lineRow.className = "webkit-line-number";
  696. lineRow.textContent = lineNumber + 1;
  697. return lineRow;
  698. }
  699. }
  700. WebInspector.TextEditorMainPanel = function(textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode)
  701. {
  702. WebInspector.TextEditorChunkedPanel.call(this, textModel);
  703. this._syncScrollListener = syncScrollListener;
  704. this._syncDecorationsForLineListener = syncDecorationsForLineListener;
  705. this._enterTextChangeMode = enterTextChangeMode;
  706. this._exitTextChangeMode = exitTextChangeMode;
  707. this._url = url;
  708. this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this));
  709. this._readOnly = true;
  710. this.element = document.createElement("div");
  711. this.element.className = "text-editor-contents";
  712. this.element.tabIndex = 0;
  713. this._container = document.createElement("div");
  714. this._container.className = "inner-container";
  715. this._container.tabIndex = 0;
  716. this.element.appendChild(this._container);
  717. this.element.addEventListener("scroll", this._scroll.bind(this), false);
  718. // In WebKit the DOMNodeRemoved event is fired AFTER the node is removed, thus it should be
  719. // attached to all DOM nodes that we want to track. Instead, we attach the DOMNodeRemoved
  720. // listeners only on the line rows, and use DOMSubtreeModified to track node removals inside
  721. // the line rows. For more info see: https://bugs.webkit.org/show_bug.cgi?id=55666
  722. this._handleDOMUpdatesCallback = this._handleDOMUpdates.bind(this);
  723. this._container.addEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false);
  724. this._container.addEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false);
  725. this._container.addEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false);
  726. this.freeCachedElements();
  727. this._buildChunks();
  728. }
  729. WebInspector.TextEditorMainPanel.prototype = {
  730. set mimeType(mimeType)
  731. {
  732. this._highlighter.mimeType = mimeType;
  733. },
  734. set readOnly(readOnly)
  735. {
  736. if (this._readOnly === readOnly)
  737. return;
  738. this.beginDomUpdates();
  739. this._readOnly = readOnly;
  740. if (this._readOnly)
  741. this._container.removeStyleClass("text-editor-editable");
  742. else
  743. this._container.addStyleClass("text-editor-editable");
  744. this.endDomUpdates();
  745. },
  746. get readOnly()
  747. {
  748. return this._readOnly;
  749. },
  750. setEditableRange: function(startLine, endLine)
  751. {
  752. this.beginDomUpdates();
  753. var firstChunkNumber = this._chunkNumberForLine(startLine);
  754. var firstChunk = this._textChunks[firstChunkNumber];
  755. if (firstChunk.startLine !== startLine) {
  756. this._splitChunkOnALine(startLine, firstChunkNumber);
  757. firstChunkNumber += 1;
  758. }
  759. var lastChunkNumber = this._textChunks.length;
  760. if (endLine !== this._textModel.linesCount) {
  761. lastChunkNumber = this._chunkNumberForLine(endLine);
  762. var lastChunk = this._textChunks[lastChunkNumber];
  763. if (lastChunk && lastChunk.startLine !== endLine) {
  764. this._splitChunkOnALine(endLine, lastChunkNumber);
  765. lastChunkNumber += 1;
  766. }
  767. }
  768. for (var chunkNumber = 0; chunkNumber < firstChunkNumber; ++chunkNumber)
  769. this._textChunks[chunkNumber].readOnly = true;
  770. for (var chunkNumber = firstChunkNumber; chunkNumber < lastChunkNumber; ++chunkNumber)
  771. this._textChunks[chunkNumber].readOnly = false;
  772. for (var chunkNumber = lastChunkNumber; chunkNumber < this._textChunks.length; ++chunkNumber)
  773. this._textChunks[chunkNumber].readOnly = true;
  774. this.endDomUpdates();
  775. },
  776. clearEditableRange: function()
  777. {
  778. for (var chunkNumber = 0; chunkNumber < this._textChunks.length; ++chunkNumber)
  779. this._textChunks[chunkNumber].readOnly = false;
  780. },
  781. markAndRevealRange: function(range)
  782. {
  783. if (this._rangeToMark) {
  784. var markedLine = this._rangeToMark.startLine;
  785. delete this._rangeToMark;
  786. // Remove the marked region immediately.
  787. if (!this._dirtyLines) {
  788. this.beginDomUpdates();
  789. var chunk = this.chunkForLine(markedLine);
  790. var wasExpanded = chunk.expanded;
  791. chunk.expanded = false;
  792. chunk.updateCollapsedLineRow();
  793. chunk.expanded = wasExpanded;
  794. this.endDomUpdates();
  795. } else
  796. this._paintLines(markedLine, markedLine + 1);
  797. }
  798. if (range) {
  799. this._rangeToMark = range;
  800. this.revealLine(range.startLine);
  801. var chunk = this.makeLineAChunk(range.startLine);
  802. this._paintLine(chunk.element);
  803. if (this._markedRangeElement)
  804. this._markedRangeElement.scrollIntoViewIfNeeded();
  805. }
  806. delete this._markedRangeElement;
  807. },
  808. highlightLine: function(lineNumber)
  809. {
  810. this.clearLineHighlight();
  811. this._highlightedLine = lineNumber;
  812. this.revealLine(lineNumber);
  813. this.addDecoration(lineNumber, "webkit-highlighted-line");
  814. },
  815. clearLineHighlight: function()
  816. {
  817. if (typeof this._highlightedLine === "number") {
  818. this.removeDecoration(this._highlightedLine, "webkit-highlighted-line");
  819. delete this._highlightedLine;
  820. }
  821. },
  822. freeCachedElements: function()
  823. {
  824. this._cachedSpans = [];
  825. this._cachedTextNodes = [];
  826. this._cachedRows = [];
  827. },
  828. handleUndoRedo: function(redo)
  829. {
  830. if (this._readOnly || this._dirtyLines)
  831. return false;
  832. this.beginUpdates();
  833. this._enterTextChangeMode();
  834. var callback = function(oldRange, newRange) {
  835. this._exitTextChangeMode(oldRange, newRange);
  836. this._enterTextChangeMode();
  837. }.bind(this);
  838. var range = redo ? this._textModel.redo(callback) : this._textModel.undo(callback);
  839. if (range)
  840. this._setCaretLocation(range.endLine, range.endColumn, true);
  841. this._exitTextChangeMode(null, null);
  842. this.endUpdates();
  843. return true;
  844. },
  845. handleTabKeyPress: function(shiftKey)
  846. {
  847. if (this._readOnly || this._dirtyLines)
  848. return false;
  849. var selection = this._getSelection();
  850. if (!selection)
  851. return false;
  852. if (shiftKey)
  853. return true;
  854. this.beginUpdates();
  855. this._enterTextChangeMode();
  856. var range = selection;
  857. if (range.startLine > range.endLine || (range.startLine === range.endLine && range.startColumn > range.endColumn))
  858. range = new WebInspector.TextRange(range.endLine, range.endColumn, range.startLine, range.startColumn);
  859. var newRange = this._setText(range, "\t");
  860. this._exitTextChangeMode(range, newRange);
  861. this.endUpdates();
  862. this._setCaretLocation(newRange.endLine, newRange.endColumn, true);
  863. return true;
  864. },
  865. _splitChunkOnALine: function(lineNumber, chunkNumber, createSuffixChunk)
  866. {
  867. var selection = this._getSelection();
  868. var chunk = WebInspector.TextEditorChunkedPanel.prototype._splitChunkOnALine.call(this, lineNumber, chunkNumber, createSuffixChunk);
  869. this._restoreSelection(selection);
  870. return chunk;
  871. },
  872. _buildChunks: function()
  873. {
  874. for (var i = 0; i < this._textModel.linesCount; ++i)
  875. this._textModel.removeAttribute(i, "highlight");
  876. WebInspector.TextEditorChunkedPanel.prototype._buildChunks.call(this);
  877. },
  878. _createNewChunk: function(startLine, endLine)
  879. {
  880. return new WebInspector.TextEditorMainChunk(this, startLine, endLine);
  881. },
  882. _expandChunks: function(fromIndex, toIndex)
  883. {
  884. var lastChunk = this._textChunks[toIndex - 1];
  885. var lastVisibleLine = lastChunk.startLine + lastChunk.linesCount;
  886. var selection = this._getSelection();
  887. this._muteHighlightListener = true;
  888. this._highlighter.highlight(lastVisibleLine);
  889. delete this._muteHighlightListener;
  890. this._restorePaintLinesOperationsCredit();
  891. WebInspector.TextEditorChunkedPanel.prototype._expandChunks.call(this, fromIndex, toIndex);
  892. this._adjustPaintLinesOperationsRefreshValue();
  893. this._restoreSelection(selection);
  894. },
  895. _highlightDataReady: function(fromLine, toLine)
  896. {
  897. if (this._muteHighlightListener)
  898. return;
  899. this._restorePaintLinesOperationsCredit();
  900. this._paintLines(fromLine, toLine, true /*restoreSelection*/);
  901. },
  902. _schedulePaintLines: function(startLine, endLine)
  903. {
  904. if (startLine >= endLine)
  905. return;
  906. if (!this._scheduledPaintLines) {
  907. this._scheduledPaintLines = [ { startLine: startLine, endLine: endLine } ];
  908. this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 0);
  909. } else {
  910. for (var i = 0; i < this._scheduledPaintLines.length; ++i) {
  911. var chunk = this._scheduledPaintLines[i];
  912. if (chunk.startLine <= endLine && chunk.endLine >= startLine) {
  913. chunk.startLine = Math.min(chunk.startLine, startLine);
  914. chunk.endLine = Math.max(chunk.endLine, endLine);
  915. return;
  916. }
  917. if (chunk.startLine > endLine) {
  918. this._scheduledPaintLines.splice(i, 0, { startLine: startLine, endLine: endLine });
  919. return;
  920. }
  921. }
  922. this._scheduledPaintLines.push({ startLine: startLine, endLine: endLine });
  923. }
  924. },
  925. _paintScheduledLines: function(skipRestoreSelection)
  926. {
  927. if (this._paintScheduledLinesTimer)
  928. clearTimeout(this._paintScheduledLinesTimer);
  929. delete this._paintScheduledLinesTimer;
  930. if (!this._scheduledPaintLines)
  931. return;
  932. // Reschedule the timer if we can not paint the lines yet, or the user is scrolling.
  933. if (this._dirtyLines || this._repaintAllTimer) {
  934. this._paintScheduledLinesTimer = setTimeout(this._paintScheduledLines.bind(this), 50);
  935. return;
  936. }
  937. var scheduledPaintLines = this._scheduledPaintLines;
  938. delete this._scheduledPaintLines;
  939. this._restorePaintLinesOperationsCredit();
  940. this._paintLineChunks(scheduledPaintLines, !skipRestoreSelection);
  941. this._adjustPaintLinesOperationsRefreshValue();
  942. },
  943. _restorePaintLinesOperationsCredit: function()
  944. {
  945. if (!this._paintLinesOperationsRefreshValue)
  946. this._paintLinesOperationsRefreshValue = 250;
  947. this._paintLinesOperationsCredit = this._paintLinesOperationsRefreshValue;
  948. this._paintLinesOperationsLastRefresh = Date.now();
  949. },
  950. _adjustPaintLinesOperationsRefreshValue: function()
  951. {
  952. var operationsDone = this._paintLinesOperationsRefreshValue - this._paintLinesOperationsCredit;
  953. if (operationsDone <= 0)
  954. return;
  955. var timePast = Date.now() - this._paintLinesOperationsLastRefresh;
  956. if (timePast <= 0)
  957. return;
  958. // Make the synchronous CPU chunk for painting the lines 50 msec.
  959. var value = Math.floor(operationsDone / timePast * 50);
  960. this._paintLinesOperationsRefreshValue = Number.constrain(value, 150, 1500);
  961. },
  962. _paintLines: function(fromLine, toLine, restoreSelection)
  963. {
  964. this._paintLineChunks([ { startLine: fromLine, endLine: toLine } ], restoreSelection);
  965. },
  966. _paintLineChunks: function(lineChunks, restoreSelection)
  967. {
  968. // First, paint visible lines, so that in case of long lines we should start highlighting
  969. // the visible area immediately, instead of waiting for the lines above the visible area.
  970. var visibleFrom = this.element.scrollTop;
  971. var firstVisibleLineNumber = this._findFirstVisibleLineNumber(visibleFrom);
  972. var chunk;
  973. var selection;
  974. var invisibleLineRows = [];
  975. for (var i = 0; i < lineChunks.length; ++i) {
  976. var lineChunk = lineChunks[i];
  977. if (this._dirtyLines || this._scheduledPaintLines) {
  978. this._schedulePaintLines(lineChunk.startLine, lineChunk.endLine);
  979. continue;
  980. }
  981. for (var lineNumber = lineChunk.startLine; lineNumber < lineChunk.endLine; ++lineNumber) {
  982. if (!chunk || lineNumber < chunk.startLine || lineNumber >= chunk.startLine + chunk.linesCount)
  983. chunk = this.chunkForLine(lineNumber);
  984. var lineRow = chunk.getExpandedLineRow(lineNumber);
  985. if (!lineRow)
  986. continue;
  987. if (lineNumber < firstVisibleLineNumber) {
  988. invisibleLineRows.push(lineRow);
  989. continue;
  990. }
  991. if (restoreSelection && !selection)
  992. selection = this._getSelection();
  993. this._paintLine(lineRow);
  994. if (this._paintLinesOperationsCredit < 0) {
  995. this._schedulePaintLines(lineNumber + 1, lineChunk.endLine);
  996. break;
  997. }
  998. }
  999. }
  1000. for (var i = 0; i < invisibleLineRows.length; ++i) {
  1001. if (restoreSelection && !selection)
  1002. selection = this._getSelection();
  1003. this._paintLine(invisibleLineRows[i]);
  1004. }
  1005. if (restoreSelection)
  1006. this._restoreSelection(selection);
  1007. },
  1008. _paintLine: function(lineRow)
  1009. {
  1010. var lineNumber = lineRow.lineNumber;
  1011. if (this._dirtyLines) {
  1012. this._schedulePaintLines(lineNumber, lineNumber + 1);
  1013. return;
  1014. }
  1015. this.beginDomUpdates();
  1016. try {
  1017. if (this._scheduledPaintLines || this._paintLinesOperationsCredit < 0) {
  1018. this._schedulePaintLines(lineNumber, lineNumber + 1);
  1019. return;
  1020. }
  1021. var highlight = this._textModel.getAttribute(lineNumber, "highlight");
  1022. if (!highlight)
  1023. return;
  1024. lineRow.removeChildren();
  1025. var line = this._textModel.line(lineNumber);
  1026. if (!line)
  1027. lineRow.appendChild(document.createElement("br"));
  1028. var plainTextStart = -1;
  1029. for (var j = 0; j < line.length;) {
  1030. if (j > 1000) {
  1031. // This line is too long - do not waste cycles on minified js highlighting.
  1032. if (plainTextStart === -1)
  1033. plainTextStart = j;
  1034. break;
  1035. }
  1036. var attribute = highlight[j];
  1037. if (!attribute || !attribute.tokenType) {
  1038. if (plainTextStart === -1)
  1039. plainTextStart = j;
  1040. j++;
  1041. } else {
  1042. if (plainTextStart !== -1) {
  1043. this._appendTextNode(lineRow, line.substring(plainTextStart, j));
  1044. plainTextStart = -1;
  1045. --this._paintLinesOperationsCredit;
  1046. }
  1047. this._appendSpan(lineRow, line.substring(j, j + attribute.length), attribute.tokenType);
  1048. j += attribute.length;
  1049. --this._paintLinesOperationsCredit;
  1050. }
  1051. }
  1052. if (plainTextStart !== -1) {
  1053. this._appendTextNode(lineRow, line.substring(plainTextStart, line.length));
  1054. --this._paintLinesOperationsCredit;
  1055. }
  1056. if (lineRow.decorationsElement)
  1057. lineRow.appendChild(lineRow.decorationsElement);
  1058. } finally {
  1059. if (this._rangeToMark && this._rangeToMark.startLine === lineNumber)
  1060. this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn);
  1061. this.endDomUpdates();
  1062. }
  1063. },
  1064. _releaseLinesHighlight: function(lineRow)
  1065. {
  1066. if (!lineRow)
  1067. return;
  1068. if ("spans" in lineRow) {
  1069. var spans = lineRow.spans;
  1070. for (var j = 0; j < spans.length; ++j)
  1071. this._cachedSpans.push(spans[j]);
  1072. delete lineRow.spans;
  1073. }
  1074. if ("textNodes" in lineRow) {
  1075. var textNodes = lineRow.textNodes;
  1076. for (var j = 0; j < textNodes.length; ++j)
  1077. this._cachedTextNodes.push(textNodes[j]);
  1078. delete lineRow.textNodes;
  1079. }
  1080. this._cachedRows.push(lineRow);
  1081. },
  1082. _getSelection: function()
  1083. {
  1084. var selection = window.getSelection();
  1085. if (!selection.rangeCount)
  1086. return null;
  1087. var selectionRange = selection.getRangeAt(0);
  1088. // Selection may be outside of the viewer.
  1089. if (!this._container.isAncestor(selectionRange.startContainer) || !this._container.isAncestor(selectionRange.endContainer))
  1090. return null;
  1091. var start = this._selectionToPosition(selectionRange.startContainer, selectionRange.startOffset);
  1092. var end = selectionRange.collapsed ? start : this._selectionToPosition(selectionRange.endContainer, selectionRange.endOffset);
  1093. if (selection.anchorNode === selectionRange.startContainer && selection.anchorOffset === selectionRange.startOffset)
  1094. return new WebInspector.TextRange(start.line, start.column, end.line, end.column);
  1095. else
  1096. return new WebInspector.TextRange(end.line, end.column, start.line, start.column);
  1097. },
  1098. _restoreSelection: function(range, scrollIntoView)
  1099. {
  1100. if (!range)
  1101. return;
  1102. var start = this._positionToSelection(range.startLine, range.startColumn);
  1103. var end = range.isEmpty() ? start : this._positionToSelection(range.endLine, range.endColumn);
  1104. window.getSelection().setBaseAndExtent(start.container, start.offset, end.container, end.offset);
  1105. if (scrollIntoView) {
  1106. for (var node = end.container; node; node = node.parentElement) {
  1107. if (node.scrollIntoViewIfNeeded) {
  1108. node.scrollIntoViewIfNeeded();
  1109. break;
  1110. }
  1111. }
  1112. }
  1113. },
  1114. _setCaretLocation: function(line, column, scrollIntoView)
  1115. {
  1116. var range = new WebInspector.TextRange(line, column, line, column);
  1117. this._restoreSelection(range, scrollIntoView);
  1118. },
  1119. _selectionToPosition: function(container, offset)
  1120. {
  1121. if (container === this._container && offset === 0)
  1122. return { line: 0, column: 0 };
  1123. if (container === this._container && offset === 1)
  1124. return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) };
  1125. var lineRow = this._enclosingLineRowOrSelf(container);
  1126. var lineNumber = lineRow.lineNumber;
  1127. if (container === lineRow && offset === 0)
  1128. return { line: lineNumber, column: 0 };
  1129. // This may be chunk and chunks may contain \n.
  1130. var column = 0;
  1131. var node = lineRow.nodeType === Node.TEXT_NODE ? lineRow : lineRow.traverseNextTextNode(lineRow);
  1132. while (node && node !== container) {
  1133. var text = node.textContent;
  1134. for (var i = 0; i < text.length; ++i) {
  1135. if (text.charAt(i) === "\n") {
  1136. lineNumber++;
  1137. column = 0;
  1138. } else
  1139. column++;
  1140. }
  1141. node = node.traverseNextTextNode(lineRow);
  1142. }
  1143. if (node === container && offset) {
  1144. var text = node.textContent;
  1145. for (var i = 0; i < offset; ++i) {
  1146. if (text.charAt(i) === "\n") {
  1147. lineNumber++;
  1148. column = 0;
  1149. } else
  1150. column++;
  1151. }
  1152. }
  1153. return { line: lineNumber, column: column };
  1154. },
  1155. _positionToSelection: function(line, column)
  1156. {
  1157. var chunk = this.chunkForLine(line);
  1158. // One-lined collapsed chunks may still stay highlighted.
  1159. var lineRow = chunk.linesCount === 1 ? chunk.element : chunk.getExpandedLineRow(line);
  1160. if (lineRow)
  1161. var rangeBoundary = lineRow.rangeBoundaryForOffset(column);
  1162. else {
  1163. var offset = column;
  1164. for (var i = chunk.startLine; i < line; ++i)
  1165. offset += this._textModel.lineLength(i) + 1; // \n
  1166. lineRow = chunk.element;
  1167. if (lineRow.firstChild)
  1168. var rangeBoundary = { container: lineRow.firstChild, offset: offset };
  1169. else
  1170. var rangeBoundary = { container: lineRow, offset: 0 };
  1171. }
  1172. return rangeBoundary;
  1173. },
  1174. _enclosingLineRowOrSelf: function(element)
  1175. {
  1176. var lineRow = element.enclosingNodeOrSelfWithClass("webkit-line-content");
  1177. if (lineRow)
  1178. return lineRow;
  1179. for (var lineRow = element; lineRow; lineRow = lineRow.parentElement) {
  1180. if (lineRow.parentElement === this._container)
  1181. return lineRow;
  1182. }
  1183. return null;
  1184. },
  1185. _appendSpan: function(element, content, className)
  1186. {
  1187. if (className === "html-resource-link" || className === "html-external-link") {
  1188. element.appendChild(this._createLink(content, className === "html-external-link"));
  1189. return;
  1190. }
  1191. var span = this._cachedSpans.pop() || document.createElement("span");
  1192. span.className = "webkit-" + className;
  1193. span.textContent = content;
  1194. element.appendChild(span);
  1195. if (!("spans" in element))
  1196. element.spans = [];
  1197. element.spans.push(span);
  1198. },
  1199. _appendTextNode: function(element, text)
  1200. {
  1201. var textNode = this._cachedTextNodes.pop();
  1202. if (textNode)
  1203. textNode.nodeValue = text;
  1204. else
  1205. textNode = document.createTextNode(text);
  1206. element.appendChild(textNode);
  1207. if (!("textNodes" in element))
  1208. element.textNodes = [];
  1209. element.textNodes.push(textNode);
  1210. },
  1211. _createLink: function(content, isExternal)
  1212. {
  1213. var quote = content.charAt(0);
  1214. if (content.length > 1 && (quote === "\"" || quote === "'"))
  1215. content = content.substring(1, content.length - 1);
  1216. else
  1217. quote = null;
  1218. var a = WebInspector.linkifyURLAsNode(this._rewriteHref(content), content, null, isExternal);
  1219. var span = document.createElement("span");
  1220. span.className = "webkit-html-attribute-value";
  1221. if (quote)
  1222. span.appendChild(document.createTextNode(quote));
  1223. span.appendChild(a);
  1224. if (quote)
  1225. span.appendChild(document.createTextNode(quote));
  1226. return span;
  1227. },
  1228. _rewriteHref: function(hrefValue, isExternal)
  1229. {
  1230. if (!this._url || !hrefValue || hrefValue.indexOf("://") > 0)
  1231. return hrefValue;
  1232. return WebInspector.completeURL(this._url, hrefValue);
  1233. },
  1234. _handleDOMUpdates: function(e)
  1235. {
  1236. if (this._domUpdateCoalescingLevel)
  1237. return;
  1238. var target = e.target;
  1239. if (target === this._container)
  1240. return;
  1241. var lineRow = this._enclosingLineRowOrSelf(target);
  1242. if (!lineRow)
  1243. return;
  1244. if (lineRow.decorationsElement && (lineRow.decorationsElement === target || lineRow.decorationsElement.isAncestor(target))) {
  1245. if (this._syncDecorationsForLineListener)
  1246. this._syncDecorationsForLineListener(lineRow.lineNumber);
  1247. return;
  1248. }
  1249. if (this._readOnly)
  1250. return;
  1251. if (target === lineRow && e.type === "DOMNodeInserted") {
  1252. // Ensure that the newly inserted line row has no lineNumber.
  1253. delete lineRow.lineNumber;
  1254. }
  1255. var startLine = 0;
  1256. for (var row = lineRow; row; row = row.previousSibling) {
  1257. if (typeof row.lineNumber === "number") {
  1258. startLine = row.lineNumber;
  1259. break;
  1260. }
  1261. }
  1262. var endLine = startLine + 1;
  1263. for (var row = lineRow.nextSibling; row; row = row.nextSibling) {
  1264. if (typeof row.lineNumber === "number" && row.lineNumber > startLine) {
  1265. endLine = row.lineNumber;
  1266. break;
  1267. }
  1268. }
  1269. if (target === lineRow && e.type === "DOMNodeRemoved") {
  1270. // Now this will no longer be valid.
  1271. delete lineRow.lineNumber;
  1272. }
  1273. if (this._dirtyLines) {
  1274. this._dirtyLines.start = Math.min(this._dirtyLines.start, startLine);
  1275. this._dirtyLines.end = Math.max(this._dirtyLines.end, endLine);
  1276. } else {
  1277. this._dirtyLines = { start: startLine, end: endLine };
  1278. setTimeout(this._applyDomUpdates.bind(this), 0);
  1279. // Remove marked ranges, if any.
  1280. this.markAndRevealRange(null);
  1281. }
  1282. },
  1283. _applyDomUpdates: function()
  1284. {
  1285. if (!this._dirtyLines)
  1286. return;
  1287. // Check if the editor had been set readOnly by the moment when this async callback got executed.
  1288. if (this._readOnly) {
  1289. delete this._dirtyLines;
  1290. return;
  1291. }
  1292. // This is a "foreign" call outside of this class. Should be before we delete the dirty lines flag.
  1293. this._enterTextChangeMode();
  1294. var dirtyLines = this._dirtyLines;
  1295. delete this._dirtyLines;
  1296. var firstChunkNumber = this._chunkNumberForLine(dirtyLines.start);
  1297. var startLine = this._textChunks[firstChunkNumber].startLine;
  1298. var endLine = this._textModel.linesCount;
  1299. // Collect lines.
  1300. var firstLineRow;
  1301. if (firstChunkNumber) {
  1302. var chunk = this._textChunks[firstChunkNumber - 1];
  1303. firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element;
  1304. firstLineRow = firstLineRow.nextSibling;
  1305. } else
  1306. firstLineRow = this._container.firstChild;
  1307. var lines = [];
  1308. for (var lineRow = firstLineRow; lineRow; lineRow = lineRow.nextSibling) {
  1309. if (typeof lineRow.lineNumber === "number" && lineRow.lineNumber >= dirtyLines.end) {
  1310. endLine = lineRow.lineNumber;
  1311. break;
  1312. }
  1313. // Update with the newest lineNumber, so that the call to the _getSelection method below should work.
  1314. lineRow.lineNumber = startLine + lines.length;
  1315. this._collectLinesFromDiv(lines, lineRow);
  1316. }
  1317. // Try to decrease the range being replaced, if possible.
  1318. var startOffset = 0;
  1319. while (startLine < dirtyLines.start && startOffset < lines.length) {
  1320. if (this._textModel.line(startLine) !== lines[startOffset])
  1321. break;
  1322. ++startOffset;
  1323. ++startLine;
  1324. }
  1325. var endOffset = lines.length;
  1326. while (endLine > dirtyLines.end && endOffset > startOffset) {
  1327. if (this._textModel.line(endLine - 1) !== lines[endOffset - 1])
  1328. break;
  1329. --endOffset;
  1330. --endLine;
  1331. }
  1332. lines = lines.slice(startOffset, endOffset);
  1333. // Try to decrease the range being replaced by column offsets, if possible.
  1334. var startColumn = 0;
  1335. var endColumn = this._textModel.lineLength(endLine - 1);
  1336. if (lines.length > 0) {
  1337. var line1 = this._textModel.line(startLine);
  1338. var line2 = lines[0];
  1339. while (line1[startColumn] && line1[startColumn] === line2[startColumn])
  1340. ++startColumn;
  1341. lines[0] = line2.substring(startColumn);
  1342. var line1 = this._textModel.line(endLine - 1);
  1343. var line2 = lines[lines.length - 1];
  1344. for (var i = 0; i < endColumn && i < line2.length; ++i) {
  1345. if (startLine === endLine - 1 && endColumn - i <= startColumn)
  1346. break;
  1347. if (line1[endColumn - i - 1] !== line2[line2.length - i - 1])
  1348. break;
  1349. }
  1350. if (i) {
  1351. endColumn -= i;
  1352. lines[lines.length - 1] = line2.substring(0, line2.length - i);
  1353. }
  1354. }
  1355. var selection = this._getSelection();
  1356. if (lines.length === 0 && endLine < this._textModel.linesCount)
  1357. var oldRange = new WebInspector.TextRange(startLine, 0, endLine, 0);
  1358. else if (lines.length === 0 && startLine > 0)
  1359. var oldRange = new WebInspector.TextRange(startLine - 1, this._textModel.lineLength(startLine - 1), endLine - 1, this._textModel.lineLength(endLine - 1));
  1360. else
  1361. var oldRange = new WebInspector.TextRange(startLine, startColumn, endLine - 1, endColumn);
  1362. var newRange = this._setText(oldRange, lines.join("\n"));
  1363. this._paintScheduledLines(true);
  1364. this._restoreSelection(selection);
  1365. this._exitTextChangeMode(oldRange, newRange);
  1366. },
  1367. textChanged: function(oldRange, newRange)
  1368. {
  1369. this.beginDomUpdates();
  1370. this._removeDecorationsInRange(oldRange);
  1371. this._updateChunksForRanges(oldRange, newRange);
  1372. this._updateHighlightsForRange(newRange);
  1373. this.endDomUpdates();
  1374. },
  1375. _setText: function(range, text)
  1376. {
  1377. if (this._lastEditedRange && (!text || text.indexOf("\n") !== -1 || this._lastEditedRange.endLine !== range.startLine || this._lastEditedRange.endColumn !== range.startColumn))
  1378. this._textModel.markUndoableState();
  1379. var newRange = this._textModel.setText(range, text);
  1380. this._lastEditedRange = newRange;
  1381. return newRange;
  1382. },
  1383. _removeDecorationsInRange: function(range)
  1384. {
  1385. for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) {
  1386. var chunk = this._textChunks[i];
  1387. if (chunk.startLine > range.endLine)
  1388. break;
  1389. chunk.removeAllDecorations();
  1390. }
  1391. },
  1392. _updateChunksForRanges: function(oldRange, newRange)
  1393. {
  1394. // Update the chunks in range: firstChunkNumber <= index <= lastChunkNumber
  1395. var firstChunkNumber = this._chunkNumberForLine(oldRange.startLine);
  1396. var lastChunkNumber = firstChunkNumber;
  1397. while (lastChunkNumber + 1 < this._textChunks.length) {
  1398. if (this._textChunks[lastChunkNumber + 1].startLine > oldRange.endLine)
  1399. break;
  1400. ++lastChunkNumber;
  1401. }
  1402. var startLine = this._textChunks[firstChunkNumber].startLine;
  1403. var linesCount = this._textChunks[lastChunkNumber].startLine + this._textChunks[lastChunkNumber].linesCount - startLine;
  1404. var linesDiff = newRange.linesCount - oldRange.linesCount;
  1405. linesCount += linesDiff;
  1406. if (linesDiff) {
  1407. // Lines shifted, update the line numbers of the chunks below.
  1408. for (var chunkNumber = lastChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber)
  1409. this._textChunks[chunkNumber].startLine += linesDiff;
  1410. }
  1411. var firstLineRow;
  1412. if (firstChunkNumber) {
  1413. var chunk = this._textChunks[firstChunkNumber - 1];
  1414. firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element;
  1415. firstLineRow = firstLineRow.nextSibling;
  1416. } else
  1417. firstLineRow = this._container.firstChild;
  1418. // Most frequent case: a chunk remained the same.
  1419. for (var chunkNumber = firstChunkNumber; chunkNumber <= lastChunkNumber; ++chunkNumber) {
  1420. var chunk = this._textChunks[chunkNumber];
  1421. if (chunk.startLine + chunk.linesCount > this._textModel.linesCount)
  1422. break;
  1423. var lineNumber = chunk.startLine;
  1424. for (var lineRow = firstLineRow; lineRow && lineNumber < chunk.startLine + chunk.linesCount; lineRow = lineRow.nextSibling) {
  1425. if (lineRow.lineNumber !== lineNumber || lineRow !== chunk.getExpandedLineRow(lineNumber) || lineRow.textContent !== this._textModel.line(lineNumber) || !lineRow.firstChild)
  1426. break;
  1427. ++lineNumber;
  1428. }
  1429. if (lineNumber < chunk.startLine + chunk.linesCount)
  1430. break;
  1431. chunk.updateCollapsedLineRow();
  1432. ++firstChunkNumber;
  1433. firstLineRow = lineRow;
  1434. startLine += chunk.linesCount;
  1435. linesCount -= chunk.linesCount;
  1436. }
  1437. if (firstChunkNumber > lastChunkNumber && linesCount === 0)
  1438. return;
  1439. // Maybe merge with the next chunk, so that we should not create 1-sized chunks when appending new lines one by one.
  1440. var chunk = this._textChunks[lastChunkNumber + 1];
  1441. var linesInLastChunk = linesCount % this._defaultChunkSize;
  1442. if (chunk && !chunk.decorated && linesInLastChunk > 0 && linesInLastChunk + chunk.linesCount <= this._defaultChunkSize) {
  1443. ++lastChunkNumber;
  1444. linesCount += chunk.linesCount;
  1445. }
  1446. var scrollTop = this.element.scrollTop;
  1447. var scrollLeft = this.element.scrollLeft;
  1448. // Delete all DOM elements that were either controlled by the old chunks, or have just been inserted.
  1449. var firstUnmodifiedLineRow = null;
  1450. var chunk = this._textChunks[lastChunkNumber + 1];
  1451. if (chunk) {
  1452. firstUnmodifiedLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine) : chunk.element;
  1453. }
  1454. while (firstLineRow && firstLineRow !== firstUnmodifiedLineRow) {
  1455. var lineRow = firstLineRow;
  1456. firstLineRow = firstLineRow.nextSibling;
  1457. this._container.removeChild(lineRow);
  1458. }
  1459. // Replace old chunks with the new ones.
  1460. for (var chunkNumber = firstChunkNumber; linesCount > 0; ++chunkNumber) {
  1461. var chunkLinesCount = Math.min(this._defaultChunkSize, linesCount);
  1462. var newChunk = this._createNewChunk(startLine, startLine + chunkLinesCount);
  1463. this._container.insertBefore(newChunk.element, firstUnmodifiedLineRow);
  1464. if (chunkNumber <= lastChunkNumber)
  1465. this._textChunks[chunkNumber] = newChunk;
  1466. else
  1467. this._textChunks.splice(chunkNumber, 0, newChunk);
  1468. startLine += chunkLinesCount;
  1469. linesCount -= chunkLinesCount;
  1470. }
  1471. if (chunkNumber <= lastChunkNumber)
  1472. this._textChunks.splice(chunkNumber, lastChunkNumber - chunkNumber + 1);
  1473. this.element.scrollTop = scrollTop;
  1474. this.element.scrollLeft = scrollLeft;
  1475. },
  1476. _updateHighlightsForRange: function(range)
  1477. {
  1478. var visibleFrom = this.element.scrollTop;
  1479. var visibleTo = this.element.scrollTop + this.element.clientHeight;
  1480. var result = this._findVisibleChunks(visibleFrom, visibleTo);
  1481. var chunk = this._textChunks[result.end - 1];
  1482. var lastVisibleLine = chunk.startLine + chunk.linesCount;
  1483. lastVisibleLine = Math.max(lastVisibleLine, range.endLine + 1);
  1484. lastVisibleLine = Math.min(lastVisibleLine, this._textModel.linesCount);
  1485. var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine);
  1486. if (!updated) {
  1487. // Highlights for the chunks below are invalid, so just collapse them.
  1488. for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i)
  1489. this._textChunks[i].expanded = false;
  1490. }
  1491. this._repaintAll();
  1492. },
  1493. _collectLinesFromDiv: function(lines, element)
  1494. {
  1495. var textContents = [];
  1496. var node = element.nodeType === Node.TEXT_NODE ? element : element.traverseNextNode(element);
  1497. while (node) {
  1498. if (element.decorationsElement === node) {
  1499. node = node.nextSibling;
  1500. continue;
  1501. }
  1502. if (node.nodeName.toLowerCase() === "br")
  1503. textContents.push("\n");
  1504. else if (node.nodeType === Node.TEXT_NODE)
  1505. textContents.push(node.textContent);
  1506. node = node.traverseNextNode(element);
  1507. }
  1508. var textContent = textContents.join("");
  1509. // The last \n (if any) does not "count" in a DIV.
  1510. textContent = textContent.replace(/\n$/, "");
  1511. textContents = textContent.split("\n");
  1512. for (var i = 0; i < textContents.length; ++i)
  1513. lines.push(textContents[i]);
  1514. }
  1515. }
  1516. WebInspector.TextEditorMainPanel.prototype.__proto__ = WebInspector.TextEditorChunkedPanel.prototype;
  1517. WebInspector.TextEditorMainChunk = function(textViewer, startLine, endLine)
  1518. {
  1519. this._textViewer = textViewer;
  1520. this._textModel = textViewer._textModel;
  1521. this.element = document.createElement("div");
  1522. this.element.lineNumber = startLine;
  1523. this.element.className = "webkit-line-content";
  1524. this.element.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false);
  1525. this._startLine = startLine;
  1526. endLine = Math.min(this._textModel.linesCount, endLine);
  1527. this.linesCount = endLine - startLine;
  1528. this._expanded = false;
  1529. this._readOnly = false;
  1530. this.updateCollapsedLineRow();
  1531. }
  1532. WebInspector.TextEditorMainChunk.prototype = {
  1533. addDecoration: function(decoration)
  1534. {
  1535. this._textViewer.beginDomUpdates();
  1536. if (typeof decoration === "string")
  1537. this.element.addStyleClass(decoration);
  1538. else {
  1539. if (!this.element.decorationsElement) {
  1540. this.element.decorationsElement = document.createElement("div");
  1541. this.element.decorationsElement.className = "webkit-line-decorations";
  1542. this.element.appendChild(this.element.decorationsElement);
  1543. }
  1544. this.element.decorationsElement.appendChild(decoration);
  1545. }
  1546. this._textViewer.endDomUpdates();
  1547. },
  1548. removeDecoration: function(decoration)
  1549. {
  1550. this._textViewer.beginDomUpdates();
  1551. if (typeof decoration === "string")
  1552. this.element.removeStyleClass(decoration);
  1553. else if (this.element.decorationsElement)
  1554. this.element.decorationsElement.removeChild(decoration);
  1555. this._textViewer.endDomUpdates();
  1556. },
  1557. removeAllDecorations: function()
  1558. {
  1559. this._textViewer.beginDomUpdates();
  1560. this.element.className = "webkit-line-content";
  1561. if (this.element.decorationsElement) {
  1562. this.element.removeChild(this.element.decorationsElement);
  1563. delete this.element.decorationsElement;
  1564. }
  1565. this._textViewer.endDomUpdates();
  1566. },
  1567. get decorated()
  1568. {
  1569. return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild);
  1570. },
  1571. get startLine()
  1572. {
  1573. return this._startLine;
  1574. },
  1575. set startLine(startLine)
  1576. {
  1577. this._startLine = startLine;
  1578. this.element.lineNumber = startLine;
  1579. if (this._expandedLineRows) {
  1580. for (var i = 0; i < this._expandedLineRows.length; ++i)
  1581. this._expandedLineRows[i].lineNumber = startLine + i;
  1582. }
  1583. },
  1584. get expanded()
  1585. {
  1586. return this._expanded;
  1587. },
  1588. set expanded(expanded)
  1589. {
  1590. if (this._expanded === expanded)
  1591. return;
  1592. this._expanded = expanded;
  1593. if (this.linesCount === 1) {
  1594. if (expanded)
  1595. this._textViewer._paintLine(this.element);
  1596. return;
  1597. }
  1598. this._textViewer.beginDomUpdates();
  1599. if (expanded) {
  1600. this._expandedLineRows = [];
  1601. var parentElement = this.element.parentElement;
  1602. for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) {
  1603. var lineRow = this._createRow(i);
  1604. this._updateElementReadOnlyState(lineRow);
  1605. parentElement.insertBefore(lineRow, this.element);
  1606. this._expandedLineRows.push(lineRow);
  1607. }
  1608. parentElement.removeChild(this.element);
  1609. this._textViewer._paintLines(this.startLine, this.startLine + this.linesCount);
  1610. } else {
  1611. var elementInserted = false;
  1612. for (var i = 0; i < this._expandedLineRows.length; ++i) {
  1613. var lineRow = this._expandedLineRows[i];
  1614. var parentElement = lineRow.parentElement;
  1615. if (parentElement) {
  1616. if (!elementInserted) {
  1617. elementInserted = true;
  1618. parentElement.insertBefore(this.element, lineRow);
  1619. }
  1620. parentElement.removeChild(lineRow);
  1621. }
  1622. this._textViewer._releaseLinesHighlight(lineRow);
  1623. }
  1624. delete this._expandedLineRows;
  1625. }
  1626. this._textViewer.endDomUpdates();
  1627. },
  1628. set readOnly(readOnly)
  1629. {
  1630. if (this._readOnly === readOnly)
  1631. return;
  1632. this._readOnly = readOnly;
  1633. this._updateElementReadOnlyState(this.element);
  1634. if (this._expandedLineRows) {
  1635. for (var i = 0; i < this._expandedLineRows.length; ++i)
  1636. this._updateElementReadOnlyState(this._expandedLineRows[i]);
  1637. }
  1638. },
  1639. get readOnly()
  1640. {
  1641. return this._readOnly;
  1642. },
  1643. _updateElementReadOnlyState: function(element)
  1644. {
  1645. if (this._readOnly)
  1646. element.addStyleClass("text-editor-read-only");
  1647. else
  1648. element.removeStyleClass("text-editor-read-only");
  1649. },
  1650. get height()
  1651. {
  1652. if (!this._expandedLineRows)
  1653. return this._textViewer._totalHeight(this.element);
  1654. return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]);
  1655. },
  1656. get offsetTop()
  1657. {
  1658. return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop;
  1659. },
  1660. _createRow: function(lineNumber)
  1661. {
  1662. var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div");
  1663. lineRow.lineNumber = lineNumber;
  1664. lineRow.className = "webkit-line-content";
  1665. lineRow.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false);
  1666. lineRow.textContent = this._textModel.line(lineNumber);
  1667. if (!lineRow.textContent)
  1668. lineRow.appendChild(document.createElement("br"));
  1669. return lineRow;
  1670. },
  1671. getExpandedLineRow: function(lineNumber)
  1672. {
  1673. if (!this._expanded || lineNumber < this.startLine || lineNumber >= this.startLine + this.linesCount)
  1674. return null;
  1675. if (!this._expandedLineRows)
  1676. return this.element;
  1677. return this._expandedLineRows[lineNumber - this.startLine];
  1678. },
  1679. updateCollapsedLineRow: function()
  1680. {
  1681. if (this.linesCount === 1 && this._expanded)
  1682. return;
  1683. var lines = [];
  1684. for (var i = this.startLine; i < this.startLine + this.linesCount; ++i)
  1685. lines.push(this._textModel.line(i));
  1686. this.element.removeChildren();
  1687. this.element.textContent = lines.join("\n");
  1688. // The last empty line will get swallowed otherwise.
  1689. if (!lines[lines.length - 1])
  1690. this.element.appendChild(document.createElement("br"));
  1691. }
  1692. }