PageRenderTime 68ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/src/3rdparty/webkit/Source/WebCore/inspector/front-end/TextViewer.js

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