diff --git a/lib/ace/anchor.js b/lib/ace/anchor.js index 3a62e632..c9c27dfe 100644 --- a/lib/ace/anchor.js +++ b/lib/ace/anchor.js @@ -101,66 +101,49 @@ var Anchor = exports.Anchor = function(doc, row, column) { **/ this.onChange = function(e) { var delta = e.data; - var range = delta.range; - - if (range.start.row == range.end.row && range.start.row != this.row) - return; - - if (range.start.row > this.row) - return; - - if (range.start.row == this.row && range.start.column > this.column) - return; - - var row = this.row; - var column = this.column; - var start = range.start; - var end = range.end; - - if (delta.action === "insertText") { - if (start.row === row && start.column <= column) { - if (start.column === column && this.$insertRight) { - // do nothing - } else if (start.row === end.row) { - column += end.column - start.column; - } else { - column -= start.column; - row += end.row - start.row; - } - } else if (start.row !== end.row && start.row < row) { - row += end.row - start.row; - } - } else if (delta.action === "insertLines") { - if (start.row <= row) { - row += end.row - start.row; - } - } else if (delta.action === "removeText") { - if (start.row === row && start.column < column) { - if (end.column >= column) - column = start.column; - else - column = Math.max(0, column - (end.column - start.column)); - - } else if (start.row !== end.row && start.row < row) { - if (end.row === row) - column = Math.max(0, column - end.column) + start.column; - row -= (end.row - start.row); - } else if (end.row === row) { - row -= end.row - start.row; - column = Math.max(0, column - end.column) + start.column; - } - } else if (delta.action == "removeLines") { - if (start.row <= row) { - if (end.row <= row) - row -= end.row - start.row; - else { - row = start.row; - column = 0; - } - } + var point = this.$getTransformedPoint(delta, {row: this.row, column: this.column}); + this.setPosition(point.row, point.column, true); + }; + + function $pointsInOrder(point1, point2, equalPointsInOrder) { + var bColIsAfter = equalPointsInOrder ? point1.column <= point2.column : point1.column < point2.column; + return (point1.row < point2.row) || (point1.row == point2.row && bColIsAfter); + } + + this.$getTransformedPoint = function (delta, point) { + + // Get delta info. + var moveIfEqual = this.$insertRight; + var deltaIsInsert = (delta.action == 'insert') + var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.range.end.row - delta.range.start.row); + var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.range.end.column - delta.range.start.column); + var deltaStart = delta.range.start; + var deltaEnd = (deltaIsInsert ? deltaStart : delta.range.end); // Collapse insert range. + + // DELTA AFTER POINT: No change needed. + if ($pointsInOrder(point, deltaStart, moveIfEqual)) { + return { + row: point.row, + column: point.column + }; } - - this.setPosition(row, column, true); + + // DELTA BEFORE POINT: Move point by delta shift. + if ($pointsInOrder(deltaEnd, point, !moveIfEqual)) { + return { + row: point.row + deltaRowShift, + column: point.column + (point.row == deltaEnd.row ? deltaColShift : 0) + }; + } + + // DELTA ENVELOPS POINT (delete only): Move point to delta start. + if (delta.action != 'remove') + throw 'Delete action expected.'; + + return { + row: deltaStart.row, + column: deltaStart.column + }; }; /** diff --git a/lib/ace/anchor_test.js b/lib/ace/anchor_test.js index 2d7fcb63..d40d459b 100644 --- a/lib/ace/anchor_test.js +++ b/lib/ace/anchor_test.js @@ -62,7 +62,7 @@ module.exports = { var doc = new Document("juhu\nkinners"); var anchor = new Anchor(doc, 1, 4); - doc.insertLines(1, ["123", "456"]); + doc.insertFullLines(1, ["123", "456"]); assert.position(anchor.getPosition(), 3, 4); }, @@ -70,7 +70,7 @@ module.exports = { var doc = new Document("juhu\nkinners"); var anchor = new Anchor(doc, 1, 4); - doc.insertNewLine({row: 0, column: 0}); + doc.insertMergedLines({row: 0, column: 0}, ['', '']); assert.position(anchor.getPosition(), 2, 4); }, @@ -78,7 +78,7 @@ module.exports = { var doc = new Document("juhu\nkinners"); var anchor = new Anchor(doc, 1, 4); - doc.insertNewLine({row: 1, column: 2}); + doc.insertMergedLines({row: 1, column: 2}, ['', '']); assert.position(anchor.getPosition(), 2, 2); }, @@ -110,7 +110,7 @@ module.exports = { var doc = new Document("juhu\n1\n2\nkinners"); var anchor = new Anchor(doc, 3, 4); - doc.removeLines(1, 2); + doc.removeFullLines(1, 2); assert.position(anchor.getPosition(), 1, 4); }, @@ -134,7 +134,7 @@ module.exports = { var doc = new Document("juhu\nkinners\n123"); var anchor = new Anchor(doc, 1, 5); - doc.removeLines(1, 1); + doc.removeFullLines(1, 1); assert.position(anchor.getPosition(), 1, 0); }, @@ -173,9 +173,9 @@ module.exports = { var doc = new Document("juhu\nkinners\n123"); var anchor = new Anchor(doc, 2, 4); - doc.removeLines(0, 3); + doc.removeFullLines(0, 3); assert.position(anchor.getPosition(), 0, 0); - doc.insertLines(0, ["a", "b", "c"]); + doc.insertFullLines(0, ["a", "b", "c"]); assert.position(anchor.getPosition(), 3, 0); assert.equal(doc.getValue(), "a\nb\nc\n"); } diff --git a/lib/ace/apply_delta.js b/lib/ace/apply_delta.js new file mode 100644 index 00000000..86bc89c6 --- /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 (docLines, position) { + var text = docLines[position.row]; + docLines[position.row] = text.slice(0, position.column); + docLines.splice(position.row + 1, 0, text.slice(position.column)); +} + +function joinLineWithNext(docLines, row) { + docLines[row] += docLines[row + 1]; + docLines.splice(row + 1, 1); +} + +function throwDeltaError(delta, errorText){ + console.log('Invalid Delta:', delta); + throw 'Invalid Delta: ' + errorText; +} + +function positionInDocument(docLines, position) { + return position.row >= 0 && position.row < docLines.length && + position.column >= 0 && position.column <= docLines[position.row].length; +} + +function validateDelta(docLines, delta) { + + // Validate action string. + if (delta.action != 'insert' && delta.action != 'remove') + throwDeltaError(delta, 'delta.action must be "insert" or "remove"'); + + // Validate lines type. + if (!(delta.lines instanceof Array)) + throwDeltaError(delta, 'delta.lines must be an Array'); + + // Validate range type. + if (!(delta.range instanceof Range)) + throwDeltaError(delta, 'delta.range must be an instance of the Range class'); + + // Validate that the start point is contained in the document. + var start = delta.range.start; + if (!positionInDocument(docLines, delta.range.start)) + throwDeltaError(delta, 'delta.range.start must be contained in document'); + + // Validate that the end point is contained in the document (remove deltas only). + var end = delta.range.end; + if (delta.action == 'remove' && !positionInDocument(docLines, end)) + throwDeltaError(delta, 'delta.range.end must contained in document for "remove" actions'); + + // Validate that the .range size matches the .lines size. + var numRangeRows = end.row - start.row; + var numRangeLastLineChars = (end.column - (numRangeRows == 0 ? start.column : 0)); + if (numRangeRows != delta.lines.length - 1 || delta.lines[numRangeRows].length != numRangeLastLineChars) + throwDeltaError(delta, 'delta.range must match delta lines'); +} + +exports.applyDelta = function(docLines, delta, doNotValidate) { + + // Validate delta. + if (!doNotValidate) + validateDelta(docLines, 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 = docLines[row]; + switch (delta.action) { + + case 'insert': + docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn); + break; + + case 'remove': + docLines[row] = line.substring(0, startColumn) + line.substring(endColumn); + break; + } + } else { + + // Apply multi-line delta. + switch (delta.action) { + + case 'insert': + splitLine(docLines, delta.range.start); + docLines.splice.apply(docLines, [delta.range.start.row + 1, 0].concat(delta.lines)); + joinLineWithNext(docLines, delta.range.start.row); + joinLineWithNext(docLines, delta.range.end.row); + break; + + case 'remove': + splitLine(docLines, delta.range.end); + splitLine(docLines, delta.range.start); + docLines.splice( + delta.range.start.row + 1, // Where to start deleting + delta.range.end.row - delta.range.start.row + 1 // Num lines to delete. + ); + joinLineWithNext(docLines, delta.range.start.row); + break; + } + } +} +}); diff --git a/lib/ace/background_tokenizer.js b/lib/ace/background_tokenizer.js index 217be1b3..4d3d7034 100644 --- a/lib/ace/background_tokenizer.js +++ b/lib/ace/background_tokenizer.js @@ -183,7 +183,7 @@ var BackgroundTokenizer = function(tokenizer, editor) { if (len === 0) { this.lines[startRow] = null; - } else if (delta.action == "removeText" || delta.action == "removeLines") { + } else if (delta.action == 'remove') { this.lines.splice(startRow, len + 1, null); this.states.splice(startRow, len + 1, null); } else { diff --git a/lib/ace/document.js b/lib/ace/document.js index 75a7920d..97d69968 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; @@ -51,17 +52,17 @@ var Anchor = require("./anchor").Anchor; * @constructor **/ -var Document = function(text) { - this.$lines = []; +var Document = function(textOrLines) { + this.$lines = ['']; // 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 (text.length == 0) { + if (textOrLines.length === 0) { this.$lines = [""]; - } else if (Array.isArray(text)) { - this._insertLines(0, text); + } else if (Array.isArray(textOrLines)) { + this.insertMergedLines({row: 0, column: 0}, textOrLines); } else { - this.insert({row: 0, column:0}, text); + this.insert({row: 0, column:0}, textOrLines); } }; @@ -107,14 +108,15 @@ var Document = function(text) { **/ // 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 + }; + } else { this.$split = function(text) { return text.split(/\r\n|\r|\n/); }; + } this.$detectNewLine = function(text) { @@ -205,33 +207,77 @@ var Document = function(text) { }; /** - * [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()); + }; + + /** + * 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.join(this.getNewLineCharacter()); + return lines; }; this.$clipPosition = function(position) { var length = this.getLength(); if (position.row >= length) { position.row = Math.max(0, length - 1); - position.column = this.getLine(length-1).length; - } else if (position.row < 0) - position.row = 0; + position.column = this.getLine(length - 1).length; + } else { + position.row = Math.max(0, position.row); + position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length); + } return position; }; + + this.$getClippedRange = function(range) { + + // Get Range object. + if (!range instanceof Range) + range = Range.fromPoints(range.start, range.end); + + // Return clipped range. + this.$clipPosition(range.start); + this.$clipPosition(range.end); + return range; + }; + + // Deprecated methods retained for backwards compatibility. + 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, ['', '']); + }; /** * Inserts a block of `text` at the indicated `position`. @@ -241,53 +287,66 @@ var Document = function(text) { * **/ this.insert = function(position, text) { - if (!text || text.length === 0) - return position; - - position = this.$clipPosition(position); - - // only detect new lines if the document has no line break yet + + // Only detect new lines if the document has no line break yet. if (this.getLength() <= 1) this.$detectNewLine(text); - - var lines = this.$split(text); - var firstLine = lines.splice(0, 1)[0]; - var lastLine = lines.length == 0 ? null : lines.splice(lines.length - 1, 1)[0]; - - position = this.insertInLine(position, firstLine); - if (lastLine !== null) { - position = this.insertNewLine(position); // terminate first line - position = this._insertLines(position.row, lines); - position = this.insertInLine(position, lastLine || ""); - } - return position; + + return this.insertMergedLines(position, this.$split(text)); }; - + /** - * Fires whenever the document changes. - * - * Several methods trigger different `"change"` events. Below is a list of each action type, followed by each property that's also available: - * - * * `"insertLines"` (emitted by [[Document.insertLines]]) - * * `range`: the [[Range]] of the change within the document - * * `lines`: the lines in the document that are changing - * * `"insertText"` (emitted by [[Document.insertNewLine]]) - * * `range`: the [[Range]] of the change within the document - * * `text`: the text that's being added - * * `"removeLines"` (emitted by [[Document.insertLines]]) - * * `range`: the [[Range]] of the change within the document - * * `lines`: the lines in the document that were removed - * * `nl`: the new line character (as defined by [[Document.getNewLineCharacter]]) - * * `"removeText"` (emitted by [[Document.removeInLine]] and [[Document.removeNewLine]]) - * * `range`: the [[Range]] of the change within the document - * * `text`: the text that's being removed - * - * @event change - * @param {Object} e Contains at least one property called `"action"`. `"action"` indicates the action that triggered the change. Each action also has a set of additional properties. - * - **/ + * Inserts `text` into the `position` at the current row. This method also triggers the `'change'` event. + * + * This differs from the `insert` method in two ways: + * 1. This does NOT handle newline characters (single-line text only). + * 2. This is faster than the `insert` method for single-line text insertions. + * + * @param {Object} position The position to insert at; it's an object that looks like `{ row: row, column: column}` + * @param {String} text A chunk of text + * @returns {Object} Returns an object containing the final row and column, like this: + * ``` + * {row: endRow, column: 0} + * ``` + **/ + this.insertInLine = function(position, text) { + + // Calculate insertion range end point. + this.$clipPosition(position); + var endPoint = { + row : position.row, + column : position.column + text.length + }; + + // Apply delta (emits change). + this.applyDelta({ + action: "insert", + range: Range.fromPoints(position, endPoint), + lines: [text] + }, true /*doNotValidate*/); + + return endPoint; + }; + /** - * Inserts the elements in `lines` into the document, starting at the row index given by `row`. This method also triggers the `'change'` event. + * Fires whenever the document changes. + * + * Several methods trigger different `"change"` events. Below is a list of each action type, followed by each property that's also available: + * + * * `"insert"` + * * `range`: the [[Range]] of the change within the document + * * `lines`: the lines being added + * * `"remove"` + * * `range`: the [[Range]] of the change within the document + * * `lines`: the lines being removed + * + * @event change + * @param {Object} e Contains at least one property called `"action"`. `"action"` indicates the action that triggered the change. Each action also has a set of additional properties. + * + **/ + + /** + * Inserts the elements in `lines` into the document as full lines (does not merge with existing line), starting at the row index given by `row`. This method also triggers the `'change'` event. * @param {Number} row The index of the row to insert at * @param {Array} lines An array of strings * @returns {Object} Contains the final row and column, like this: @@ -300,99 +359,59 @@ var Document = function(text) { * ``` * **/ - this.insertLines = function(row, lines) { - if (row >= this.getLength()) - return this.insert({row: row, column: 0}, "\n" + lines.join("\n")); - return this._insertLines(Math.max(row, 0), lines); - }; - this._insertLines = function(row, lines) { - if (lines.length == 0) - return {row: row, column: 0}; - - // apply doesn't work for big arrays (smallest threshold is on safari 0xFFFF) - // to circumvent that we have to break huge inserts into smaller chunks here - if (lines.length > 0xFFFF) { - var end = this._insertLines(row, lines.slice(0xFFFF)); - lines = lines.slice(0, 0xFFFF); + this.insertFullLines = function(row, lines) { + // Clip to document. + // Allow one past the document end. + row = Math.min(Math.max(row, 0), this.getLength()); + + // Calculate insertion point. + var column = 0; + if (row < this.getLength()) { + // Insert before the specified row. + lines = lines.concat(['']); + column = 0; + } else { + // Insert after the last row in the document. + lines = [''].concat(lines); + row--; + column = this.$lines[row].length; } + + // Insert. + this.insertMergedLines({row: row, column: column}, lines); + }; - var args = [row, 0]; - args.push.apply(args, lines); - this.$lines.splice.apply(this.$lines, args); + /** + * Inserts the elements in `lines` into the document, starting at the position index given by `row`. This method also triggers the `'change'` event. + * @param {Number} row The index of the row to insert at + * @param {Array} lines An array of strings + * @returns {Object} Contains the final row and column, like this: + * ``` + * {row: endRow, column: 0} + * ``` + * If `lines` is empty, this function returns an object containing the current row, and column, like this: + * ``` + * {row: row, column: 0} + * ``` + * + **/ + this.insertMergedLines = function(position, lines) { - var range = new Range(row, 0, row + lines.length, 0); - var delta = { - action: "insertLines", - range: range, + // Calculate insertion range end point. + this.$clipPosition(position); + var endPoint = { + row : position.row + lines.length - 1, + column : (lines.length == 1 ? position.column : 0) + lines[lines.length - 1].length + }; + + // Apply delta (emits change). + this.applyDelta({ + action: "insert", + range: Range.fromPoints(position, endPoint), lines: lines - }; - this._emit("change", { data: delta }); - return end || range.end; - }; - - /** - * Inserts a new line into the document at the current row's `position`. This method also triggers the `'change'` event. - * @param {Object} position The position to insert at - * @returns {Object} Returns an object containing the final row and column, like this:
- * ``` - * {row: endRow, column: 0} - * ``` - * - **/ - this.insertNewLine = function(position) { - position = this.$clipPosition(position); - var line = this.$lines[position.row] || ""; - - this.$lines[position.row] = line.substring(0, position.column); - this.$lines.splice(position.row + 1, 0, line.substring(position.column, line.length)); - - var end = { - row : position.row + 1, - column : 0 - }; - - var delta = { - action: "insertText", - range: Range.fromPoints(position, end), - text: this.getNewLineCharacter() - }; - this._emit("change", { data: delta }); - - return end; - }; - - /** - * Inserts `text` into the `position` at the current row. This method also triggers the `'change'` event. - * @param {Object} position The position to insert at; it's an object that looks like `{ row: row, column: column}` - * @param {String} text A chunk of text - * @returns {Object} Returns an object containing the final row and column, like this: - * ``` - * {row: endRow, column: 0} - * ``` - * - **/ - this.insertInLine = function(position, text) { - if (text.length == 0) - return position; - - var line = this.$lines[position.row] || ""; - - this.$lines[position.row] = line.substring(0, position.column) + text - + line.substring(position.column); - - var end = { - row : position.row, - column : position.column + text.length - }; - - var delta = { - action: "insertText", - range: Range.fromPoints(position, end), - text: text - }; - this._emit("change", { data: delta }); - - return end; + }); + + return endPoint; }; /** @@ -402,37 +421,15 @@ var Document = function(text) { * **/ this.remove = function(range) { - if (!range instanceof Range) - range = Range.fromPoints(range.start, range.end); - // clip to document - range.start = this.$clipPosition(range.start); - range.end = this.$clipPosition(range.end); - - if (range.isEmpty()) - return range.start; - - var firstRow = range.start.row; - var lastRow = range.end.row; - - if (range.isMultiLine()) { - var firstFullRow = range.start.column == 0 ? firstRow : firstRow + 1; - var lastFullRow = lastRow - 1; - - if (range.end.column > 0) - this.removeInLine(lastRow, 0, range.end.column); - - if (lastFullRow >= firstFullRow) - this._removeLines(firstFullRow, lastFullRow); - - if (firstFullRow != firstRow) { - this.removeInLine(firstRow, range.start.column, this.getLine(firstRow).length); - this.removeNewLine(range.start.row); - } - } - else { - this.removeInLine(firstRow, range.start.column, range.end.column); - } - return range.start; + + // Apply delta (emits change). + range = this.$getClippedRange(range); + this.applyDelta({ + action: 'remove', + range: range, + lines: this.getLinesForRange(range), + }); + return range.start; }; /** @@ -444,21 +441,18 @@ var Document = function(text) { * **/ this.removeInLine = function(row, startColumn, endColumn) { - if (startColumn == endColumn) - return; - + + // Calculate deleteion range. var range = new Range(row, startColumn, row, endColumn); - var line = this.getLine(row); - var removed = line.substring(startColumn, endColumn); - var newLine = line.substring(0, startColumn) + line.substring(endColumn, line.length); - this.$lines.splice(row, 1, newLine); - - var delta = { - action: "removeText", + range = this.$getClippedRange(range); + + // Apply delta (emits change). + this.applyDelta({ + action: 'remove', range: range, - text: removed - }; - this._emit("change", { data: delta }); + lines: this.getLinesForRange(range), + }, true /*doNotValidate*/); + return range.start; }; @@ -469,24 +463,35 @@ var Document = function(text) { * @returns {[String]} Returns all the removed lines. * **/ - this.removeLines = function(firstRow, lastRow) { - if (firstRow < 0 || lastRow >= this.getLength()) - return this.remove(new Range(firstRow, 0, lastRow + 1, 0)); - return this._removeLines(firstRow, lastRow); - }; - - this._removeLines = function(firstRow, lastRow) { - var range = new Range(firstRow, 0, lastRow + 1, 0); - var removed = this.$lines.splice(firstRow, lastRow - firstRow + 1); - - var delta = { - action: "removeLines", + this.removeFullLines = function(firstRow, lastRow) { + + // Clip to document. + firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1); + lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1); + + // Calculate deletion range. + // Delete the ending new line unless we're at the end of the document. + // If we're at the end of the document, delete the starting new line. + var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0; + var deleteLastNewLine = lastRow < this.getLength() - 1; + var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow ); + var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 ); + var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow ); + var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length ); + var range = new Range(startRow, startCol, endRow, endCol); + + // Store delelted lines with bounding newlines ommitted (maintains previous behavior). + var deletedLines = this.$lines.slice(firstRow, lastRow + 1); + + // Apply delta (emits change). + this.applyDelta({ + action: 'remove', range: range, - nl: this.getNewLineCharacter(), - lines: removed - }; - this._emit("change", { data: delta }); - return removed; + lines: this.getLinesForRange(range) + }); + + // Return the deleted lines. + return deletedLines; }; /** @@ -495,20 +500,15 @@ var Document = function(text) { * **/ this.removeNewLine = function(row) { - var firstLine = this.getLine(row); - var secondLine = this.getLine(row+1); - - var range = new Range(row, firstLine.length, row+1, 0); - var line = firstLine + secondLine; - - this.$lines.splice(row, 2, line); - - var delta = { - action: "removeText", - range: range, - text: this.getNewLineCharacter() - }; - this._emit("change", { data: delta }); + + if (row < this.getLength() - 1 && row >= 0) { + // Apply delta (emits change). + this.applyDelta({ + action: 'remove', + range: new Range(row, this.getLine(row).length, row + 1, 0), + lines: ['', ''] + }); + } }; /** @@ -524,7 +524,7 @@ var Document = function(text) { 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,55 +533,90 @@ var Document = function(text) { return range.end; this.remove(range); + var end; if (text) { - var end = this.insert(range.start, text); + end = this.insert(range.start, text); } else { end = range.start; } - + return end; }; /** - * Applies all the changes previously accumulated. These can be either `'includeText'`, `'insertLines'`, `'removeText'`, and `'removeLines'`. + * Applies all changes in `deltas` to the document. + * @param {Array} deltas An array of delta objects (can include 'insert' and 'remove' actions) **/ this.applyDeltas = function(deltas) { for (var i=0; i=0; i--) { - var delta = deltas[i]; - - var range = Range.fromPoints(delta.range.start, delta.range.end); - - if (delta.action == "insertLines") - this._removeLines(range.start.row, range.end.row - 1); - else if (delta.action == "insertText") - this.remove(range); - else if (delta.action == "removeLines") - this._insertLines(range.start.row, delta.lines); - else if (delta.action == "removeText") - this.insert(range.start, delta.text); + this.revertDelta(deltas[i]); } }; + + /** + * Applies `delta` to the document. + * @param {Object} delta A delta object (can include 'insert' and 'remove' actions) + **/ + this.applyDelta = function(delta, doNotValidate) { + // An empty range is a NOOP. + if (delta.range.isEmpty()) + return; + + // Split large insert deltas. This is necessary because: + // 1. We need to support splicing delta lines into the document via $lines.splice.apply(...) + // 2. fn.apply() doesn't work for a large number of params. The mallest threshold is on safari 0xFFFF. + // + // To Do: Ideally we'd be consistent and also split 'delete' deltas. We don't do this now, because delete + // delta handling is too slow. If we make delete delta handling faster we can split all large deltas + // as shown in https://gist.github.com/aldendaniels/8367109#file-document-snippet-js + // If we do this, update validateDelta() to limit the number of lines in a delete delta. + var bIsInsert = delta.action == 'insert'; + while (bIsInsert && delta.lines.length > 65001) { + + // Get split deltas. + var lines = delta.lines.splice(0, 65000); + lines.push(''); + this.applyDelta({ + action: delta.action, + lines: lines, + range: new Range(delta.range.start.row, delta.range.start.column, + delta.range.start.row + 65000, 0) + }); + + // Update remaining delta. + delta.range.start.row += 65000; + delta.range.start.column = 0; + } + + // Apply. + applyDelta(this.$lines, delta, doNotValidate); + this._emit("change", { data: delta }); + }; + + /** + * Reverts `delta` from the document. + * @param {Object} delta A delta object (can include 'insert' and 'remove' actions) + **/ + this.revertDelta = function(delta) { + this.applyDelta({ + action: (delta.action == 'insert' ? 'remove' : 'insert'), + 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/document_test.js b/lib/ace/document_test.js index 5c324db0..a91fccf2 100644 --- a/lib/ace/document_test.js +++ b/lib/ace/document_test.js @@ -65,7 +65,7 @@ module.exports = { var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - doc.insertNewLine({row: 0, column: 1}); + doc.insertMergedLines({row: 0, column: 1}, ['', '']); assert.equal(doc.getValue(), ["1", "2", "34"].join("\n")); var d = deltas.concat(); @@ -82,7 +82,7 @@ module.exports = { var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - doc.insertLines(0, ["aa", "bb"]); + doc.insertFullLines(0, ["aa", "bb"]); assert.equal(doc.getValue(), ["aa", "bb", "12", "34"].join("\n")); var d = deltas.concat(); @@ -99,7 +99,7 @@ module.exports = { var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - doc.insertLines(2, ["aa", "bb"]); + doc.insertFullLines(2, ["aa", "bb"]); assert.equal(doc.getValue(), ["12", "34", "aa", "bb"].join("\n")); }, @@ -109,7 +109,7 @@ module.exports = { var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - doc.insertLines(1, ["aa", "bb"]); + doc.insertFullLines(1, ["aa", "bb"]); assert.equal(doc.getValue(), ["12", "aa", "bb", "34"].join("\n")); var d = deltas.concat(); @@ -143,7 +143,7 @@ module.exports = { var deltas = []; doc.on("change", function(e) { deltas.push(e.data); }); - doc.insert({row: 2, column: 0}, "aa\nbb\ncc"); + doc.insert({row: 1, column: 2}, "aa\nbb\ncc"); assert.equal(doc.getValue(), ["12", "34aa", "bb", "cc"].join("\n")); var d = deltas.concat(); @@ -235,7 +235,7 @@ module.exports = { "test: remove lines should return the removed lines" : function() { var doc = new Document(["1234", "5678", "abcd"]); - var removed = doc.removeLines(1, 2); + var removed = doc.removeFullLines(1, 2); assert.equal(removed.join("\n"), ["5678", "abcd"].join("\n")); }, diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index a63ceeef..414b68c1 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -1151,6 +1151,19 @@ var EditSession = function(text, mode) { this.remove = function(range) { return this.doc.remove(range); }; + + /** + * Removes a range of full lines. This method also triggers the `'change'` event. + * @param {Number} firstRow The first row to be removed + * @param {Number} lastRow The last row to be removed + * @returns {[String]} Returns all the removed lines. + * + * @related Document.removeFullLines + * + **/ + this.removeFullLines = function(firstRow, lastRow){ + return this.doc.removeFullLines(firstRow, lastRow); + }; /** * Reverts previous changes to your document. @@ -1227,9 +1240,7 @@ var EditSession = function(text, mode) { this.$getUndoSelection = function(deltas, isUndo, lastUndoRange) { function isInsert(delta) { - var insert = - delta.action === "insertText" || delta.action === "insertLines"; - return isUndo ? !insert : insert; + return isUndo ? delta.action !== "insert" : delta.action === "insert"; } var delta = deltas[0]; @@ -1373,7 +1384,7 @@ var EditSession = function(text, mode) { this.indentRows = function(startRow, endRow, indentString) { indentString = indentString.replace(/\t/g, this.getTabString()); for (var row=startRow; row<=endRow; row++) - this.insert({row: row, column:0}, indentString); + this.doc.insertInLine({row: row, column: 0}, indentString); }; /** @@ -1384,25 +1395,25 @@ var EditSession = function(text, mode) { **/ this.outdentRows = function (range) { var rowRange = range.collapseRows(); - var deleteRange = new Range(0, 0, 0, 0); var size = this.getTabSize(); - + for (var i = rowRange.start.row; i <= rowRange.end.row; ++i) { var line = this.getLine(i); - - deleteRange.start.row = i; - deleteRange.end.row = i; + var row = i; + var startCol = 0; + var endCol = 0; + for (var j = 0; j < size; ++j) if (line.charAt(j) != ' ') break; if (j < size && line.charAt(j) == '\t') { - deleteRange.start.column = j; - deleteRange.end.column = j + 1; + startCol = j; + endCol = j + 1; } else { - deleteRange.start.column = 0; - deleteRange.end.column = j; + startCol = 0; + endCol = j; } - this.remove(deleteRange); + this.doc.removeInLine(row, startCol, endCol); } }; @@ -1430,11 +1441,11 @@ var EditSession = function(text, mode) { x.end.row += diff; return x; }); - + var lines = dir == 0 ? this.doc.getLines(firstRow, lastRow) - : this.doc.removeLines(firstRow, lastRow); - this.doc.insertLines(firstRow+diff, lines); + : this.doc.removeFullLines(firstRow, lastRow); + this.doc.insertFullLines(firstRow+diff, lines); folds.length && this.addFolds(folds); return diff; }; @@ -1444,8 +1455,6 @@ var EditSession = function(text, mode) { * @param {Number} lastRow The final row to move up * @returns {Number} If `firstRow` is less-than or equal to 0, this function returns 0. Otherwise, on success, it returns -1. * - * @related Document.insertLines - * **/ this.moveLinesUp = function(firstRow, lastRow) { return this.$moveLines(firstRow, lastRow, -1); @@ -1456,8 +1465,6 @@ var EditSession = function(text, mode) { * @param {Number} firstRow The starting row to move down * @param {Number} lastRow The final row to move down * @returns {Number} If `firstRow` is less-than or equal to 0, this function returns 0. Otherwise, on success, it returns -1. - * - * @related Document.insertLines **/ this.moveLinesDown = function(firstRow, lastRow) { return this.$moveLines(firstRow, lastRow, 1); @@ -1606,7 +1613,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); @@ -1668,28 +1675,17 @@ var EditSession = function(text, mode) { this.$updateInternalDataOnChange = function(e) { var useWrapMode = this.$useWrapMode; - var len; var action = e.data.action; - var firstRow = e.data.range.start.row; - var lastRow = e.data.range.end.row; var start = e.data.range.start; var end = e.data.range.end; + var firstRow = start.row; + var lastRow = end.row; + var len = lastRow - firstRow; var removedFolds = null; - - if (action.indexOf("Lines") != -1) { - if (action == "insertLines") { - lastRow = firstRow + (e.data.lines.length); - } else { - lastRow = firstRow; - } - len = e.data.lines ? e.data.lines.length : lastRow - firstRow; - } else { - len = lastRow - firstRow; - } - + this.$updating = true; if (len != 0) { - if (action.indexOf("remove") != -1) { + if (action == 'remove') { this[useWrapMode ? "$wrapData" : "$rowLengthCache"].splice(firstRow, len); var foldLines = this.$foldData; @@ -1736,7 +1732,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); @@ -1764,7 +1760,7 @@ var EditSession = function(text, mode) { // Realign folds. E.g. if you add some new chars before a fold, the // fold should "move" to the right. len = Math.abs(e.data.range.start.column - e.data.range.end.column); - if (action.indexOf("remove") != -1) { + if (action == 'remove') { // Get all the folds in the change range and remove them. removedFolds = this.getFoldsInRange(e.data.range); this.removeFolds(removedFolds); @@ -2213,7 +2209,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/edit_session/folding.js b/lib/ace/edit_session/folding.js index 474818b2..583dba0d 100644 --- a/lib/ace/edit_session/folding.js +++ b/lib/ace/edit_session/folding.js @@ -833,7 +833,7 @@ function Folding() { if (len === 0) { this.foldWidgets[firstRow] = null; - } else if (delta.action == "removeText" || delta.action == "removeLines") { + } else if (delta.action == 'remove') { this.foldWidgets.splice(firstRow, len + 1, null); } else { var args = Array(len + 1); diff --git a/lib/ace/edit_session_test.js b/lib/ace/edit_session_test.js index 87cc9567..75abf468 100644 --- a/lib/ace/edit_session_test.js +++ b/lib/ace/edit_session_test.js @@ -430,12 +430,12 @@ module.exports = { session.setTabSize(4); assert.equal(session.getScreenWidth(), 2); - session.doc.insertNewLine({row: 0, column: Infinity}); - session.doc.insertLines(1, ["123"]); + session.doc.insertMergedLines({row: 0, column: Infinity}, ['', '']); + session.doc.insertFullLines(1, ["123"]); assert.equal(session.getScreenWidth(), 3); - session.doc.insertNewLine({row: 0, column: Infinity}); - session.doc.insertLines(1, ["\t\t"]); + session.doc.insertMergedLines({row: 0, column: Infinity}, ['', '']); + session.doc.insertFullLines(1, ["\t\t"]); assert.equal(session.getScreenWidth(), 8); @@ -459,9 +459,9 @@ module.exports = { session.setUseWrapMode(true); - document.insertLines(0, ["a", "b"]); - document.insertLines(2, ["c", "d"]); - document.removeLines(1, 2); + document.insertFullLines(0, ["a", "b"]); + document.insertFullLines(2, ["c", "d"]); + document.removeFullLines(1, 2); }, "test wrapMode init has to create wrapData array": function() { diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 6486a881..af991ef1 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -601,19 +601,14 @@ var Editor = function(renderer, session) { * **/ this.onDocumentChange = function(e) { - var delta = e.data; - var range = delta.range; - var lastRow; - - if (range.start.row == range.end.row && delta.action != "insertLines" && delta.action != "removeLines") - lastRow = range.end.row; - else - lastRow = Infinity; + + // Rerender and emit "change" event. + var range = e.data.range; + var lastRow = (range.start.row == range.end.row ? range.end.row : Infinity); this.renderer.updateLines(range.start.row, lastRow); - this._emit("change", e); - - // update cursor because tab characters can influence the cursor position + + // Update cursor because tab characters can influence the cursor position. this.$cursorChange(); }; @@ -875,7 +870,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); @@ -1361,7 +1356,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); @@ -1516,15 +1511,7 @@ var Editor = function(renderer, session) { **/ this.removeLines = function() { var rows = this.$getSelectedRows(); - var range; - if (rows.first === 0 || rows.last+1 < this.session.getLength()) - range = new Range(rows.first, 0, rows.last+1, 0); - else - range = new Range( - rows.first-1, this.session.getLine(rows.first-1).length, - rows.last, this.session.getLine(rows.last).length - ); - this.session.remove(range); + this.session.removeFullLines(rows.first, rows.last); this.clearSelection(); }; diff --git a/lib/ace/ext/chromevox.js b/lib/ace/ext/chromevox.js index 9f7a7996..4abb8278 100644 --- a/lib/ace/ext/chromevox.js +++ b/lib/ace/ext/chromevox.js @@ -581,12 +581,12 @@ var onSelectionChange = function(evt) { var onChange = function(evt) { var data = evt.data; switch (data.action) { - case 'removeText': + case 'remove': cvox.Api.speak(data.text, 0, DELETED_PROP); /* Let the future cursor change event know it's from text change. */ changed = true; break; - case 'insertText': + case 'insert': cvox.Api.speak(data.text, 0); /* Let the future cursor change event know it's from text change. */ changed = true; diff --git a/lib/ace/layer/gutter.js b/lib/ace/layer/gutter.js index 5a535094..235425ab 100644 --- a/lib/ace/layer/gutter.js +++ b/lib/ace/layer/gutter.js @@ -108,7 +108,7 @@ var Gutter = function(parentEl) { var len = range.end.row - firstRow; if (len === 0) { // do nothing - } else if (delta.action == "removeText" || delta.action == "removeLines") { + } else if (delta.action == 'remove') { this.$annotations.splice(firstRow, len + 1, null); } else { var args = new Array(len + 1); diff --git a/lib/ace/line_widgets.js b/lib/ace/line_widgets.js index 597b3ca2..d0c7581e 100644 --- a/lib/ace/line_widgets.js +++ b/lib/ace/line_widgets.js @@ -123,7 +123,7 @@ function LineWidgets(session) { if (len === 0) { // return - } else if (delta.action == "removeText" || delta.action == "removeLines") { + } else if (delta.action == 'remove') { var removed = cells.splice(startRow + 1, len); removed.forEach(function(w) { w && this.removeLineWidget(w); diff --git a/lib/ace/multi_select.js b/lib/ace/multi_select.js index 7f4c5355..965cd961 100644 --- a/lib/ace/multi_select.js +++ b/lib/ace/multi_select.js @@ -740,9 +740,9 @@ var Editor = require("./editor").Editor; if (fr < 0) fr = 0; if (lr >= max) lr = max - 1; } - var lines = this.session.doc.removeLines(fr, lr); + var lines = this.session.removeFullLines(fr, lr); lines = this.$reAlignText(lines, guessRange); - this.session.doc.insert({row: fr, column: 0}, lines.join("\n") + "\n"); + this.session.insert({row: fr, column: 0}, lines.join("\n") + "\n"); if (!guessRange) { range.start.column = 0; range.end.column = lines[lines.length - 1].length; diff --git a/lib/ace/placeholder.js b/lib/ace/placeholder.js index bcb44ce2..1d345a24 100644 --- a/lib/ace/placeholder.js +++ b/lib/ace/placeholder.js @@ -155,21 +155,21 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) if(range.start.row !== this.pos.row) return; if (this.$updating) return; this.$updating = true; - var lengthDiff = delta.action === "insertText" ? range.end.column - range.start.column : range.start.column - range.end.column; + var lengthDiff = delta.action === "insert" ? range.end.column - range.start.column : range.start.column - range.end.column; if(range.start.column >= this.pos.column && range.start.column <= this.pos.column + this.length + 1) { var distanceFromStart = range.start.column - this.pos.column; this.length += lengthDiff; if(!this.session.$fromUndo) { - if(delta.action === "insertText") { + if(delta.action === 'insert') { for (var i = this.others.length - 1; i >= 0; i--) { var otherPos = this.others[i]; var newPos = {row: otherPos.row, column: otherPos.column + distanceFromStart}; if(otherPos.row === range.start.row && range.start.column < otherPos.column) newPos.column += lengthDiff; - this.doc.insert(newPos, delta.text); + this.doc.insertMergedLines(newPos, delta.lines); } - } else if(delta.action === "removeText") { + } else if(delta.action === 'remove') { for (var i = this.others.length - 1; i >= 0; i--) { var otherPos = this.others[i]; var newPos = {row: otherPos.row, column: otherPos.column + distanceFromStart}; @@ -179,7 +179,7 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) } } // Special case: insert in beginning - if(range.start.column === this.pos.column && delta.action === "insertText") { + if(range.start.column === this.pos.column && delta.action === 'insert') { setTimeout(function() { this.pos.setPosition(this.pos.row, this.pos.column - lengthDiff); for (var i = 0; i < this.others.length; i++) { @@ -191,7 +191,7 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass) } }.bind(this), 0); } - else if(range.start.column === this.pos.column && delta.action === "removeText") { + else if(range.start.column === this.pos.column && delta.action === 'remove') { setTimeout(function() { for (var i = 0; i < this.others.length; i++) { var other = this.others[i]; 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; diff --git a/lib/ace/undomanager.js b/lib/ace/undomanager.js index 304dac23..209cf3db 100644 --- a/lib/ace/undomanager.js +++ b/lib/ace/undomanager.js @@ -61,14 +61,19 @@ var UndoManager = function() { * **/ this.execute = function(options) { - var deltas = options.args[0]; + + // Normalize deltas for storage. + var deltaSets = this.$serializeDeltas(options.args[0]); + + // Add deltas to undo stack. this.$doc = options.args[1]; if (options.merge && this.hasUndo()){ - deltas = this.$undoStack.pop().concat(deltas); + deltaSets = this.$undoStack.pop().concat(deltaSets); } - this.$undoStack.push(deltas); + this.$undoStack.push(deltaSets); + + // Reset redo stack. this.$redoStack = []; - if (this.dirtyCounter < 0) { // The user has made a change after undoing past the last clean state. // We can never get back to a clean state now until markClean() is called. @@ -85,12 +90,11 @@ var UndoManager = function() { * @returns {Range} The range of the undo. **/ this.undo = function(dontSelect) { - var deltas = this.$undoStack.pop(); + var deltaSets = this.$undoStack.pop(); var undoSelectionRange = null; - if (deltas) { - undoSelectionRange = - this.$doc.undoChanges(deltas, dontSelect); - this.$redoStack.push(deltas); + if (deltaSets) { + undoSelectionRange = this.$doc.undoChanges(this.$deserializeDeltas(deltaSets), dontSelect); + this.$redoStack.push(deltaSets); this.dirtyCounter--; } @@ -104,15 +108,14 @@ var UndoManager = function() { * **/ this.redo = function(dontSelect) { - var deltas = this.$redoStack.pop(); + var deltaSets = this.$redoStack.pop(); var redoSelectionRange = null; - if (deltas) { + if (deltaSets) { redoSelectionRange = - this.$doc.redoChanges(deltas, dontSelect); - this.$undoStack.push(deltas); + this.$doc.redoChanges(this.$deserializeDeltas(deltaSets), dontSelect); + this.$undoStack.push(deltaSets); this.dirtyCounter++; } - return redoSelectionRange; }; @@ -160,7 +163,50 @@ var UndoManager = function() { this.isClean = function() { return this.dirtyCounter === 0; }; - + + // Serializes deltaSets to reduce memory usage. + this.$serializeDeltas = function(deltaSets) { + return cloneDeltaSetsObj(deltaSets, $serializeDelta); + }; + + // Deserializes deltaSets to allow application to the document. + this.$deserializeDeltas = function(deltaSets) { + return cloneDeltaSetsObj(deltaSets, $deserializeDelta); + }; + + function $serializeDelta(delta){ + return { + action: delta.action, + range: delta.range, + lines: (delta.lines.length == 1 ? null : delta.lines), + text: (delta.lines.length == 1 ? delta.lines[0] : null ), + }; + } + + function $deserializeDelta(delta) { + return { + action: delta.action, + range: delta.range, + lines: (delta.text === null ? delta.lines : [delta.text]) + }; + } + + function cloneDeltaSetsObj(deltaSets_old, fnGetModifiedDelta) { + var deltaSets_new = new Array(deltaSets_old.length); + for (var i in deltaSets_old) { + var deltaSet_old = deltaSets_old[i]; + var deltaSet_new = { group: deltaSet_old.group, deltas: new Array(deltaSet_old.length)}; + + for (var i_ in deltaSet_old.deltas) { + var delta_old = deltaSet_old.deltas[i_]; + deltaSet_new.deltas[i_] = fnGetModifiedDelta(delta_old); + } + + deltaSets_new[i] = deltaSet_new; + } + return deltaSets_new; + } + }).call(UndoManager.prototype); exports.UndoManager = UndoManager; diff --git a/lib/ace/worker/mirror.js b/lib/ace/worker/mirror.js index 7a3318fb..2f33fea6 100644 --- a/lib/ace/worker/mirror.js +++ b/lib/ace/worker/mirror.js @@ -1,6 +1,7 @@ define(function(require, exports, module) { "use strict"; +var Range = require("../range").Range; var Document = require("../document").Document; var lang = require("../lib/lang"); @@ -12,7 +13,16 @@ var Mirror = exports.Mirror = function(sender) { var _self = this; sender.on("change", function(e) { - doc.applyDeltas(e.data); + + // Convert delta.range back into a Range instance since + // window.onMessage loses non-primitive data. See http://jsfiddle.net/nqJfw/1/. + var deltas = e.data; + for (var i in deltas) { + var delta = deltas[i]; + delta.range = Range.fromPoints(delta.range.start, delta.range.end); + } + + doc.applyDeltas(deltas); if (_self.$timeout) return deferredUpdate.schedule(_self.$timeout); _self.onUpdate();