From b503e65e0370cb01a251a34719bde5ddb508e2c4 Mon Sep 17 00:00:00 2001 From: aldendaniels Date: Fri, 3 Jan 2014 14:59:22 -0600 Subject: [PATCH] Break on applyDelta into its own module This makes it possible to break out helper functions without exposing them to the rest of the document class. Also, long term, we may want to have a stand-alone test suite for applyDelta, so it makes sense in its own file. All other changes involve syntax corrections (some syntax issues were mine, others pre-existed) to make the documentation compilation work. --- lib/ace/apply_delta.js | 137 +++++++++++++++++++++++++++ lib/ace/document.js | 204 +++++++++++++--------------------------- lib/ace/edit_session.js | 14 +-- lib/ace/editor.js | 8 +- lib/ace/scrollbar.js | 8 +- 5 files changed, 215 insertions(+), 156 deletions(-) create mode 100644 lib/ace/apply_delta.js diff --git a/lib/ace/apply_delta.js b/lib/ace/apply_delta.js new file mode 100644 index 00000000..02c0a498 --- /dev/null +++ b/lib/ace/apply_delta.js @@ -0,0 +1,137 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Distributed under the BSD license: + * + * Copyright (c) 2010, Ajax.org B.V. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Ajax.org B.V. nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { +"use strict"; + +var Range = require("./range").Range; + +function splitLine (lines, point) { + var text = lines[point.row]; + lines[point.row] = text.slice(0, point.column); + lines.splice(point.row + 1, 0, text.slice(point.column)); +} + +function joinLineWithNext(lines, row) { + lines[row] += lines[row + 1]; + lines.splice(row + 1, 1); +} + +function throwDeltaError(delta, errorText){ + errorText = 'Invalid Delta: ' + errorText; + console.log(errorText, delta); + throw errorText; +} + +function validateDelta(lines, delta) { + + // Validate action. + if (delta.action != 'insert' && delta.action != 'delete') + fnThrow('Delta action must be "insert" or "delete".'); + + // Validate lines. + if (!delta.lines instanceof Array) + fnThrow('Delta lines must be an array'); + + // Validate range type. + if (!delta.range instanceof Range) + fnThrow('Range object is not an instance of the Range class'); + + // Validate start point. + var start = delta.range.start; + if (Math.min(Math.max(start.row, 0), lines.length - 1 ) != start.row || + Math.min(Math.max(start.column, 0), lines[start.row].length) != start.column) + { + fnThrow('Range start point not contained in document'); + } + + // Validate ending row offset. + if (delta.lines.length - 1 != delta.range.end.row - delta.range.start.row) + fnThrow('Range row offsets does not match delta lines'); + + // TODO: + // - Validate that the ending column offset matches the lines. + // - Validate the deleted lines match the lines in the document. +} + + +exports.applyDelta = function(lines, delta) { + + // Validate delta. + validateDelta(lines, delta); + + // Apply delta. + if (delta.range.start.row == delta.range.end.row) + { + // Apply single-line delta. + // Note: The multi-line code below correctly handle single-line + // deltas too, but we need to short-circuit for speed. + var row = delta.range.start.row; + var startColumn = delta.range.start.column; + var endColumn = delta.range.end.column; + var line = lines[row]; + switch (delta.action) { + + case 'insert': + lines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); + break; + + case 'delete': + lines[row] = line.substring(0, startColumn) + line.substring(endColumn); + break; + } + } else { + + // Apply multi-line delta. + switch (delta.action) { + + case 'insert': + splitLine(lines, delta.range.start); + for (var i = 0; i < delta.lines.length; i++) { + var row = delta.range.start.row + 1 + i; + lines.splice(row, 0, delta.lines[i]); + } + joinLineWithNext(lines, delta.range.start.row); + joinLineWithNext(lines, delta.range.end.row); + break; + + case 'delete': + splitLine(lines, delta.range.end); + splitLine(lines, delta.range.start); + lines.splice( + delta.range.start.row + 1, // Where to start deleting + delta.range.end.row - delta.range.start.row + 1 // Num lines to delete. + ); + joinLineWithNext(lines, delta.range.start.row); + break; + } + } +} +}); diff --git a/lib/ace/document.js b/lib/ace/document.js index 9d6aaac3..768182a4 100644 --- a/lib/ace/document.js +++ b/lib/ace/document.js @@ -32,6 +32,7 @@ define(function(require, exports, module) { "use strict"; var oop = require("./lib/oop"); +var applyDelta = require("./apply_delta").applyDelta; var EventEmitter = require("./lib/event_emitter").EventEmitter; var Range = require("./range").Range; var Anchor = require("./anchor").Anchor; @@ -56,7 +57,7 @@ var Document = function(textOrLines) { // There has to be one line at least in the document. If you pass an empty // string to the insert function, nothing will happen. Workaround. - if (textOrLines.length == 0) { + if (textOrLines.length === 0) { this.$lines = [""]; } else if (Array.isArray(textOrLines)) { this.insertMergedLines({row: 0, column: 0}, textOrLines); @@ -107,10 +108,10 @@ var Document = function(textOrLines) { **/ // check for IE split bug - if ("aaa".split(/a/).length == 0) + if ("aaa".split(/a/).length === 0) this.$split = function(text) { return text.replace(/\r\n|\r/g, "\n").split("\n"); - } + }; else this.$split = function(text) { return text.split(/\r\n|\r|\n/); @@ -205,30 +206,41 @@ var Document = function(textOrLines) { }; /** - * [Given a range within the document, this function returns all the text within that range as a single string.]{: #Document.getTextRange.desc} - * @param {Range} range The range to work with + * Returns all the text within `range` as a single string. + * @param {Range} range The range to work with. * * @returns {String} **/ this.getTextRange = function(range) { - return this._getLinesForRange(range).join(this.getNewLineCharacter()); + return this.getLinesForRange(range).join(this.getNewLineCharacter()); }; - this._getLinesForRange = function(range) { + /** + * Returns all the text within `range` as an array of lines. + * @param {Range} range The range to work with. + * + * @returns {Array} + **/ + this.getLinesForRange = function(range) { + var lines; if (range.start.row == range.end.row) { - return [this.getLine(range.start.row) - .substring(range.start.column, range.end.column)]; + + // Handle a single-line range. + lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)]; + + } else { + + // Handle a multi-line range. + lines = this.getLines(range.start.row, range.end.row); + lines[0] = (lines[0] || "").substring(range.start.column); + var l = lines.length - 1; + if (range.end.row - range.start.row == l) + lines[l] = lines[l].substring(0, range.end.column); } - var lines = this.getLines(range.start.row, range.end.row); - lines[0] = (lines[0] || "").substring(range.start.column); - var l = lines.length - 1; - if (range.end.row - range.start.row == l) - lines[l] = lines[l].substring(0, range.end.column); return lines; }; this.$clipPosition = function(position) { - var length = this.getLength(); if (position.row >= length) { position.row = Math.max(0, length - 1); @@ -240,8 +252,8 @@ var Document = function(textOrLines) { return position; }; - this.$getClippedRange = function(range) - { + this.$getClippedRange = function(range) { + // Get Range object. if (!range instanceof Range) range = Range.fromPoints(range.start, range.end); @@ -250,67 +262,29 @@ var Document = function(textOrLines) { this.$clipPosition(range.start); this.$clipPosition(range.end); return range; - } + }; - this.$validateDelta = function(delta) - { - function fnThrow(errorText) - { - errorText = 'Invalid Delta: ' + errorText; - console.log(errorText, delta); - throw errorText; - } - - // Validate action. - if (delta.action != 'insert' && delta.action != 'delete') - fnThrow('Delta action must be "insert" or "delete".'); - - // Validate lines. - if (!delta.lines instanceof Array) - fnThrow('Delta lines must be an array'); - - // Validate range type. - if (!delta.range instanceof Range) - fnThrow('Range object is not an instance of the Range class'); - - // Validate start point. - var start = delta.range.start; - if (Math.min(Math.max(start.row, 0), this.getLength() - 1 ) != start.row || - Math.min(Math.max(start.column, 0), this.$lines[start.row].length) != start.column) - { - fnThrow('Range start point not contained in document'); - } - - // Validate ending row offset. - if (delta.lines.length - 1 != delta.range.end.row - delta.range.start.row) - fnThrow('Range row offsets does not match delta lines'); - - // TODO: - // - Validate that the ending column offset matches the lines. - // - Validate the deleted lines match the lines in the document. - }, - // Deprecated methods retained for backwards compatibility. this.insert = function(position, text){ console.warn('Use of document.insert is deprecated. Use the insertText method instead.'); return this.insertText(position, text); - } + }; this.insertLines = function(row, lines) { console.warn('Use of document.insertLines is deprecated. Use the insertFullLines method instead.'); return this.insertFullLines(row, lines); - } + }; this.removeLines = function(firstRow, lastRow) { console.warn('Use of document.removeLines is deprecated. Use the removeFullLines method instead.'); return this.removeFullLines(firstRow, lastRow); - } + }; this.insertNewLine = function(position) { console.warn('Use of document.insertNewLine is deprecated. Use insertMergedLines(position, [\'\', \'\']) instead.'); return this.insertMergedLines(position, ['', '']); - } + }; this.insertInLine = function(position, text) { console.warn('Use of document.insertInLine is deprecated. Use insertText instead.'); return this.insertText(position, text); - } + }; /** * Inserts a block of `text` at the indicated `position`. @@ -374,12 +348,12 @@ var Document = function(textOrLines) { // Insert after the last row in the document. lines = [''].concat(lines); row--; - var column = this.$lines[row].length; + column = this.$lines[row].length; } // Insert. this.insertMergedLines({row: row, column: column}, lines); - }, + }; /** * Inserts the elements in `lines` into the document, starting at the position index given by `row`. This method also triggers the `'change'` event. @@ -412,7 +386,7 @@ var Document = function(textOrLines) { }); return endPoint; - } + }; /** * Removes the `range` from the document. @@ -427,7 +401,7 @@ var Document = function(textOrLines) { this.applyDelta({ action: 'delete', range: range, - lines: this._getLinesForRange(range), + lines: this.getLinesForRange(range), }); return range.start; }; @@ -450,7 +424,7 @@ var Document = function(textOrLines) { this.applyDelta({ action: "delete", range: range, - lines: this._getLinesForRange(range) + lines: this.getLinesForRange(range) }); return range.start; @@ -487,7 +461,7 @@ var Document = function(textOrLines) { this.applyDelta({ action: "delete", range: range, - lines: this._getLinesForRange(range) + lines: this.getLinesForRange(range) }); // Return the deleted lines. @@ -524,7 +498,7 @@ var Document = function(textOrLines) { this.replace = function(range, text) { if (!range instanceof Range) range = Range.fromPoints(range.start, range.end); - if (text.length == 0 && range.isEmpty()) + if (text.length === 0 && range.isEmpty()) return range.start; // Shortcut: If the text we want to insert is the same as it is already @@ -533,8 +507,9 @@ var Document = function(textOrLines) { return range.end; this.remove(range); + var end; if (text) { - var end = this.insertText(range.start, text); + end = this.insertText(range.start, text); } else { end = range.start; @@ -544,7 +519,8 @@ var Document = function(textOrLines) { }; /** - * Applies all the changes previously accumulated. These can be either `'insert'` or `'delete'`. + * Applies all changes in `deltas` to the document. + * @param {Array} deltas An array of delta objects (can include 'insert' and 'delete' actions) **/ this.applyDeltas = function(deltas) { for (var i=0; i=0; i--) { @@ -629,6 +538,19 @@ var Document = function(textOrLines) { } }; + /** + * Applies `delta` to the document. + * @param {Object} delta A delta object (can include 'insert' and 'delete' actions) + **/ + this.applyDelta = function(delta) { + applyDelta(this.$lines, delta); + this._emit("change", { data: delta }); + }; + + /** + * Reverts `delta` from the document. + * @param {Object} delta A delta object (can include 'insert' and 'delete' actions) + **/ this.revertDelta = function(delta) { this.applyDelta({ @@ -636,8 +558,8 @@ var Document = function(textOrLines) { range: delta.range.clone(), lines: delta.lines.slice() }); - }, - + }; + /** * Converts an index position in a document to a `{row, column}` object. * diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index 0845418b..0d4df080 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -1131,8 +1131,8 @@ var EditSession = function(text, mode) { // Deprecated method retained for backwards compatibility. this.insert = function(position, text){ console.warn('Use of editsession.insert is deprecated. Use the insertText method instead.'); - return this.insertText(position, text) - } + return this.insertText(position, text); + }; /** * Inserts a block of `text` and the indicated `position`. @@ -1168,8 +1168,8 @@ var EditSession = function(text, mode) { * **/ this.removeFullLines = function(firstRow, lastRow){ - return this.doc.removeFullLines(firstRow, lastRow) - } + return this.doc.removeFullLines(firstRow, lastRow); + }; /** * Reverts previous changes to your document. @@ -1619,7 +1619,7 @@ var EditSession = function(text, mode) { * @private **/ this.adjustWrapLimit = function(desiredLimit, $printMargin) { - var limits = this.$wrapLimitRange + var limits = this.$wrapLimitRange; if (limits.max < 0) limits = {min: $printMargin, max: $printMargin}; var wrapLimit = this.$constrainWrapLimit(desiredLimit, limits.min, limits.max); @@ -1738,7 +1738,7 @@ var EditSession = function(text, mode) { var foldLine = this.getFoldLine(firstRow); var idx = 0; if (foldLine) { - var cmp = foldLine.range.compareInside(start.row, start.column) + var cmp = foldLine.range.compareInside(start.row, start.column); // Inside of the foldLine range. Need to split stuff up. if (cmp == 0) { foldLine = foldLine.split(start.row, start.column); @@ -2215,7 +2215,7 @@ var EditSession = function(text, mode) { return { row: maxRow, column: this.getLine(maxRow).length - } + }; } else { line = this.getLine(docRow); foldLine = null; diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 6f5980a1..79f59e68 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -833,8 +833,8 @@ var Editor = function(renderer, session) { // Deprecated method retained for backwards compatibility. this.insert = function(text){ console.warn('Use of editor.insert is deprecated. Use the insertText method instead.'); - return this.insertText(text) - } + return this.insertText(text); + }; /** * Inserts `text` into wherever the cursor is pointing. @@ -876,7 +876,7 @@ var Editor = function(renderer, session) { } if (text == "\n" || text == "\r\n") { - var line = session.getLine(cursor.row) + var line = session.getLine(cursor.row); if (cursor.column > line.search(/\S|$/)) { var d = line.substr(cursor.column).search(/\S|$/); session.doc.removeInLine(cursor.row, cursor.column, cursor.column + d); @@ -1362,7 +1362,7 @@ var Editor = function(renderer, session) { } } - var line = session.getLine(range.start.row) + var line = session.getLine(range.start.row); var position = range.start; var size = session.getTabSize(); var column = session.documentToScreenColumn(position.row, position.column); diff --git a/lib/ace/scrollbar.js b/lib/ace/scrollbar.js index 28f41fe4..466d1af1 100644 --- a/lib/ace/scrollbar.js +++ b/lib/ace/scrollbar.js @@ -156,9 +156,9 @@ oop.inherits(VScrollBar, ScrollBar); * Sets the scroll top of the scroll bar. * @param {Number} scrollTop The new scroll top **/ - // on chrome 17+ for small zoom levels after calling this function - // this.element.scrollTop != scrollTop which makes page to scroll up. this.setScrollTop = function(scrollTop) { + // on chrome 17+ for small zoom levels after calling this function + // this.element.scrollTop != scrollTop which makes page to scroll up. if (this.scrollTop != scrollTop) { this.skipEvent = true; this.scrollTop = this.element.scrollTop = scrollTop; @@ -249,9 +249,9 @@ oop.inherits(HScrollBar, ScrollBar); * Sets the scroll left of the scroll bar. * @param {Number} scrollTop The new scroll left **/ - // on chrome 17+ for small zoom levels after calling this function - // this.element.scrollTop != scrollTop which makes page to scroll up. this.setScrollLeft = function(scrollLeft) { + // on chrome 17+ for small zoom levels after calling this function + // this.element.scrollTop != scrollTop which makes page to scroll up. if (this.scrollLeft != scrollLeft) { this.skipEvent = true; this.scrollLeft = this.element.scrollLeft = scrollLeft;