diff --git a/ChangeLog.txt b/ChangeLog.txt
index e9c41e4c..3077d171 100644
--- a/ChangeLog.txt
+++ b/ChangeLog.txt
@@ -1,5 +1,16 @@
+Version 1.2.0-pre
+
+* New Features
+ - Indented soft wrap (danyaPostfactum)
+
+* API Changes
+ - unified delta types `{start, end, action, lines}` (Alden Daniels https://github.com/ajaxorg/ace/pull/1745)
+ - "change" event listeners on session and editor get delta objects directly
+
2015.04.03 Version 1.1.9
+ - Small Enhancements and Bugfixes
+
2014.11.08 Version 1.1.8
* API Changes
diff --git a/build b/build
index e3ccd2c6..a4e495d8 160000
--- a/build
+++ b/build
@@ -1 +1 @@
-Subproject commit e3ccd2c654cf45ee41ffb09d0e7fa5b40cf91a8f
+Subproject commit a4e495d8901876c6bafe3870a35cb8e32c827e97
diff --git a/lib/ace/anchor.js b/lib/ace/anchor.js
index 9f5e159d..39b78e0f 100644
--- a/lib/ace/anchor.js
+++ b/lib/ace/anchor.js
@@ -99,72 +99,54 @@ var Anchor = exports.Anchor = function(doc, row, column) {
* - `value`: An object describing the new Anchor position
*
**/
- this.onChange = function(e) {
- var delta = e.data;
- var range = delta.range;
-
- if (range.start.row == range.end.row && range.start.row != this.row)
+ this.onChange = function(delta) {
+ if (delta.start.row == delta.end.row && delta.start.row != this.row)
return;
- if (range.start.row > this.row)
+ if (delta.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 && column === 0 && this.$insertRight) {
- // do nothing
- }
- else 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;
- }
- }
- }
-
- this.setPosition(row, column, true);
+
+ var point = $getTransformedPoint(delta, {row: this.row, column: this.column}, this.$insertRight);
+ 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);
+ }
+
+ function $getTransformedPoint(delta, point, moveIfEqual) {
+ // Get delta info.
+ var deltaIsInsert = delta.action == "insert";
+ var deltaRowShift = (deltaIsInsert ? 1 : -1) * (delta.end.row - delta.start.row);
+ var deltaColShift = (deltaIsInsert ? 1 : -1) * (delta.end.column - delta.start.column);
+ var deltaStart = delta.start;
+ var deltaEnd = deltaIsInsert ? deltaStart : delta.end; // Collapse insert range.
+
+ // DELTA AFTER POINT: No change needed.
+ if ($pointsInOrder(point, deltaStart, moveIfEqual)) {
+ return {
+ row: point.row,
+ column: point.column
+ };
+ }
+
+ // 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.
+ // TODO warn if delta.action != "remove" ?
+
+ return {
+ row: deltaStart.row,
+ column: deltaStart.column
+ };
+ }
/**
* Sets the anchor position to the specified row and column. If `noClip` is `true`, the position is not clipped.
diff --git a/lib/ace/anchor_test.js b/lib/ace/anchor_test.js
index b5c62941..c0b97273 100644
--- a/lib/ace/anchor_test.js
+++ b/lib/ace/anchor_test.js
@@ -71,7 +71,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);
},
@@ -105,7 +105,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);
},
@@ -113,7 +113,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);
},
@@ -145,7 +145,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);
},
@@ -169,7 +169,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);
},
@@ -208,9 +208,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..98bfe148
--- /dev/null
+++ b/lib/ace/apply_delta.js
@@ -0,0 +1,108 @@
+/* ***** 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";
+
+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.start || !delta.end)
+ throwDeltaError(delta, "delta.start/end must be an present");
+
+ // Validate that the start point is contained in the document.
+ var start = delta.start;
+ if (!positionInDocument(docLines, delta.start))
+ throwDeltaError(delta, "delta.start must be contained in document");
+
+ // Validate that the end point is contained in the document (remove deltas only).
+ var end = delta.end;
+ if (delta.action == "remove" && !positionInDocument(docLines, end))
+ throwDeltaError(delta, "delta.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) {
+ // disabled validation since it breaks autocompletion popup
+ // if (!doNotValidate)
+ // validateDelta(docLines, delta);
+
+ var row = delta.start.row;
+ var startColumn = delta.start.column;
+ var line = docLines[row] || "";
+ switch (delta.action) {
+ case "insert":
+ var lines = delta.lines;
+ if (lines.length === 1) {
+ docLines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn);
+ } else {
+ var args = [row, 1].concat(delta.lines);
+ docLines.splice.apply(docLines, args);
+ docLines[row] = line.substring(0, startColumn) + docLines[row];
+ docLines[row + delta.lines.length - 1] += line.substring(startColumn);
+ }
+ break;
+ case "remove":
+ var endColumn = delta.end.column;
+ var endRow = delta.end.row;
+ if (row === endRow) {
+ docLines[row] = line.substring(0, startColumn) + line.substring(endColumn);
+ } else {
+ docLines.splice(
+ row, endRow - row + 1,
+ line.substring(0, startColumn) + docLines[endRow].substring(endColumn)
+ );
+ }
+ break;
+ }
+}
+});
diff --git a/lib/ace/autocomplete/popup.js b/lib/ace/autocomplete/popup.js
index ead0c47c..6c480b38 100644
--- a/lib/ace/autocomplete/popup.js
+++ b/lib/ace/autocomplete/popup.js
@@ -220,8 +220,8 @@ var AcePopup = function(parentNode) {
popup.data = [];
popup.setData = function(list) {
- popup.data = list || [];
popup.setValue(lang.stringRepeat("\n", list.length), -1);
+ popup.data = list || [];
popup.setRow(0);
};
popup.getData = function(row) {
diff --git a/lib/ace/background_tokenizer.js b/lib/ace/background_tokenizer.js
index 08825d99..ea4ddd0f 100644
--- a/lib/ace/background_tokenizer.js
+++ b/lib/ace/background_tokenizer.js
@@ -171,13 +171,12 @@ var BackgroundTokenizer = function(tokenizer, editor) {
}
this.$updateOnChange = function(delta) {
- var range = delta.range;
- var startRow = range.start.row;
- var len = range.end.row - startRow;
+ var startRow = delta.start.row;
+ var len = delta.end.row - startRow;
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 2e6e5db3..7b3e6221 100644
--- a/lib/ace/document.js
+++ b/lib/ace/document.js
@@ -32,36 +32,36 @@ 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;
/**
* Contains the text of the document. Document can be attached to several [[EditSession `EditSession`]]s.
- *
* At its core, `Document`s are just an array of strings, with each row in the document matching up to the array index.
*
* @class Document
**/
- /**
+/**
*
* Creates a new `Document`. If `text` is included, the `Document` contains those strings; otherwise, it's empty.
* @param {String | Array} text The starting text
* @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);
}
};
@@ -75,9 +75,9 @@ var Document = function(text) {
* @param {String} text The text to use
**/
this.setValue = function(text) {
- var len = this.getLength();
- this.remove(new Range(0, 0, len, this.getLine(len-1).length));
- this.insert({row: 0, column:0}, text);
+ var len = this.getLength() - 1;
+ this.remove(new Range(0, 0, len, this.getLine(len).length));
+ this.insert({row: 0, column: 0}, text);
};
/**
@@ -98,7 +98,7 @@ var Document = function(text) {
};
/**
- * Splits a string of text on any newline (`\n`) or carriage-return ('\r') characters.
+ * Splits a string of text on any newline (`\n`) or carriage-return (`\r`) characters.
*
* @method $split
* @param {String} text The text to work with
@@ -107,14 +107,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) {
@@ -207,32 +208,49 @@ 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) {
- if (range.start.row == range.end.row) {
- return this.getLine(range.start.row)
- .substring(range.start.column, range.end.column);
+ 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) {
+ // 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;
- return position;
+ // 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, ["", ""]);
};
/**
@@ -243,53 +261,97 @@ 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 this.insertMergedLines(position, this.$split(text));
+ };
+
+ /**
+ * 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) {
+ var start = this.clippedPos(position.row, position.column);
+ var end = this.pos(position.row, position.column + text.length);
+
+ this.applyDelta({
+ start: start,
+ end: end,
+ action: "insert",
+ lines: [text]
+ }, true);
+
+ return this.clonePos(end);
+ };
+
+ this.clippedPos = function(row, column) {
+ var length = this.getLength();
+ if (row === undefined) {
+ row = length;
+ } else if (row < 0) {
+ row = 0;
+ } else if (row >= length) {
+ row = length - 1;
+ column = undefined;
+ }
+ var line = this.getLine(row);
+ if (column == undefined)
+ column = line.length;
+ column = Math.min(Math.max(column, 0), line.length);
+ return {row: row, column: column};
+ };
+
+ this.clonePos = function(pos) {
+ return {row: pos.row, column: pos.column};
+ };
+
+ this.pos = function(row, column) {
+ return {row: row, column: column};
+ };
+
+ 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 {
+ position.row = Math.max(0, position.row);
+ position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length);
}
return position;
};
/**
- * 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.
- *
- **/
+ * 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, starting at the row index given by `row`. This method also triggers the `'change'` event.
+ * 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:
@@ -302,101 +364,57 @@ 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 due to max call stack detection.
- // Chrome 64 bit requires threshold smaller than 32k, using a safe value of 20k.
- // To circumvent that we have to break huge inserts into smaller chunks here.
- while (lines.length > 20000) {
- var end = this._insertLines(row, lines.slice(0, 20000));
- lines = lines.slice(20000);
- row = end.row;
+ 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);
-
- var range = new Range(row, 0, row + lines.length, 0);
- var delta = {
- action: "insertLines",
- range: range,
+ /**
+ * 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 start = this.clippedPos(position.row, position.column);
+ var end = {
+ row: start.row + lines.length - 1,
+ column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length
+ };
+
+ this.applyDelta({
+ start: start,
+ end: end,
+ action: "insert",
lines: lines
- };
- this._signal("change", { data: delta });
- return 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._signal("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._signal("change", { data: delta });
-
- return end;
+ });
+
+ return this.clonePos(end);
};
/**
@@ -406,113 +424,90 @@ 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;
+ var start = this.clippedPos(range.start.row, range.start.column);
+ var end = this.clippedPos(range.end.row, range.end.column);
+ this.applyDelta({
+ start: start,
+ end: end,
+ action: "remove",
+ lines: this.getLinesForRange({start: start, end: end})
+ });
+ return this.clonePos(start);
};
/**
- * Removes the specified columns from the `row`. This method also triggers the `'change'` event.
- * @param {Number} row The row to remove from
- * @param {Number} startColumn The column to start removing at
- * @param {Number} endColumn The column to stop removing at
- * @returns {Object} Returns an object containing `startRow` and `startColumn`, indicating the new row and column values.
If `startColumn` is equal to `endColumn`, this function returns nothing.
- *
- **/
+ * Removes the specified columns from the `row`. This method also triggers a `"change"` event.
+ * @param {Number} row The row to remove from
+ * @param {Number} startColumn The column to start removing at
+ * @param {Number} endColumn The column to stop removing at
+ * @returns {Object} Returns an object containing `startRow` and `startColumn`, indicating the new row and column values.
If `startColumn` is equal to `endColumn`, this function returns nothing.
+ *
+ **/
this.removeInLine = function(row, startColumn, endColumn) {
- if (startColumn == endColumn)
- return;
-
- 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: range,
- text: removed
- };
- this._signal("change", { data: delta });
- return range.start;
+ var start = this.clippedPos(row, startColumn);
+ var end = this.clippedPos(row, endColumn);
+
+ this.applyDelta({
+ start: start,
+ end: end,
+ action: "remove",
+ lines: this.getLinesForRange({start: start, end: end})
+ }, true);
+
+ return this.clonePos(start);
};
/**
- * Removes a range of full lines. This method also triggers the `'change'` event.
+ * 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.
*
**/
- 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",
- range: range,
- nl: this.getNewLineCharacter(),
- lines: removed
- };
- this._signal("change", { data: delta });
- return removed;
+ 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);
+
+ this.applyDelta({
+ start: range.start,
+ end: range.end,
+ action: "remove",
+ lines: this.getLinesForRange(range)
+ });
+
+ // Return the deleted lines.
+ return deletedLines;
};
/**
- * Removes the new line between `row` and the row immediately following it. This method also triggers the `'change'` event.
+ * Removes the new line between `row` and the row immediately following it. This method also triggers the `"change"` event.
* @param {Number} row The row to check
*
**/
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._signal("change", { data: delta });
+ if (row < this.getLength() - 1 && row >= 0) {
+ this.applyDelta({
+ start: this.pos(row, this.getLine(row).length),
+ end: this.pos(row + 1, 0),
+ action: "remove",
+ lines: ["", ""]
+ });
+ }
};
/**
@@ -526,9 +521,9 @@ var Document = function(text) {
*
**/
this.replace = function(range, text) {
- if (!(range instanceof Range))
+ 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
@@ -537,55 +532,106 @@ 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 `'insertText'`, `'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) {
+ var isInsert = delta.action == "insert";
+ // An empty range is a NOOP.
+ if (isInsert ? delta.lines.length <= 1 && !delta.lines[0]
+ : !Range.comparePoints(delta.start, delta.end)) {
+ return;
+ }
+
+ if (isInsert && delta.lines.length > 20000)
+ this.$splitAndapplyLargeDelta(delta, 20000);
+
+ // Apply.
+ applyDelta(this.$lines, delta, doNotValidate);
+ this._signal("change", delta);
+ };
+
+ this.$splitAndapplyLargeDelta = function(delta, MAX) {
+ // 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 smallest threshold is on chrome 40 ~42000.
+ // we use 20000 to leave some space for actual stack
+ //
+ // 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 lines = delta.lines;
+ var l = lines.length;
+ var row = delta.start.row;
+ var column = delta.start.column;
+ var from = 0, to = 0;
+ do {
+ from = to;
+ to += MAX - 1;
+ var chunk = lines.slice(from, to);
+ if (to > l) {
+ // Update remaining delta.
+ delta.lines = chunk;
+ delta.start.row = row + from;
+ delta.start.column = column;
+ break;
+ }
+ chunk.push("");
+ this.applyDelta({
+ start: this.pos(row + from, column),
+ end: this.pos(row + to, column = 0),
+ action: delta.action,
+ lines: chunk
+ }, true);
+ } while(true);
+ };
+
+ /**
+ * Reverts `delta` from the document.
+ * @param {Object} delta A delta object (can include "insert" and "remove" actions)
+ **/
+ this.revertDelta = function(delta) {
+ this.applyDelta({
+ start: this.clonePos(delta.start),
+ end: this.clonePos(delta.end),
+ action: (delta.action == "insert" ? "remove" : "insert"),
+ 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 051434b4..ada56cd2 100644
--- a/lib/ace/document_test.js
+++ b/lib/ace/document_test.js
@@ -46,7 +46,7 @@ module.exports = {
var doc = new Document(["12", "34"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
doc.insert({row: 0, column: 1}, "juhu");
assert.equal(doc.getValue(), ["1juhu2", "34"].join("\n"));
@@ -63,9 +63,9 @@ module.exports = {
var doc = new Document(["12", "34"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
- 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();
@@ -80,9 +80,9 @@ module.exports = {
var doc = new Document(["12", "34"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
- doc.insertLines(0, ["aa", "bb"]);
+ doc.insertFullLines(0, ["aa", "bb"]);
assert.equal(doc.getValue(), ["aa", "bb", "12", "34"].join("\n"));
var d = deltas.concat();
@@ -97,9 +97,9 @@ module.exports = {
var doc = new Document(["12", "34"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
- doc.insertLines(2, ["aa", "bb"]);
+ doc.insertFullLines(2, ["aa", "bb"]);
assert.equal(doc.getValue(), ["12", "34", "aa", "bb"].join("\n"));
},
@@ -107,9 +107,9 @@ module.exports = {
var doc = new Document(["12", "34"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
- doc.insertLines(1, ["aa", "bb"]);
+ doc.insertFullLines(1, ["aa", "bb"]);
assert.equal(doc.getValue(), ["12", "aa", "bb", "34"].join("\n"));
var d = deltas.concat();
@@ -124,7 +124,7 @@ module.exports = {
var doc = new Document(["12", "34"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
doc.insert({row: 0, column: 0}, "aa\nbb\ncc");
assert.equal(doc.getValue(), ["aa", "bb", "cc12", "34"].join("\n"));
@@ -141,9 +141,9 @@ module.exports = {
var doc = new Document(["12", "34"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
- 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();
@@ -158,7 +158,7 @@ module.exports = {
var doc = new Document(["12", "34"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
doc.insert({row: 0, column: 1}, "aa\nbb\ncc");
assert.equal(doc.getValue(), ["1aa", "bb", "cc2", "34"].join("\n"));
@@ -175,7 +175,7 @@ module.exports = {
var doc = new Document(["1234", "5678"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
doc.remove(new Range(0, 1, 0, 3));
assert.equal(doc.getValue(), ["14", "5678"].join("\n"));
@@ -192,7 +192,7 @@ module.exports = {
var doc = new Document(["1234", "5678"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
doc.remove(new Range(0, 4, 1, 0));
assert.equal(doc.getValue(), ["12345678"].join("\n"));
@@ -209,7 +209,7 @@ module.exports = {
var doc = new Document(["1234", "5678", "abcd"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
doc.remove(new Range(0, 2, 2, 2));
assert.equal(doc.getValue(), ["12cd"].join("\n"));
@@ -226,7 +226,7 @@ module.exports = {
var doc = new Document(["1234", "5678", "abcd"]);
var deltas = [];
- doc.on("change", function(e) { deltas.push(e.data); });
+ doc.on("change", function(e) { deltas.push(e); });
doc.remove(new Range(1, 0, 3, 0));
assert.equal(doc.getValue(), ["1234", ""].join("\n"));
@@ -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 f9b43a76..2aa9b378 100644
--- a/lib/ace/edit_session.js
+++ b/lib/ace/edit_session.js
@@ -249,13 +249,12 @@ var EditSession = function(text, mode) {
this.$resetRowCache(fold.start.row);
};
- this.onChange = function(e) {
- var delta = e.data;
+ this.onChange = function(delta) {
this.$modified = true;
- this.$resetRowCache(delta.range.start.row);
+ this.$resetRowCache(delta.start.row);
- var removedFolds = this.$updateInternalDataOnChange(e);
+ var removedFolds = this.$updateInternalDataOnChange(delta);
if (!this.$fromUndo && this.$undoManager && !delta.ignore) {
this.$deltasDoc.push(delta);
if (removedFolds && removedFolds.length != 0) {
@@ -269,7 +268,7 @@ var EditSession = function(text, mode) {
}
this.bgTokenizer && this.bgTokenizer.$updateOnChange(delta);
- this._signal("change", e);
+ this._signal("change", delta);
};
/**
@@ -1146,6 +1145,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.
@@ -1222,39 +1234,36 @@ 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];
var range, point;
var lastDeltaIsInsert = false;
if (isInsert(delta)) {
- range = Range.fromPoints(delta.range.start, delta.range.end);
+ range = Range.fromPoints(delta.start, delta.end);
lastDeltaIsInsert = true;
} else {
- range = Range.fromPoints(delta.range.start, delta.range.start);
+ range = Range.fromPoints(delta.start, delta.start);
lastDeltaIsInsert = false;
}
for (var i = 1; i < deltas.length; i++) {
delta = deltas[i];
if (isInsert(delta)) {
- point = delta.range.start;
+ point = delta.start;
if (range.compare(point.row, point.column) == -1) {
- range.setStart(delta.range.start);
+ range.setStart(point);
}
- point = delta.range.end;
+ point = delta.end;
if (range.compare(point.row, point.column) == 1) {
- range.setEnd(delta.range.end);
+ range.setEnd(point);
}
lastDeltaIsInsert = true;
} else {
- point = delta.range.start;
+ point = delta.start;
if (range.compare(point.row, point.column) == -1) {
- range =
- Range.fromPoints(delta.range.start, delta.range.start);
+ range = Range.fromPoints(delta.start, delta.start);
}
lastDeltaIsInsert = false;
}
@@ -1368,7 +1377,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);
};
/**
@@ -1425,11 +1434,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;
};
@@ -1439,8 +1448,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);
@@ -1451,8 +1458,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);
@@ -1656,34 +1661,23 @@ var EditSession = function(text, mode) {
};
};
- this.$updateInternalDataOnChange = function(e) {
+ this.$updateInternalDataOnChange = function(delta) {
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 action = delta.action;
+ var start = delta.start;
+ var end = delta.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;
- removedFolds = this.getFoldsInRange(e.data.range);
+ removedFolds = this.getFoldsInRange(delta);
this.removeFolds(removedFolds);
var foldLine = this.getFoldLine(end.row);
@@ -1748,10 +1742,10 @@ var EditSession = function(text, mode) {
} else {
// 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) {
+ len = Math.abs(delta.start.column - delta.end.column);
+ if (action === "remove") {
// Get all the folds in the change range and remove them.
- removedFolds = this.getFoldsInRange(e.data.range);
+ removedFolds = this.getFoldsInRange(delta);
this.removeFolds(removedFolds);
len = -len;
diff --git a/lib/ace/edit_session/folding.js b/lib/ace/edit_session/folding.js
index 53071dda..77c0f22d 100644
--- a/lib/ace/edit_session/folding.js
+++ b/lib/ace/edit_session/folding.js
@@ -829,15 +829,13 @@ function Folding() {
}
};
- this.updateFoldWidgets = function(e) {
- var delta = e.data;
- var range = delta.range;
- var firstRow = range.start.row;
- var len = range.end.row - firstRow;
+ this.updateFoldWidgets = function(delta) {
+ var firstRow = delta.start.row;
+ var len = delta.end.row - firstRow;
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 5e309cf6..0b904b63 100644
--- a/lib/ace/edit_session_test.js
+++ b/lib/ace/edit_session_test.js
@@ -442,12 +442,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);
@@ -471,9 +471,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 19d4b84e..22b43dd2 100644
--- a/lib/ace/editor.js
+++ b/lib/ace/editor.js
@@ -692,20 +692,15 @@ var Editor = function(renderer, session) {
*
*
**/
- this.onDocumentChange = function(e) {
- var delta = e.data;
- var range = delta.range;
- var lastRow;
+ this.onDocumentChange = function(delta) {
+ // Rerender and emit "change" event.
+ var wrap = this.session.$useWrapMode;
+ var lastRow = (delta.start.row == delta.end.row ? delta.end.row : Infinity);
+ this.renderer.updateLines(delta.start.row, lastRow, wrap);
- if (range.start.row == range.end.row && delta.action != "insertLines" && delta.action != "removeLines")
- lastRow = range.end.row;
- else
- lastRow = Infinity;
- this.renderer.updateLines(range.start.row, lastRow, this.session.$useWrapMode);
-
- this._signal("change", e);
-
- // update cursor because tab characters can influence the cursor position
+ this._signal("change", delta);
+
+ // Update cursor because tab characters can influence the cursor position.
this.$cursorChange();
this.$updateHighlightActiveLine();
};
@@ -1633,15 +1628,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..52a180d4 100644
--- a/lib/ace/ext/chromevox.js
+++ b/lib/ace/ext/chromevox.js
@@ -578,15 +578,14 @@ var onSelectionChange = function(evt) {
* and deleting text.
* @param {!Event} evt The event.
*/
-var onChange = function(evt) {
- var data = evt.data;
+var onChange = function(delta) {
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/ext/elastic_tabstops_lite.js b/lib/ace/ext/elastic_tabstops_lite.js
index 9901c5df..0f89423a 100644
--- a/lib/ace/ext/elastic_tabstops_lite.js
+++ b/lib/ace/ext/elastic_tabstops_lite.js
@@ -44,13 +44,12 @@ var ElasticTabstopsLite = function(editor) {
this.onExec = function() {
recordChanges = true;
};
- this.onChange = function(e) {
- var range = e.data.range
+ this.onChange = function(delta) {
if (recordChanges) {
- if (changedRows.indexOf(range.start.row) == -1)
- changedRows.push(range.start.row);
- if (range.end.row != range.start.row)
- changedRows.push(range.end.row);
+ if (changedRows.indexOf(delta.start.row) == -1)
+ changedRows.push(delta.start.row);
+ if (delta.end.row != delta.start.row)
+ changedRows.push(delta.end.row);
}
};
};
diff --git a/lib/ace/keyboard/vim.js b/lib/ace/keyboard/vim.js
index fc41bb90..17c26c16 100644
--- a/lib/ace/keyboard/vim.js
+++ b/lib/ace/keyboard/vim.js
@@ -178,13 +178,6 @@ define(function(require, exports, module) {
return this.ace.inVirtualSelectionMode && this.ace.selection.index;
};
this.onChange = function(delta) {
- var oldDelta = delta.data;
- delta = {
- start: oldDelta.range.start,
- end: oldDelta.range.end,
- action: oldDelta.action,
- lines: oldDelta.lines || [oldDelta.text]
- };// v1.2 api compatibility
if (delta.action[0] == 'i') {
var change = { text: delta.lines };
var curOp = this.curOp = this.curOp || {};
@@ -578,7 +571,6 @@ define(function(require, exports, module) {
highlight.session = null;
};
highlight.updateOnChange = function(delta) {
- delta = delta.data.range;// v1.2 api compatibility
var row = delta.start.row;
if (row == delta.end.row) highlight.cache[row] = undefined;
else highlight.cache.splice(row, highlight.cache.length);
diff --git a/lib/ace/layer/gutter.js b/lib/ace/layer/gutter.js
index 13ee8a25..dc1055c2 100644
--- a/lib/ace/layer/gutter.js
+++ b/lib/ace/layer/gutter.js
@@ -100,16 +100,14 @@ var Gutter = function(parentEl) {
}
};
- this.$updateAnnotations = function (e) {
+ this.$updateAnnotations = function (delta) {
if (!this.$annotations.length)
return;
- var delta = e.data;
- var range = delta.range;
- var firstRow = range.start.row;
- var len = range.end.row - firstRow;
+ var firstRow = delta.start.row;
+ var len = delta.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 d4080b53..ba87d857 100644
--- a/lib/ace/line_widgets.js
+++ b/lib/ace/line_widgets.js
@@ -113,18 +113,16 @@ function LineWidgets(session) {
});
};
- this.updateOnChange = function(e) {
+ this.updateOnChange = function(delta) {
var lineWidgets = this.session.lineWidgets;
if (!lineWidgets) return;
-
- var delta = e.data;
- var range = delta.range;
- var startRow = range.start.row;
- var len = range.end.row - startRow;
+
+ var startRow = delta.start.row;
+ var len = delta.end.row - startRow;
if (len === 0) {
// return
- } else if (delta.action == "removeText" || delta.action == "removeLines") {
+ } else if (delta.action == 'remove') {
var removed = lineWidgets.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 85611a33..1d0b69e7 100644
--- a/lib/ace/multi_select.js
+++ b/lib/ace/multi_select.js
@@ -754,9 +754,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/multi_select_test.js b/lib/ace/multi_select_test.js
index f63038d6..7fd504fb 100644
--- a/lib/ace/multi_select_test.js
+++ b/lib/ace/multi_select_test.js
@@ -30,6 +30,7 @@
if (typeof process !== "undefined") {
require("amd-loader");
+ require("./test/mockdom");
}
define(function(require, exports, module) {
@@ -41,6 +42,7 @@ var Range = require("./range").Range;
var Editor = require("./editor").Editor;
var EditSession = require("./edit_session").EditSession;
var MockRenderer = require("./test/mockrenderer").MockRenderer;
+var UndoManager = require("./undomanager").UndoManager;
var editor;
var exec = function(name, times, args) {
@@ -259,6 +261,20 @@ module.exports = {
selection.fromJSON(after);
assert.ok(!selection.isEqual(before));
assert.ok(selection.isEqual(after));
+ },
+
+ "test multiselect align": function() {
+ var doc = new EditSession(["l1", "l2", "l3"]);
+ doc.setUndoManager(new UndoManager());
+ editor = new Editor(new MockRenderer(), doc);
+ var selection = editor.selection;
+ selection.addRange(new Range(1,0,1,0))
+ selection.addRange(new Range(2,2,2,2))
+ editor.execCommand("alignCursors");
+ assert.equal(' l1\n l2\nl3', editor.getValue());
+ doc.markUndoGroup();
+ editor.execCommand("undo");
+ assert.equal('l1\nl2\nl3', editor.getValue());
}
};
diff --git a/lib/ace/placeholder.js b/lib/ace/placeholder.js
index 582e2c2b..0e299594 100644
--- a/lib/ace/placeholder.js
+++ b/lib/ace/placeholder.js
@@ -150,28 +150,27 @@ var PlaceHolder = function(session, length, pos, others, mainClass, othersClass)
* Emitted when the place holder updates.
*
**/
- this.onUpdate = function(event) {
- var delta = event.data;
- var range = delta.range;
+ this.onUpdate = function(delta) {
+ var range = delta;
if(range.start.row !== range.end.row) return;
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};
@@ -181,7 +180,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++) {
@@ -193,7 +192,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/range_list.js b/lib/ace/range_list.js
index 0cbcc394..326bc41b 100644
--- a/lib/ace/range_list.js
+++ b/lib/ace/range_list.js
@@ -180,14 +180,13 @@ var RangeList = function() {
this.session = null;
};
- this.$onChange = function(e) {
- var changeRange = e.data.range;
- if (e.data.action[0] == "i"){
- var start = changeRange.start;
- var end = changeRange.end;
+ this.$onChange = function(delta) {
+ if (delta.action == "insert"){
+ var start = delta.start;
+ var end = delta.end;
} else {
- var end = changeRange.start;
- var start = changeRange.end;
+ var end = delta.start;
+ var start = delta.end;
}
var startRow = start.row;
var endRow = end.row;
diff --git a/lib/ace/snippets.js b/lib/ace/snippets.js
index 10c34bf0..699cd6e9 100644
--- a/lib/ace/snippets.js
+++ b/lib/ace/snippets.js
@@ -667,11 +667,11 @@ var TabstopManager = function(editor) {
this.editor = null;
};
- this.onChange = function(e) {
- var changeRange = e.data.range;
- var isRemove = e.data.action[0] == "r";
- var start = changeRange.start;
- var end = changeRange.end;
+ this.onChange = function(delta) {
+ var changeRange = delta;
+ var isRemove = delta.action[0] == "r";
+ var start = delta.start;
+ var end = delta.end;
var startRow = start.row;
var endRow = end.row;
var lineDif = endRow - startRow;
diff --git a/lib/ace/undomanager.js b/lib/ace/undomanager.js
index 4411ae8b..6da50a8b 100644
--- a/lib/ace/undomanager.js
+++ b/lib/ace/undomanager.js
@@ -61,15 +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]);
+ var deltaSets = options.args[0];
+ // Add deltas to undo stack.
this.$doc = options.args[1];
if (options.merge && this.hasUndo()){
this.dirtyCounter--;
- 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.
@@ -86,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--;
}
@@ -105,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;
};
@@ -161,7 +163,52 @@ 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,
+ start: delta.start,
+ end: delta.end,
+ lines: delta.lines.length == 1 ? null : delta.lines,
+ text: delta.lines.length == 1 ? delta.lines[0] : null,
+ };
+ }
+
+ function $deserializeDelta(delta) {
+ return {
+ action: delta.action,
+ start: delta.start,
+ end: delta.end,
+ lines: delta.lines || [delta.text]
+ };
+ }
+
+ function cloneDeltaSetsObj(deltaSets_old, fnGetModifiedDelta) {
+ var deltaSets_new = new Array(deltaSets_old.length);
+ for (var i = 0; i < deltaSets_old.length; i++) {
+ var deltaSet_old = deltaSets_old[i];
+ var deltaSet_new = { group: deltaSet_old.group, deltas: new Array(deltaSet_old.length)};
+
+ for (var j = 0; j < deltaSet_old.deltas.length; j++) {
+ var delta_old = deltaSet_old.deltas[j];
+ deltaSet_new.deltas[j] = 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..ef6e2aa3 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,19 @@ var Mirror = exports.Mirror = function(sender) {
var _self = this;
sender.on("change", function(e) {
- doc.applyDeltas(e.data);
+ var data = e.data;
+ if (data[0].start) {
+ doc.applyDeltas(data);
+ } else {
+ for (var i = 0; i < data.length; i += 2) {
+ if (Array.isArray(data[i+1])) {
+ var d = {action: "insert", start: data[i], lines: data[i+1]};
+ } else {
+ var d = {action: "remove", start: data[i], end: data[i+1]};
+ }
+ doc.applyDelta(d, true);
+ }
+ }
if (_self.$timeout)
return deferredUpdate.schedule(_self.$timeout);
_self.onUpdate();
diff --git a/lib/ace/worker/worker_client.js b/lib/ace/worker/worker_client.js
index ad445287..ba4f20a0 100644
--- a/lib/ace/worker/worker_client.js
+++ b/lib/ace/worker/worker_client.js
@@ -162,19 +162,22 @@ var WorkerClient = function(topLevelNamespaces, mod, classname, workerUrl) {
doc.on("change", this.changeListener);
};
- this.changeListener = function(e) {
+ this.changeListener = function(delta) {
if (!this.deltaQueue) {
- this.deltaQueue = [e.data];
+ this.deltaQueue = [];
setTimeout(this.$sendDeltaQueue, 0);
- } else
- this.deltaQueue.push(e.data);
+ }
+ if (delta.action == "insert")
+ this.deltaQueue.push(delta.start, delta.lines);
+ else
+ this.deltaQueue.push(delta.start, delta.end);
};
this.$sendDeltaQueue = function() {
var q = this.deltaQueue;
if (!q) return;
this.deltaQueue = null;
- if (q.length > 20 && q.length > this.$doc.getLength() >> 1) {
+ if (q.length > 50 && q.length > this.$doc.getLength() >> 1) {
this.call("setValue", [this.$doc.getValue()]);
} else
this.emit("change", {data: q});