Merge pull request #1745 aldendaniels:master into v-1.2
Refactor document delta handling
This commit is contained in:
commit
8d57d8765f
18 changed files with 627 additions and 433 deletions
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
137
lib/ace/apply_delta.js
Normal file
137
lib/ace/apply_delta.js
Normal 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 (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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:<br/>
|
||||
* ```
|
||||
* {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<deltas.length; i++) {
|
||||
var delta = deltas[i];
|
||||
var range = Range.fromPoints(delta.range.start, delta.range.end);
|
||||
|
||||
if (delta.action == "insertLines")
|
||||
this.insertLines(range.start.row, delta.lines);
|
||||
else if (delta.action == "insertText")
|
||||
this.insert(range.start, delta.text);
|
||||
else if (delta.action == "removeLines")
|
||||
this._removeLines(range.start.row, range.end.row - 1);
|
||||
else if (delta.action == "removeText")
|
||||
this.remove(range);
|
||||
this.applyDelta(deltas[i]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reverts any changes previously applied. These can be either `'includeText'`, `'insertLines'`, `'removeText'`, and `'removeLines'`.
|
||||
* Reverts all changes in `deltas` from the document.
|
||||
* @param {Array} deltas An array of delta objects (can include 'insert' and 'remove' actions)
|
||||
**/
|
||||
this.revertDeltas = function(deltas) {
|
||||
for (var i=deltas.length-1; 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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue