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;