diff options
author | Ben Murdoch <benm@google.com> | 2011-05-24 11:24:40 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2011-06-02 09:53:15 +0100 |
commit | 81bc750723a18f21cd17d1b173cd2a4dda9cea6e (patch) | |
tree | 7a9e5ed86ff429fd347a25153107221543909b19 /Source/WebCore/inspector/front-end/TextViewer.js | |
parent | 94088a6d336c1dd80a1e734af51e96abcbb689a7 (diff) | |
download | external_webkit-81bc750723a18f21cd17d1b173cd2a4dda9cea6e.zip external_webkit-81bc750723a18f21cd17d1b173cd2a4dda9cea6e.tar.gz external_webkit-81bc750723a18f21cd17d1b173cd2a4dda9cea6e.tar.bz2 |
Merge WebKit at r80534: Intial merge by Git
Change-Id: Ia7a83357124c9e1cdb1debf55d9661ec0bd09a61
Diffstat (limited to 'Source/WebCore/inspector/front-end/TextViewer.js')
-rw-r--r-- | Source/WebCore/inspector/front-end/TextViewer.js | 790 |
1 files changed, 652 insertions, 138 deletions
diff --git a/Source/WebCore/inspector/front-end/TextViewer.js b/Source/WebCore/inspector/front-end/TextViewer.js index ce6502d..65b4724 100644 --- a/Source/WebCore/inspector/front-end/TextViewer.js +++ b/Source/WebCore/inspector/front-end/TextViewer.js @@ -37,9 +37,11 @@ WebInspector.TextViewer = function(textModel, platform, url) this.element = document.createElement("div"); this.element.className = "text-editor monospace"; + var enterTextChangeMode = this._enterInternalTextChangeMode.bind(this); + var exitTextChangeMode = this._exitInternalTextChangeMode.bind(this); var syncScrollListener = this._syncScroll.bind(this); var syncDecorationsForLineListener = this._syncDecorationsForLine.bind(this); - this._mainPanel = new WebInspector.TextEditorMainPanel(this._textModel, url, syncScrollListener, syncDecorationsForLineListener); + this._mainPanel = new WebInspector.TextEditorMainPanel(this._textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode); this._gutterPanel = new WebInspector.TextEditorGutterPanel(this._textModel, syncDecorationsForLineListener); this.element.appendChild(this._mainPanel.element); this.element.appendChild(this._gutterPanel.element); @@ -51,6 +53,21 @@ WebInspector.TextViewer.prototype = { this._mainPanel.mimeType = mimeType; }, + set readOnly(readOnly) + { + this._mainPanel.readOnly = readOnly; + }, + + set startEditingListener(startEditingListener) + { + this._startEditingListener = startEditingListener; + }, + + set endEditingListener(endEditingListener) + { + this._endEditingListener = endEditingListener; + }, + get textModel() { return this._textModel; @@ -129,6 +146,7 @@ WebInspector.TextViewer.prototype = { { this._mainPanel.endUpdates(); this._gutterPanel.endUpdates(); + this._updatePanelOffsets(); }, resize: function() @@ -141,9 +159,31 @@ WebInspector.TextViewer.prototype = { // WebInspector.TextModel listener _textChanged: function(oldRange, newRange, oldText, newText) { - this._mainPanel.textChanged(); - this._gutterPanel.textChanged(); + if (!this._internalTextChangeMode) { + this._mainPanel.textChanged(oldRange, newRange); + this._gutterPanel.textChanged(oldRange, newRange); + this._updatePanelOffsets(); + } + }, + + _enterInternalTextChangeMode: function() + { + this._internalTextChangeMode = true; + + if (this._startEditingListener) + this._startEditingListener(); + }, + + _exitInternalTextChangeMode: function(oldRange, newRange) + { + this._internalTextChangeMode = false; + + // Update the gutter panel. + this._gutterPanel.textChanged(oldRange, newRange); this._updatePanelOffsets(); + + if (this._endEditingListener) + this._endEditingListener(oldRange, newRange); }, _updatePanelOffsets: function() @@ -161,13 +201,8 @@ WebInspector.TextViewer.prototype = { setTimeout(function() { var mainElement = this._mainPanel.element; var gutterElement = this._gutterPanel.element; - // Handle horizontal scroll bar at the bottom of the main panel. - if (gutterElement.offsetHeight > mainElement.clientHeight) - gutterElement.style.setProperty("padding-bottom", (gutterElement.offsetHeight - mainElement.clientHeight) + "px"); - else - gutterElement.style.removeProperty("padding-bottom"); - + this._gutterPanel.syncClientHeight(mainElement.clientHeight); gutterElement.scrollTop = mainElement.scrollTop; }.bind(this), 0); }, @@ -177,13 +212,19 @@ WebInspector.TextViewer.prototype = { if (lineNumber >= this._textModel.linesCount) return; - var mainChunk = this._mainPanel.makeLineAChunk(lineNumber); - var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber); - var height = mainChunk.height; - if (height) - gutterChunk.element.style.setProperty("height", height + "px"); - else - gutterChunk.element.style.removeProperty("height"); + var mainChunk = this._mainPanel.chunkForLine(lineNumber); + if (mainChunk.linesCount === 1) { + var gutterChunk = this._gutterPanel.makeLineAChunk(lineNumber); + var height = mainChunk.height; + if (height) + gutterChunk.element.style.setProperty("height", height + "px"); + else + gutterChunk.element.style.removeProperty("height"); + } else { + var gutterChunk = this._gutterPanel.chunkForLine(lineNumber); + if (gutterChunk.linesCount === 1) + gutterChunk.element.style.removeProperty("height"); + } } } @@ -193,6 +234,7 @@ WebInspector.TextEditorChunkedPanel = function(textModel) this._defaultChunkSize = 50; this._paintCoalescingLevel = 0; + this._domUpdateCoalescingLevel = 0; } WebInspector.TextEditorChunkedPanel.prototype = { @@ -222,23 +264,27 @@ WebInspector.TextEditorChunkedPanel.prototype = { chunk.removeDecoration(decoration); }, - textChanged: function(oldRange, newRange, oldText, newText) + textChanged: function(oldRange, newRange) { this._buildChunks(); }, _buildChunks: function() { - this.element.removeChildren(); + this.beginDomUpdates(); + + this._container.removeChildren(); this._textChunks = []; for (var i = 0; i < this._textModel.linesCount; i += this._defaultChunkSize) { var chunk = this._createNewChunk(i, i + this._defaultChunkSize); this._textChunks.push(chunk); - this.element.appendChild(chunk.element); + this._container.appendChild(chunk.element); } this._repaintAll(); + + this.endDomUpdates(); }, makeLineAChunk: function(lineNumber) @@ -251,6 +297,8 @@ WebInspector.TextEditorChunkedPanel.prototype = { if (oldChunk.linesCount === 1) return oldChunk; + this.beginDomUpdates(); + var wasExpanded = oldChunk.expanded; oldChunk.expanded = false; @@ -260,24 +308,24 @@ WebInspector.TextEditorChunkedPanel.prototype = { if (lineNumber > oldChunk.startLine) { var prefixChunk = this._createNewChunk(oldChunk.startLine, lineNumber); this._textChunks.splice(insertIndex++, 0, prefixChunk); - this.element.insertBefore(prefixChunk.element, oldChunk.element); + this._container.insertBefore(prefixChunk.element, oldChunk.element); } // Line chunk. var lineChunk = this._createNewChunk(lineNumber, lineNumber + 1); this._textChunks.splice(insertIndex++, 0, lineChunk); - this.element.insertBefore(lineChunk.element, oldChunk.element); + this._container.insertBefore(lineChunk.element, oldChunk.element); // Suffix chunk. if (oldChunk.startLine + oldChunk.linesCount > lineNumber + 1) { var suffixChunk = this._createNewChunk(lineNumber + 1, oldChunk.startLine + oldChunk.linesCount); this._textChunks.splice(insertIndex, 0, suffixChunk); - this.element.insertBefore(suffixChunk.element, oldChunk.element); + this._container.insertBefore(suffixChunk.element, oldChunk.element); } // Remove enclosing chunk. this._textChunks.splice(chunkNumber, 1); - this.element.removeChild(oldChunk.element); + this._container.removeChild(oldChunk.element); if (wasExpanded) { if (prefixChunk) @@ -287,11 +335,17 @@ WebInspector.TextEditorChunkedPanel.prototype = { suffixChunk.expanded = true; } + this.endDomUpdates(); + return lineChunk; }, _scroll: function() { + // FIXME: Replace the "2" with the padding-left value from CSS. + if (this.element.scrollLeft <= 2) + this.element.scrollLeft = 0; + this._scheduleRepaintAll(); if (this._syncScrollListener) this._syncScrollListener(); @@ -316,26 +370,52 @@ WebInspector.TextEditorChunkedPanel.prototype = { this._repaintAll(); }, + beginDomUpdates: function() + { + this._domUpdateCoalescingLevel++; + }, + + endDomUpdates: function() + { + this._domUpdateCoalescingLevel--; + }, + _chunkNumberForLine: function(lineNumber) { - for (var i = 0; i < this._textChunks.length; ++i) { - var line = this._textChunks[i].startLine; - if (lineNumber >= line && lineNumber < line + this._textChunks[i].linesCount) - return i; + function compareLineNumbers(value, chunk) + { + return value < chunk.startLine ? -1 : 1; } - return this._textChunks.length - 1; + var insertBefore = insertionIndexForObjectInListSortedByFunction(lineNumber, this._textChunks, compareLineNumbers); + return insertBefore - 1; }, - _chunkForLine: function(lineNumber) + chunkForLine: function(lineNumber) { return this._textChunks[this._chunkNumberForLine(lineNumber)]; }, + _findVisibleChunks: function(visibleFrom, visibleTo) + { + function compareOffsetTops(value, chunk) + { + return value < chunk.offsetTop ? -1 : 1; + } + var insertBefore = insertionIndexForObjectInListSortedByFunction(visibleFrom, this._textChunks, compareOffsetTops); + + var from = insertBefore - 1; + for (var to = from + 1; to < this._textChunks.length; ++to) { + if (this._textChunks[to].offsetTop >= visibleTo) + break; + } + return { start: from, end: to }; + }, + _repaintAll: function() { delete this._repaintAllTimer; - if (this._paintCoalescingLevel) + if (this._paintCoalescingLevel || this._dirtyLines) return; if (!this._textChunks) @@ -344,25 +424,10 @@ WebInspector.TextEditorChunkedPanel.prototype = { var visibleFrom = this.element.scrollTop; var visibleTo = this.element.scrollTop + this.element.clientHeight; - var offset = 0; - var fromIndex = -1; - var toIndex = 0; - for (var i = 0; i < this._textChunks.length; ++i) { - var chunk = this._textChunks[i]; - var chunkHeight = chunk.height; - if (offset + chunkHeight > visibleFrom && offset < visibleTo) { - if (fromIndex === -1) - fromIndex = i; - toIndex = i + 1; - } else { - if (offset >= visibleTo) - break; - } - offset += chunkHeight; + if (visibleTo) { + var result = this._findVisibleChunks(visibleFrom, visibleTo); + this._expandChunks(result.start, result.end); } - - if (toIndex) - this._expandChunks(fromIndex, toIndex); }, _totalHeight: function(firstElement, lastElement) @@ -370,11 +435,19 @@ WebInspector.TextEditorChunkedPanel.prototype = { lastElement = (lastElement || firstElement).nextElementSibling; if (lastElement) return lastElement.offsetTop - firstElement.offsetTop; - else if (firstElement.offsetParent) - return firstElement.offsetParent.scrollHeight - firstElement.offsetTop; - return firstElement.offsetHeight; + + var offsetParent = firstElement.offsetParent; + if (offsetParent && offsetParent.scrollHeight > offsetParent.clientHeight) + return offsetParent.scrollHeight - firstElement.offsetTop; + + var total = 0; + while (firstElement && firstElement !== lastElement) { + total += firstElement.offsetHeight; + firstElement = firstElement.nextElementSibling; + } + return total; }, - + resize: function() { this._repaintAll(); @@ -390,6 +463,10 @@ WebInspector.TextEditorGutterPanel = function(textModel, syncDecorationsForLineL this.element = document.createElement("div"); this.element.className = "text-editor-lines"; + this._container = document.createElement("div"); + this._container.className = "inner-container"; + this.element.appendChild(this._container); + this.element.addEventListener("scroll", this._scroll.bind(this), false); this.freeCachedElements(); @@ -409,9 +486,59 @@ WebInspector.TextEditorGutterPanel.prototype = { _expandChunks: function(fromIndex, toIndex) { - for (var i = 0; i < this._textChunks.length; ++i) { + for (var i = 0; i < this._textChunks.length; ++i) this._textChunks[i].expanded = (fromIndex <= i && i < toIndex); + }, + + textChanged: function(oldRange, newRange) + { + if (!this._textChunks) { + this._buildChunks(); + return; } + + var linesDiff = newRange.linesCount - oldRange.linesCount; + if (linesDiff) { + // Remove old chunks (if needed). + for (var chunkNumber = this._textChunks.length - 1; chunkNumber >= 0 ; --chunkNumber) { + var chunk = this._textChunks[chunkNumber]; + if (chunk.startLine + chunk.linesCount <= this._textModel.linesCount) + break; + chunk.expanded = false; + this._container.removeChild(chunk.element); + } + this._textChunks.length = chunkNumber + 1; + + // Add new chunks (if needed). + var totalLines = 0; + if (this._textChunks.length) { + var lastChunk = this._textChunks[this._textChunks.length - 1]; + totalLines = lastChunk.startLine + lastChunk.linesCount; + } + for (var i = totalLines; i < this._textModel.linesCount; i += this._defaultChunkSize) { + var chunk = this._createNewChunk(i, i + this._defaultChunkSize); + this._textChunks.push(chunk); + this._container.appendChild(chunk.element); + } + this._repaintAll(); + } else { + // Decorations may have been removed, so we may have to sync those lines. + var chunkNumber = this._chunkNumberForLine(newRange.startLine); + var chunk = this._textChunks[chunkNumber]; + while (chunk && chunk.startLine <= newRange.endLine) { + if (chunk.linesCount === 1) + this._syncDecorationsForLineListener(chunk.startLine); + chunk = this._textChunks[++chunkNumber]; + } + } + }, + + syncClientHeight: function(clientHeight) + { + if (this.element.offsetHeight > clientHeight) + this._container.style.setProperty("padding-bottom", (this.element.offsetHeight - clientHeight) + "px"); + else + this._container.style.removeProperty("padding-bottom"); } } @@ -444,9 +571,8 @@ WebInspector.TextEditorGutterChunk = function(textViewer, startLine, endLine) this.element.appendChild(outerSpan); } else { var lineNumbers = []; - for (var i = startLine; i < endLine; ++i) { + for (var i = startLine; i < endLine; ++i) lineNumbers.push(i + 1); - } this.element.textContent = lineNumbers.join("\n"); } } @@ -454,16 +580,18 @@ WebInspector.TextEditorGutterChunk = function(textViewer, startLine, endLine) WebInspector.TextEditorGutterChunk.prototype = { addDecoration: function(decoration) { - if (typeof decoration === "string") { + this._textViewer.beginDomUpdates(); + if (typeof decoration === "string") this.element.addStyleClass(decoration); - } + this._textViewer.endDomUpdates(); }, removeDecoration: function(decoration) { - if (typeof decoration === "string") { + this._textViewer.beginDomUpdates(); + if (typeof decoration === "string") this.element.removeStyleClass(decoration); - } + this._textViewer.endDomUpdates(); }, get expanded() @@ -484,6 +612,8 @@ WebInspector.TextEditorGutterChunk.prototype = { if (this.linesCount === 1) return; + this._textViewer.beginDomUpdates(); + if (expanded) { this._expandedLineRows = []; var parentElement = this.element.parentElement; @@ -503,12 +633,14 @@ WebInspector.TextEditorGutterChunk.prototype = { elementInserted = true; parentElement.insertBefore(this.element, lineRow); } - this._textViewer._cachedRows.push(lineRow); parentElement.removeChild(lineRow); } + this._textViewer._cachedRows.push(lineRow); } delete this._expandedLineRows; } + + this._textViewer.endDomUpdates(); }, get height() @@ -518,6 +650,11 @@ WebInspector.TextEditorGutterChunk.prototype = { return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); }, + get offsetTop() + { + return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; + }, + _createRow: function(lineNumber) { var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div"); @@ -528,27 +665,42 @@ WebInspector.TextEditorGutterChunk.prototype = { } } -WebInspector.TextEditorMainPanel = function(textModel, url, syncScrollListener, syncDecorationsForLineListener) +WebInspector.TextEditorMainPanel = function(textModel, url, syncScrollListener, syncDecorationsForLineListener, enterTextChangeMode, exitTextChangeMode) { WebInspector.TextEditorChunkedPanel.call(this, textModel); this._syncScrollListener = syncScrollListener; this._syncDecorationsForLineListener = syncDecorationsForLineListener; + this._enterTextChangeMode = enterTextChangeMode; + this._exitTextChangeMode = exitTextChangeMode; this._url = url; this._highlighter = new WebInspector.TextEditorHighlighter(textModel, this._highlightDataReady.bind(this)); + this._readOnly = true; this.element = document.createElement("div"); this.element.className = "text-editor-contents"; this.element.tabIndex = 0; + this._container = document.createElement("div"); + this._container.className = "inner-container"; + this._container.tabIndex = 0; + this.element.appendChild(this._container); + this.element.addEventListener("scroll", this._scroll.bind(this), false); - this.element.addEventListener("keydown", this._handleKeyDown.bind(this), false); - var handleDOMUpdates = this._handleDOMUpdates.bind(this); - this.element.addEventListener("DOMCharacterDataModified", handleDOMUpdates, false); - this.element.addEventListener("DOMNodeInserted", handleDOMUpdates, false); - this.element.addEventListener("DOMNodeRemoved", handleDOMUpdates, false); + // FIXME: Remove old live editing functionality and Preferences.sourceEditorEnabled flag. + if (!Preferences.sourceEditorEnabled) + this._container.addEventListener("keydown", this._handleKeyDown.bind(this), false); + + // In WebKit the DOMNodeRemoved event is fired AFTER the node is removed, thus it should be + // attached to all DOM nodes that we want to track. Instead, we attach the DOMNodeRemoved + // listeners only on the line rows, and use DOMSubtreeModified to track node removals inside + // the line rows. For more info see: https://bugs.webkit.org/show_bug.cgi?id=55666 + this._handleDOMUpdatesCallback = this._handleDOMUpdates.bind(this); + this._container.addEventListener("DOMCharacterDataModified", this._handleDOMUpdatesCallback, false); + this._container.addEventListener("DOMNodeInserted", this._handleDOMUpdatesCallback, false); + this._container.addEventListener("DOMSubtreeModified", this._handleDOMUpdatesCallback, false); this.freeCachedElements(); this._buildChunks(); @@ -560,6 +712,21 @@ WebInspector.TextEditorMainPanel.prototype = { this._highlighter.mimeType = mimeType; }, + set readOnly(readOnly) + { + // FIXME: Remove the Preferences.sourceEditorEnabled flag. + if (!Preferences.sourceEditorEnabled) + return; + + this.beginDomUpdates(); + this._readOnly = readOnly; + if (this._readOnly) + this._container.removeStyleClass("text-editor-editable"); + else + this._container.addStyleClass("text-editor-editable"); + this.endDomUpdates(); + }, + markAndRevealRange: function(range) { if (this._rangeToMark) { @@ -653,6 +820,9 @@ WebInspector.TextEditorMainPanel.prototype = { _buildChunks: function() { this._highlighter.reset(); + for (var i = 0; i < this._textModel.linesCount; ++i) + this._textModel.removeAttribute(i, "highlight"); + WebInspector.TextEditorChunkedPanel.prototype._buildChunks.call(this); }, @@ -672,9 +842,8 @@ WebInspector.TextEditorMainPanel.prototype = { this._highlighter.highlight(lastVisibleLine); delete this._muteHighlightListener; - for (var i = 0; i < this._textChunks.length; ++i) { + for (var i = 0; i < this._textChunks.length; ++i) this._textChunks[i].expanded = (fromIndex <= i && i < toIndex); - } this._restoreSelection(selection); }, @@ -686,13 +855,46 @@ WebInspector.TextEditorMainPanel.prototype = { this._paintLines(fromLine, toLine, true /*restoreSelection*/); }, + _markSkippedPaintLines: function(startLine, endLine) + { + if (!this._skippedPaintLines) + this._skippedPaintLines = [ { startLine: startLine, endLine: endLine } ]; + else { + for (var i = 0; i < this._skippedPaintLines.length; ++i) { + var chunk = this._skippedPaintLines[i]; + if (chunk.startLine <= endLine && chunk.endLine >= startLine) { + chunk.startLine = Math.min(chunk.startLine, startLine); + chunk.endLine = Math.max(chunk.endLine, endLine); + return; + } + } + this._skippedPaintLines.push({ startLine: startLine, endLine: endLine }); + } + }, + + _paintSkippedLines: function() + { + if (!this._skippedPaintLines || this._dirtyLines) + return; + for (var i = 0; i < this._skippedPaintLines.length; ++i) { + var chunk = this._skippedPaintLines[i]; + this._paintLines(chunk.startLine, chunk.endLine); + } + delete this._skippedPaintLines; + }, + _paintLines: function(fromLine, toLine, restoreSelection) { + if (this._dirtyLines) { + this._markSkippedPaintLines(fromLine, toLine); + return; + } + var selection; - var chunk = this._chunkForLine(fromLine); + var chunk = this.chunkForLine(fromLine); for (var i = fromLine; i < toLine; ++i) { if (i >= chunk.startLine + chunk.linesCount) - chunk = this._chunkForLine(i); + chunk = this.chunkForLine(i); var lineRow = chunk.getExpandedLineRow(i); if (!lineRow) continue; @@ -706,46 +908,56 @@ WebInspector.TextEditorMainPanel.prototype = { _paintLine: function(lineRow, lineNumber) { - var highlight = this._textModel.getAttribute(lineNumber, "highlight"); - if (!highlight) { - if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) - this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn); + if (this._dirtyLines) { + this._markSkippedPaintLines(lineNumber, lineNumber + 1); return; } - lineRow.removeChildren(); - var line = this._textModel.line(lineNumber); - if (!line) - lineRow.appendChild(document.createElement("br")); - - var plainTextStart = -1; - for (var j = 0; j < line.length;) { - if (j > 1000) { - // This line is too long - do not waste cycles on minified js highlighting. - if (plainTextStart === -1) - plainTextStart = j; - break; + this.beginDomUpdates(); + try { + var highlight = this._textModel.getAttribute(lineNumber, "highlight"); + if (!highlight) { + if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) + this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn); + return; } - var attribute = highlight[j]; - if (!attribute || !attribute.tokenType) { - if (plainTextStart === -1) - plainTextStart = j; - j++; - } else { - if (plainTextStart !== -1) { - this._appendTextNode(lineRow, line.substring(plainTextStart, j)); - plainTextStart = -1; + + lineRow.removeChildren(); + var line = this._textModel.line(lineNumber); + if (!line) + lineRow.appendChild(document.createElement("br")); + + var plainTextStart = -1; + for (var j = 0; j < line.length;) { + if (j > 1000) { + // This line is too long - do not waste cycles on minified js highlighting. + if (plainTextStart === -1) + plainTextStart = j; + break; + } + var attribute = highlight[j]; + if (!attribute || !attribute.tokenType) { + if (plainTextStart === -1) + plainTextStart = j; + j++; + } else { + if (plainTextStart !== -1) { + this._appendTextNode(lineRow, line.substring(plainTextStart, j)); + plainTextStart = -1; + } + this._appendSpan(lineRow, line.substring(j, j + attribute.length), attribute.tokenType); + j += attribute.length; } - this._appendSpan(lineRow, line.substring(j, j + attribute.length), attribute.tokenType); - j += attribute.length; } + if (plainTextStart !== -1) + this._appendTextNode(lineRow, line.substring(plainTextStart, line.length)); + if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) + this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn); + if (lineRow.decorationsElement) + lineRow.appendChild(lineRow.decorationsElement); + } finally { + this.endDomUpdates(); } - if (plainTextStart !== -1) - this._appendTextNode(lineRow, line.substring(plainTextStart, line.length)); - if (this._rangeToMark && this._rangeToMark.startLine === lineNumber) - this._markedRangeElement = highlightSearchResult(lineRow, this._rangeToMark.startColumn, this._rangeToMark.endColumn - this._rangeToMark.startColumn); - if (lineRow.decorationsElement) - lineRow.appendChild(lineRow.decorationsElement); }, _releaseLinesHighlight: function(lineRow) @@ -774,7 +986,7 @@ WebInspector.TextEditorMainPanel.prototype = { return null; var selectionRange = selection.getRangeAt(0); // Selection may be outside of the viewer. - if (!this.element.isAncestor(selectionRange.startContainer) || !this.element.isAncestor(selectionRange.endContainer)) + if (!this._container.isAncestor(selectionRange.startContainer) || !this._container.isAncestor(selectionRange.endContainer)) return null; var start = this._selectionToPosition(selectionRange.startContainer, selectionRange.startOffset); var end = selectionRange.collapsed ? start : this._selectionToPosition(selectionRange.endContainer, selectionRange.endOffset); @@ -795,9 +1007,9 @@ WebInspector.TextEditorMainPanel.prototype = { _selectionToPosition: function(container, offset) { - if (container === this.element && offset === 0) + if (container === this._container && offset === 0) return { line: 0, column: 0 }; - if (container === this.element && offset === 1) + if (container === this._container && offset === 1) return { line: this._textModel.linesCount - 1, column: this._textModel.lineLength(this._textModel.linesCount - 1) }; var lineRow = container.enclosingNodeOrSelfWithNodeName("DIV"); @@ -835,7 +1047,7 @@ WebInspector.TextEditorMainPanel.prototype = { _positionToSelection: function(line, column) { - var chunk = this._chunkForLine(line); + var chunk = this.chunkForLine(line); var lineRow = chunk.getExpandedLineRow(line); if (lineRow) var rangeBoundary = lineRow.rangeBoundaryForOffset(column); @@ -909,17 +1121,268 @@ WebInspector.TextEditorMainPanel.prototype = { _handleDOMUpdates: function(e) { + if (this._domUpdateCoalescingLevel) + return; + var target = e.target; + if (target === this._container) + return; + var lineRow = target.enclosingNodeOrSelfWithClass("webkit-line-content"); - if (lineRow === target || !lineRow || !lineRow.decorationsElement || !lineRow.decorationsElement.isAncestor(target)) + if (!lineRow) return; - if (this._syncDecorationsForLineListener) { - // Wait until this event is processed and only then sync the sizes. This is necessary in - // case of the DOMNodeRemoved event, because it is dispatched before the removal takes place. - setTimeout(function() { + + if (lineRow.decorationsElement && (lineRow.decorationsElement === target || lineRow.decorationsElement.isAncestor(target))) { + if (this._syncDecorationsForLineListener) this._syncDecorationsForLineListener(lineRow.lineNumber); - }.bind(this), 0); + return; + } + + if (this._readOnly) + return; + + var lineNumber = lineRow.lineNumber; + if (this._dirtyLines) { + this._dirtyLines.start = Math.min(this._dirtyLines.start, lineNumber); + this._dirtyLines.end = Math.max(this._dirtyLines.end, lineNumber + 1); + } else { + this._dirtyLines = { start: lineNumber, end: lineNumber + 1 }; + setTimeout(this._applyDomUpdates.bind(this), 0); + // Remove marked ranges, if any. + this.markAndRevealRange(null); + } + }, + + _applyDomUpdates: function() + { + if (!this._dirtyLines) + return; + + // Check if the editor had been set readOnly by the moment when this async callback got executed. + if (this._readOnly) { + delete this._dirtyLines; + return; + } + + // This is a "foreign" call outside of this class. Should be before we delete the dirty lines flag. + this._enterTextChangeMode(); + + var dirtyLines = this._dirtyLines; + delete this._dirtyLines; + + var firstChunkNumber = this._chunkNumberForLine(dirtyLines.start); + var startLine = this._textChunks[firstChunkNumber].startLine; + var endLine = this._textModel.linesCount; + + // Collect lines. + var firstLineRow; + if (firstChunkNumber) { + var chunk = this._textChunks[firstChunkNumber - 1]; + firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element; + firstLineRow = firstLineRow.nextSibling; + } else + firstLineRow = this._container.firstChild; + + var lines = []; + for (var lineRow = firstLineRow; lineRow; lineRow = lineRow.nextSibling) { + if (typeof lineRow.lineNumber === "number" && lineRow.lineNumber >= dirtyLines.end) { + endLine = lineRow.lineNumber; + break; + } + // Update with the newest lineNumber, so that the call to the _getSelection method below should work. + lineRow.lineNumber = startLine + lines.length; + this._collectLinesFromDiv(lines, lineRow); + } + + // Try to decrease the range being replaced if possible. + var startOffset = 0; + while (startLine < dirtyLines.start && startOffset < lines.length) { + if (this._textModel.line(startLine) !== lines[startOffset]) + break; + ++startOffset; + ++startLine; + } + + var endOffset = lines.length; + while (endLine > dirtyLines.end && endOffset > startOffset) { + if (this._textModel.line(endLine - 1) !== lines[endOffset - 1]) + break; + --endOffset; + --endLine; + } + + lines = lines.slice(startOffset, endOffset); + + var selection = this._getSelection(); + + if (lines.length === 0 && endLine < this._textModel.linesCount) { + var oldRange = new WebInspector.TextRange(startLine, 0, endLine, 0); + var newRange = this._textModel.setText(oldRange, ""); + } else { + var oldRange = new WebInspector.TextRange(startLine, 0, endLine - 1, this._textModel.lineLength(endLine - 1)); + var newRange = this._textModel.setText(oldRange, lines.join("\n")); + } + + this.beginDomUpdates(); + this._removeDecorationsInRange(oldRange); + this._updateChunksForRanges(oldRange, newRange); + this._updateHighlightsForRange(newRange); + this._paintSkippedLines(); + this.endDomUpdates(); + + this._restoreSelection(selection); + + this._exitTextChangeMode(oldRange, newRange); + }, + + _removeDecorationsInRange: function(range) + { + for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) { + var chunk = this._textChunks[i]; + if (chunk.startLine > range.endLine) + break; + chunk.removeAllDecorations(); + } + }, + + _updateChunksForRanges: function(oldRange, newRange) + { + // Update the chunks in range: firstChunkNumber <= index <= lastChunkNumber + var firstChunkNumber = this._chunkNumberForLine(oldRange.startLine); + var lastChunkNumber = firstChunkNumber; + while (lastChunkNumber + 1 < this._textChunks.length) { + if (this._textChunks[lastChunkNumber + 1].startLine > oldRange.endLine) + break; + ++lastChunkNumber; + } + + var startLine = this._textChunks[firstChunkNumber].startLine; + var linesCount = this._textChunks[lastChunkNumber].startLine + this._textChunks[lastChunkNumber].linesCount - startLine; + var linesDiff = newRange.linesCount - oldRange.linesCount; + linesCount += linesDiff; + + if (linesDiff) { + // Lines shifted, update the line numbers of the chunks below. + for (var chunkNumber = lastChunkNumber + 1; chunkNumber < this._textChunks.length; ++chunkNumber) + this._textChunks[chunkNumber].startLine += linesDiff; + } + + var firstLineRow; + if (firstChunkNumber) { + var chunk = this._textChunks[firstChunkNumber - 1]; + firstLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine + chunk.linesCount - 1) : chunk.element; + firstLineRow = firstLineRow.nextSibling; + } else + firstLineRow = this._container.firstChild; + + // Most frequent case: a chunk remained the same. + for (var chunkNumber = firstChunkNumber; chunkNumber <= lastChunkNumber; ++chunkNumber) { + var chunk = this._textChunks[chunkNumber]; + var lineNumber = chunk.startLine; + for (var lineRow = firstLineRow; lineRow && lineNumber < chunk.startLine + chunk.linesCount; lineRow = lineRow.nextSibling) { + if (lineRow.lineNumber !== lineNumber || lineRow !== chunk.getExpandedLineRow(lineNumber) || lineRow.textContent !== this._textModel.line(lineNumber) || !lineRow.firstChild) + break; + ++lineNumber; + } + if (lineNumber < chunk.startLine + chunk.linesCount) + break; + chunk.updateCollapsedLineRow(); + ++firstChunkNumber; + firstLineRow = lineRow; + startLine += chunk.linesCount; + linesCount -= chunk.linesCount; + } + + if (firstChunkNumber > lastChunkNumber && linesCount === 0) + return; + + // Maybe merge with the next chunk, so that we should not create 1-sized chunks when appending new lines one by one. + var chunk = this._textChunks[lastChunkNumber + 1]; + var linesInLastChunk = linesCount % this._defaultChunkSize; + if (chunk && !chunk.decorated && linesInLastChunk > 0 && linesInLastChunk + chunk.linesCount <= this._defaultChunkSize) { + ++lastChunkNumber; + linesCount += chunk.linesCount; + } + + var scrollTop = this.element.scrollTop; + var scrollLeft = this.element.scrollLeft; + + // Delete all DOM elements that were either controlled by the old chunks, or have just been inserted. + var firstUnmodifiedLineRow = null; + var chunk = this._textChunks[lastChunkNumber + 1]; + if (chunk) { + firstUnmodifiedLineRow = chunk.expanded ? chunk.getExpandedLineRow(chunk.startLine) : chunk.element; } + while (firstLineRow && firstLineRow !== firstUnmodifiedLineRow) { + var lineRow = firstLineRow; + firstLineRow = firstLineRow.nextSibling; + this._container.removeChild(lineRow); + } + + // Replace old chunks with the new ones. + for (var chunkNumber = firstChunkNumber; linesCount > 0; ++chunkNumber) { + var chunkLinesCount = Math.min(this._defaultChunkSize, linesCount); + var newChunk = this._createNewChunk(startLine, startLine + chunkLinesCount); + this._container.insertBefore(newChunk.element, firstUnmodifiedLineRow); + + if (chunkNumber <= lastChunkNumber) + this._textChunks[chunkNumber] = newChunk; + else + this._textChunks.splice(chunkNumber, 0, newChunk); + startLine += chunkLinesCount; + linesCount -= chunkLinesCount; + } + if (chunkNumber <= lastChunkNumber) + this._textChunks.splice(chunkNumber, lastChunkNumber - chunkNumber + 1); + + this.element.scrollTop = scrollTop; + this.element.scrollLeft = scrollLeft; + }, + + _updateHighlightsForRange: function(range) + { + var visibleFrom = this.element.scrollTop; + var visibleTo = this.element.scrollTop + this.element.clientHeight; + + var result = this._findVisibleChunks(visibleFrom, visibleTo); + var chunk = this._textChunks[result.end - 1]; + var lastVisibleLine = chunk.startLine + chunk.linesCount; + + lastVisibleLine = Math.max(lastVisibleLine, range.endLine); + + var updated = this._highlighter.updateHighlight(range.startLine, lastVisibleLine); + if (!updated) { + // Highlights for the chunks below are invalid, so just collapse them. + for (var i = this._chunkNumberForLine(range.startLine); i < this._textChunks.length; ++i) + this._textChunks[i].expanded = false; + } + + this._repaintAll(); + }, + + _collectLinesFromDiv: function(lines, element) + { + var textContents = []; + var node = element.traverseNextNode(element); + while (node) { + if (element.decorationsElement === node) { + node = node.nextSibling; + continue; + } + if (node.nodeName.toLowerCase() === "br") + textContents.push("\n"); + else if (node.nodeType === Node.TEXT_NODE) + textContents.push(node.textContent); + node = node.traverseNextNode(element); + } + + var textContent = textContents.join(""); + // The last \n (if any) does not "count" in a DIV. + textContent = textContent.replace(/\n$/, ""); + + textContents = textContent.split("\n"); + for (var i = 0; i < textContents.length; ++i) + lines.push(textContents[i]); } } @@ -933,49 +1396,73 @@ WebInspector.TextEditorMainChunk = function(textViewer, startLine, endLine) this.element = document.createElement("div"); this.element.lineNumber = startLine; this.element.className = "webkit-line-content"; + this.element.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false); - this.startLine = startLine; + this._startLine = startLine; endLine = Math.min(this._textModel.linesCount, endLine); this.linesCount = endLine - startLine; this._expanded = false; - var lines = []; - for (var i = startLine; i < endLine; ++i) { - lines.push(this._textModel.line(i)); - } - - this.element.textContent = lines.join("\n"); - - // The last empty line will get swallowed otherwise. - if (!lines[lines.length - 1]) - this.element.appendChild(document.createElement("br")); + this.updateCollapsedLineRow(); } WebInspector.TextEditorMainChunk.prototype = { addDecoration: function(decoration) { - if (typeof decoration === "string") { + this._textViewer.beginDomUpdates(); + if (typeof decoration === "string") this.element.addStyleClass(decoration); - return; - } - if (!this.element.decorationsElement) { - this.element.decorationsElement = document.createElement("div"); - this.element.decorationsElement.className = "webkit-line-decorations"; - this.element.appendChild(this.element.decorationsElement); + else { + if (!this.element.decorationsElement) { + this.element.decorationsElement = document.createElement("div"); + this.element.decorationsElement.className = "webkit-line-decorations"; + this.element.appendChild(this.element.decorationsElement); + } + this.element.decorationsElement.appendChild(decoration); } - this.element.decorationsElement.appendChild(decoration); + this._textViewer.endDomUpdates(); }, removeDecoration: function(decoration) { - if (typeof decoration === "string") { + this._textViewer.beginDomUpdates(); + if (typeof decoration === "string") this.element.removeStyleClass(decoration); - return; + else if (this.element.decorationsElement) + this.element.decorationsElement.removeChild(decoration); + this._textViewer.endDomUpdates(); + }, + + removeAllDecorations: function() + { + this._textViewer.beginDomUpdates(); + this.element.className = "webkit-line-content"; + if (this.element.decorationsElement) { + this.element.removeChild(this.element.decorationsElement); + delete this.element.decorationsElement; + } + this._textViewer.endDomUpdates(); + }, + + get decorated() + { + return this.element.className !== "webkit-line-content" || !!(this.element.decorationsElement && this.element.decorationsElement.firstChild); + }, + + get startLine() + { + return this._startLine; + }, + + set startLine(startLine) + { + this._startLine = startLine; + this.element.lineNumber = startLine; + if (this._expandedLineRows) { + for (var i = 0; i < this._expandedLineRows.length; ++i) + this._expandedLineRows[i].lineNumber = startLine + i; } - if (!this.element.decorationsElement) - return; - this.element.decorationsElement.removeChild(decoration); }, get expanded() @@ -996,6 +1483,8 @@ WebInspector.TextEditorMainChunk.prototype = { return; } + this._textViewer.beginDomUpdates(); + if (expanded) { this._expandedLineRows = []; var parentElement = this.element.parentElement; @@ -1016,12 +1505,14 @@ WebInspector.TextEditorMainChunk.prototype = { elementInserted = true; parentElement.insertBefore(this.element, lineRow); } - this._textViewer._releaseLinesHighlight(lineRow); parentElement.removeChild(lineRow); } + this._textViewer._releaseLinesHighlight(lineRow); } delete this._expandedLineRows; } + + this._textViewer.endDomUpdates(); }, get height() @@ -1031,11 +1522,17 @@ WebInspector.TextEditorMainChunk.prototype = { return this._textViewer._totalHeight(this._expandedLineRows[0], this._expandedLineRows[this._expandedLineRows.length - 1]); }, + get offsetTop() + { + return (this._expandedLineRows && this._expandedLineRows.length) ? this._expandedLineRows[0].offsetTop : this.element.offsetTop; + }, + _createRow: function(lineNumber) { var lineRow = this._textViewer._cachedRows.pop() || document.createElement("div"); lineRow.lineNumber = lineNumber; lineRow.className = "webkit-line-content"; + lineRow.addEventListener("DOMNodeRemoved", this._textViewer._handleDOMUpdatesCallback, false); lineRow.textContent = this._textModel.line(lineNumber); if (!lineRow.textContent) lineRow.appendChild(document.createElement("br")); @@ -1049,5 +1546,22 @@ WebInspector.TextEditorMainChunk.prototype = { if (!this._expandedLineRows) return this.element; return this._expandedLineRows[lineNumber - this.startLine]; + }, + + updateCollapsedLineRow: function() + { + if (this.linesCount === 1 && this._expanded) + return; + + var lines = []; + for (var i = this.startLine; i < this.startLine + this.linesCount; ++i) + lines.push(this._textModel.line(i)); + + this.element.removeChildren(); + this.element.textContent = lines.join("\n"); + + // The last empty line will get swallowed otherwise. + if (!lines[lines.length - 1]) + this.element.appendChild(document.createElement("br")); } } |