Break on applyDelta into its own module

This makes it possible to break out helper functions without exposing
them to the rest of the document class. Also, long term, we may want to
have a stand-alone test suite for applyDelta, so it makes sense in its
own file.

All other changes involve syntax corrections (some syntax issues were
mine, others pre-existed) to make the documentation compilation work.
This commit is contained in:
aldendaniels 2014-01-03 14:59:22 -06:00
commit b503e65e03
5 changed files with 215 additions and 156 deletions

137
lib/ace/apply_delta.js Normal file
View file

@ -0,0 +1,137 @@
/* ***** BEGIN LICENSE BLOCK *****
* Distributed under the BSD license:
*
* Copyright (c) 2010, Ajax.org B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Ajax.org B.V. nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* ***** END LICENSE BLOCK ***** */
define(function(require, exports, module) {
"use strict";
var Range = require("./range").Range;
function splitLine (lines, point) {
var text = lines[point.row];
lines[point.row] = text.slice(0, point.column);
lines.splice(point.row + 1, 0, text.slice(point.column));
}
function joinLineWithNext(lines, row) {
lines[row] += lines[row + 1];
lines.splice(row + 1, 1);
}
function throwDeltaError(delta, errorText){
errorText = 'Invalid Delta: ' + errorText;
console.log(errorText, delta);
throw errorText;
}
function validateDelta(lines, delta) {
// Validate action.
if (delta.action != 'insert' && delta.action != 'delete')
fnThrow('Delta action must be "insert" or "delete".');
// Validate lines.
if (!delta.lines instanceof Array)
fnThrow('Delta lines must be an array');
// Validate range type.
if (!delta.range instanceof Range)
fnThrow('Range object is not an instance of the Range class');
// Validate start point.
var start = delta.range.start;
if (Math.min(Math.max(start.row, 0), lines.length - 1 ) != start.row ||
Math.min(Math.max(start.column, 0), lines[start.row].length) != start.column)
{
fnThrow('Range start point not contained in document');
}
// Validate ending row offset.
if (delta.lines.length - 1 != delta.range.end.row - delta.range.start.row)
fnThrow('Range row offsets does not match delta lines');
// TODO:
// - Validate that the ending column offset matches the lines.
// - Validate the deleted lines match the lines in the document.
}
exports.applyDelta = function(lines, delta) {
// Validate delta.
validateDelta(lines, delta);
// Apply delta.
if (delta.range.start.row == delta.range.end.row)
{
// Apply single-line delta.
// Note: The multi-line code below correctly handle single-line
// deltas too, but we need to short-circuit for speed.
var row = delta.range.start.row;
var startColumn = delta.range.start.column;
var endColumn = delta.range.end.column;
var line = lines[row];
switch (delta.action) {
case 'insert':
lines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn);
break;
case 'delete':
lines[row] = line.substring(0, startColumn) + line.substring(endColumn);
break;
}
} else {
// Apply multi-line delta.
switch (delta.action) {
case 'insert':
splitLine(lines, delta.range.start);
for (var i = 0; i < delta.lines.length; i++) {
var row = delta.range.start.row + 1 + i;
lines.splice(row, 0, delta.lines[i]);
}
joinLineWithNext(lines, delta.range.start.row);
joinLineWithNext(lines, delta.range.end.row);
break;
case 'delete':
splitLine(lines, delta.range.end);
splitLine(lines, delta.range.start);
lines.splice(
delta.range.start.row + 1, // Where to start deleting
delta.range.end.row - delta.range.start.row + 1 // Num lines to delete.
);
joinLineWithNext(lines, delta.range.start.row);
break;
}
}
}
});

View file

@ -32,6 +32,7 @@ define(function(require, exports, module) {
"use strict";
var oop = require("./lib/oop");
var applyDelta = require("./apply_delta").applyDelta;
var EventEmitter = require("./lib/event_emitter").EventEmitter;
var Range = require("./range").Range;
var Anchor = require("./anchor").Anchor;
@ -56,7 +57,7 @@ var Document = function(textOrLines) {
// There has to be one line at least in the document. If you pass an empty
// string to the insert function, nothing will happen. Workaround.
if (textOrLines.length == 0) {
if (textOrLines.length === 0) {
this.$lines = [""];
} else if (Array.isArray(textOrLines)) {
this.insertMergedLines({row: 0, column: 0}, textOrLines);
@ -107,10 +108,10 @@ var Document = function(textOrLines) {
**/
// check for IE split bug
if ("aaa".split(/a/).length == 0)
if ("aaa".split(/a/).length === 0)
this.$split = function(text) {
return text.replace(/\r\n|\r/g, "\n").split("\n");
}
};
else
this.$split = function(text) {
return text.split(/\r\n|\r|\n/);
@ -205,30 +206,41 @@ var Document = function(textOrLines) {
};
/**
* [Given a range within the document, this function returns all the text within that range as a single string.]{: #Document.getTextRange.desc}
* @param {Range} range The range to work with
* Returns all the text within `range` as a single string.
* @param {Range} range The range to work with.
*
* @returns {String}
**/
this.getTextRange = function(range) {
return this._getLinesForRange(range).join(this.getNewLineCharacter());
return this.getLinesForRange(range).join(this.getNewLineCharacter());
};
this._getLinesForRange = function(range) {
/**
* Returns all the text within `range` as an array of lines.
* @param {Range} range The range to work with.
*
* @returns {Array}
**/
this.getLinesForRange = function(range) {
var lines;
if (range.start.row == range.end.row) {
return [this.getLine(range.start.row)
.substring(range.start.column, range.end.column)];
// Handle a single-line range.
lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)];
} else {
// Handle a multi-line range.
lines = this.getLines(range.start.row, range.end.row);
lines[0] = (lines[0] || "").substring(range.start.column);
var l = lines.length - 1;
if (range.end.row - range.start.row == l)
lines[l] = lines[l].substring(0, range.end.column);
}
var lines = this.getLines(range.start.row, range.end.row);
lines[0] = (lines[0] || "").substring(range.start.column);
var l = lines.length - 1;
if (range.end.row - range.start.row == l)
lines[l] = lines[l].substring(0, range.end.column);
return lines;
};
this.$clipPosition = function(position) {
var length = this.getLength();
if (position.row >= length) {
position.row = Math.max(0, length - 1);
@ -240,8 +252,8 @@ var Document = function(textOrLines) {
return position;
};
this.$getClippedRange = function(range)
{
this.$getClippedRange = function(range) {
// Get Range object.
if (!range instanceof Range)
range = Range.fromPoints(range.start, range.end);
@ -250,67 +262,29 @@ var Document = function(textOrLines) {
this.$clipPosition(range.start);
this.$clipPosition(range.end);
return range;
}
};
this.$validateDelta = function(delta)
{
function fnThrow(errorText)
{
errorText = 'Invalid Delta: ' + errorText;
console.log(errorText, delta);
throw errorText;
}
// Validate action.
if (delta.action != 'insert' && delta.action != 'delete')
fnThrow('Delta action must be "insert" or "delete".');
// Validate lines.
if (!delta.lines instanceof Array)
fnThrow('Delta lines must be an array');
// Validate range type.
if (!delta.range instanceof Range)
fnThrow('Range object is not an instance of the Range class');
// Validate start point.
var start = delta.range.start;
if (Math.min(Math.max(start.row, 0), this.getLength() - 1 ) != start.row ||
Math.min(Math.max(start.column, 0), this.$lines[start.row].length) != start.column)
{
fnThrow('Range start point not contained in document');
}
// Validate ending row offset.
if (delta.lines.length - 1 != delta.range.end.row - delta.range.start.row)
fnThrow('Range row offsets does not match delta lines');
// TODO:
// - Validate that the ending column offset matches the lines.
// - Validate the deleted lines match the lines in the document.
},
// Deprecated methods retained for backwards compatibility.
this.insert = function(position, text){
console.warn('Use of document.insert is deprecated. Use the insertText method instead.');
return this.insertText(position, text);
}
};
this.insertLines = function(row, lines) {
console.warn('Use of document.insertLines is deprecated. Use the insertFullLines method instead.');
return this.insertFullLines(row, lines);
}
};
this.removeLines = function(firstRow, lastRow) {
console.warn('Use of document.removeLines is deprecated. Use the removeFullLines method instead.');
return this.removeFullLines(firstRow, lastRow);
}
};
this.insertNewLine = function(position) {
console.warn('Use of document.insertNewLine is deprecated. Use insertMergedLines(position, [\'\', \'\']) instead.');
return this.insertMergedLines(position, ['', '']);
}
};
this.insertInLine = function(position, text) {
console.warn('Use of document.insertInLine is deprecated. Use insertText instead.');
return this.insertText(position, text);
}
};
/**
* Inserts a block of `text` at the indicated `position`.
@ -374,12 +348,12 @@ var Document = function(textOrLines) {
// Insert after the last row in the document.
lines = [''].concat(lines);
row--;
var column = this.$lines[row].length;
column = this.$lines[row].length;
}
// Insert.
this.insertMergedLines({row: row, column: column}, lines);
},
};
/**
* Inserts the elements in `lines` into the document, starting at the position index given by `row`. This method also triggers the `'change'` event.
@ -412,7 +386,7 @@ var Document = function(textOrLines) {
});
return endPoint;
}
};
/**
* Removes the `range` from the document.
@ -427,7 +401,7 @@ var Document = function(textOrLines) {
this.applyDelta({
action: 'delete',
range: range,
lines: this._getLinesForRange(range),
lines: this.getLinesForRange(range),
});
return range.start;
};
@ -450,7 +424,7 @@ var Document = function(textOrLines) {
this.applyDelta({
action: "delete",
range: range,
lines: this._getLinesForRange(range)
lines: this.getLinesForRange(range)
});
return range.start;
@ -487,7 +461,7 @@ var Document = function(textOrLines) {
this.applyDelta({
action: "delete",
range: range,
lines: this._getLinesForRange(range)
lines: this.getLinesForRange(range)
});
// Return the deleted lines.
@ -524,7 +498,7 @@ var Document = function(textOrLines) {
this.replace = function(range, text) {
if (!range instanceof Range)
range = Range.fromPoints(range.start, range.end);
if (text.length == 0 && range.isEmpty())
if (text.length === 0 && range.isEmpty())
return range.start;
// Shortcut: If the text we want to insert is the same as it is already
@ -533,8 +507,9 @@ var Document = function(textOrLines) {
return range.end;
this.remove(range);
var end;
if (text) {
var end = this.insertText(range.start, text);
end = this.insertText(range.start, text);
}
else {
end = range.start;
@ -544,7 +519,8 @@ var Document = function(textOrLines) {
};
/**
* Applies all the changes previously accumulated. These can be either `'insert'` or `'delete'`.
* Applies all changes in `deltas` to the document.
* @param {Array} deltas An array of delta objects (can include 'insert' and 'delete' actions)
**/
this.applyDeltas = function(deltas) {
for (var i=0; i<deltas.length; i++) {
@ -552,76 +528,9 @@ var Document = function(textOrLines) {
}
};
this.applyDelta = function(delta) {
function splitLine(lines, point)
{
var text = lines[point.row];
lines[point.row] = text.slice(0, point.column);
lines.splice(point.row + 1, 0, text.slice(point.column));
}
function joinLineWithNext(lines, row)
{
lines[row] += lines[row + 1];
lines.splice(row + 1, 1);
}
// Validate delta.
this.$validateDelta(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 = this.$lines[row];
switch (delta.action) {
case 'insert':
this.$lines[row] = line.substring(0, startColumn) + delta.lines[0] + line.substring(startColumn);
break;
case 'delete':
this.$lines[row] = line.substring(0, startColumn) + line.substring(endColumn);
break;
}
} else {
// Apply multi-line delta.
switch (delta.action) {
case 'insert':
splitLine(this.$lines, delta.range.start);
for (var i = 0; i < delta.lines.length; i++) {
var row = delta.range.start.row + 1 + i;
this.$lines.splice(row, 0, delta.lines[i]);
}
joinLineWithNext(this.$lines, delta.range.start.row);
joinLineWithNext(this.$lines, delta.range.end.row);
break;
case 'delete':
splitLine(this.$lines, delta.range.end);
splitLine(this.$lines, delta.range.start);
this.$lines.splice(
delta.range.start.row + 1, // Where to start deleting
delta.range.end.row - delta.range.start.row + 1 // Num lines to delete.
);
joinLineWithNext(this.$lines, delta.range.start.row);
break;
}
}
this._emit("change", { data: delta });
},
/**
* Reverts any changes previously applied. These can be either `'insert'` or `'delete'`.
* Reverts all changes in `deltas` from the document.
* @param {Array} deltas An array of delta objects (can include 'insert' and 'delete' actions)
**/
this.revertDeltas = function(deltas) {
for (var i=deltas.length-1; i>=0; i--) {
@ -629,6 +538,19 @@ var Document = function(textOrLines) {
}
};
/**
* Applies `delta` to the document.
* @param {Object} delta A delta object (can include 'insert' and 'delete' actions)
**/
this.applyDelta = function(delta) {
applyDelta(this.$lines, delta);
this._emit("change", { data: delta });
};
/**
* Reverts `delta` from the document.
* @param {Object} delta A delta object (can include 'insert' and 'delete' actions)
**/
this.revertDelta = function(delta)
{
this.applyDelta({
@ -636,8 +558,8 @@ var Document = function(textOrLines) {
range: delta.range.clone(),
lines: delta.lines.slice()
});
},
};
/**
* Converts an index position in a document to a `{row, column}` object.
*

View file

@ -1131,8 +1131,8 @@ var EditSession = function(text, mode) {
// Deprecated method retained for backwards compatibility.
this.insert = function(position, text){
console.warn('Use of editsession.insert is deprecated. Use the insertText method instead.');
return this.insertText(position, text)
}
return this.insertText(position, text);
};
/**
* Inserts a block of `text` and the indicated `position`.
@ -1168,8 +1168,8 @@ var EditSession = function(text, mode) {
*
**/
this.removeFullLines = function(firstRow, lastRow){
return this.doc.removeFullLines(firstRow, lastRow)
}
return this.doc.removeFullLines(firstRow, lastRow);
};
/**
* Reverts previous changes to your document.
@ -1619,7 +1619,7 @@ var EditSession = function(text, mode) {
* @private
**/
this.adjustWrapLimit = function(desiredLimit, $printMargin) {
var limits = this.$wrapLimitRange
var limits = this.$wrapLimitRange;
if (limits.max < 0)
limits = {min: $printMargin, max: $printMargin};
var wrapLimit = this.$constrainWrapLimit(desiredLimit, limits.min, limits.max);
@ -1738,7 +1738,7 @@ var EditSession = function(text, mode) {
var foldLine = this.getFoldLine(firstRow);
var idx = 0;
if (foldLine) {
var cmp = foldLine.range.compareInside(start.row, start.column)
var cmp = foldLine.range.compareInside(start.row, start.column);
// Inside of the foldLine range. Need to split stuff up.
if (cmp == 0) {
foldLine = foldLine.split(start.row, start.column);
@ -2215,7 +2215,7 @@ var EditSession = function(text, mode) {
return {
row: maxRow,
column: this.getLine(maxRow).length
}
};
} else {
line = this.getLine(docRow);
foldLine = null;

View file

@ -833,8 +833,8 @@ var Editor = function(renderer, session) {
// Deprecated method retained for backwards compatibility.
this.insert = function(text){
console.warn('Use of editor.insert is deprecated. Use the insertText method instead.');
return this.insertText(text)
}
return this.insertText(text);
};
/**
* Inserts `text` into wherever the cursor is pointing.
@ -876,7 +876,7 @@ var Editor = function(renderer, session) {
}
if (text == "\n" || text == "\r\n") {
var line = session.getLine(cursor.row)
var line = session.getLine(cursor.row);
if (cursor.column > line.search(/\S|$/)) {
var d = line.substr(cursor.column).search(/\S|$/);
session.doc.removeInLine(cursor.row, cursor.column, cursor.column + d);
@ -1362,7 +1362,7 @@ var Editor = function(renderer, session) {
}
}
var line = session.getLine(range.start.row)
var line = session.getLine(range.start.row);
var position = range.start;
var size = session.getTabSize();
var column = session.documentToScreenColumn(position.row, position.column);

View file

@ -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;