document.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(ace_require, exports, module) {
  31. "use strict";
  32. var oop = ace_require("./lib/oop");
  33. var applyDelta = ace_require("./apply_delta").applyDelta;
  34. var EventEmitter = ace_require("./lib/event_emitter").EventEmitter;
  35. var Range = ace_require("./range").Range;
  36. var Anchor = ace_require("./anchor").Anchor;
  37. /**
  38. * Contains the text of the document. Document can be attached to several [[EditSession `EditSession`]]s.
  39. * At its core, `Document`s are just an array of strings, with each row in the document matching up to the array index.
  40. *
  41. * @class Document
  42. **/
  43. /**
  44. *
  45. * Creates a new `Document`. If `text` is included, the `Document` contains those strings; otherwise, it's empty.
  46. * @param {String | Array} text The starting text
  47. * @constructor
  48. **/
  49. var Document = function(textOrLines) {
  50. this.$lines = [""];
  51. // There has to be one line at least in the document. If you pass an empty
  52. // string to the insert function, nothing will happen. Workaround.
  53. if (textOrLines.length === 0) {
  54. this.$lines = [""];
  55. } else if (Array.isArray(textOrLines)) {
  56. this.insertMergedLines({row: 0, column: 0}, textOrLines);
  57. } else {
  58. this.insert({row: 0, column:0}, textOrLines);
  59. }
  60. };
  61. (function() {
  62. oop.implement(this, EventEmitter);
  63. /**
  64. * Replaces all the lines in the current `Document` with the value of `text`.
  65. *
  66. * @param {String} text The text to use
  67. **/
  68. this.setValue = function(text) {
  69. var len = this.getLength() - 1;
  70. this.remove(new Range(0, 0, len, this.getLine(len).length));
  71. this.insert({row: 0, column: 0}, text);
  72. };
  73. /**
  74. * Returns all the lines in the document as a single string, joined by the new line character.
  75. **/
  76. this.getValue = function() {
  77. return this.getAllLines().join(this.getNewLineCharacter());
  78. };
  79. /**
  80. * Creates a new `Anchor` to define a floating point in the document.
  81. * @param {Number} row The row number to use
  82. * @param {Number} column The column number to use
  83. *
  84. **/
  85. this.createAnchor = function(row, column) {
  86. return new Anchor(this, row, column);
  87. };
  88. /**
  89. * Splits a string of text on any newline (`\n`) or carriage-return (`\r`) characters.
  90. *
  91. * @method $split
  92. * @param {String} text The text to work with
  93. * @returns {String} A String array, with each index containing a piece of the original `text` string.
  94. *
  95. **/
  96. // check for IE split bug
  97. if ("aaa".split(/a/).length === 0) {
  98. this.$split = function(text) {
  99. return text.replace(/\r\n|\r/g, "\n").split("\n");
  100. };
  101. } else {
  102. this.$split = function(text) {
  103. return text.split(/\r\n|\r|\n/);
  104. };
  105. }
  106. this.$detectNewLine = function(text) {
  107. var match = text.match(/^.*?(\r\n|\r|\n)/m);
  108. this.$autoNewLine = match ? match[1] : "\n";
  109. this._signal("changeNewLineMode");
  110. };
  111. /**
  112. * Returns the newline character that's being used, depending on the value of `newLineMode`.
  113. * @returns {String} If `newLineMode == windows`, `\r\n` is returned.
  114. * If `newLineMode == unix`, `\n` is returned.
  115. * If `newLineMode == auto`, the value of `autoNewLine` is returned.
  116. *
  117. **/
  118. this.getNewLineCharacter = function() {
  119. switch (this.$newLineMode) {
  120. case "windows":
  121. return "\r\n";
  122. case "unix":
  123. return "\n";
  124. default:
  125. return this.$autoNewLine || "\n";
  126. }
  127. };
  128. this.$autoNewLine = "";
  129. this.$newLineMode = "auto";
  130. /**
  131. * [Sets the new line mode.]{: #Document.setNewLineMode.desc}
  132. * @param {String} newLineMode [The newline mode to use; can be either `windows`, `unix`, or `auto`]{: #Document.setNewLineMode.param}
  133. *
  134. **/
  135. this.setNewLineMode = function(newLineMode) {
  136. if (this.$newLineMode === newLineMode)
  137. return;
  138. this.$newLineMode = newLineMode;
  139. this._signal("changeNewLineMode");
  140. };
  141. /**
  142. * [Returns the type of newlines being used; either `windows`, `unix`, or `auto`]{: #Document.getNewLineMode}
  143. * @returns {String}
  144. **/
  145. this.getNewLineMode = function() {
  146. return this.$newLineMode;
  147. };
  148. /**
  149. * Returns `true` if `text` is a newline character (either `\r\n`, `\r`, or `\n`).
  150. * @param {String} text The text to check
  151. *
  152. **/
  153. this.isNewLine = function(text) {
  154. return (text == "\r\n" || text == "\r" || text == "\n");
  155. };
  156. /**
  157. * Returns a verbatim copy of the given line as it is in the document
  158. * @param {Number} row The row index to retrieve
  159. *
  160. **/
  161. this.getLine = function(row) {
  162. return this.$lines[row] || "";
  163. };
  164. /**
  165. * Returns an array of strings of the rows between `firstRow` and `lastRow`. This function is inclusive of `lastRow`.
  166. * @param {Number} firstRow The first row index to retrieve
  167. * @param {Number} lastRow The final row index to retrieve
  168. *
  169. **/
  170. this.getLines = function(firstRow, lastRow) {
  171. return this.$lines.slice(firstRow, lastRow + 1);
  172. };
  173. /**
  174. * Returns all lines in the document as string array.
  175. **/
  176. this.getAllLines = function() {
  177. return this.getLines(0, this.getLength());
  178. };
  179. /**
  180. * Returns the number of rows in the document.
  181. **/
  182. this.getLength = function() {
  183. return this.$lines.length;
  184. };
  185. /**
  186. * Returns all the text within `range` as a single string.
  187. * @param {Range} range The range to work with.
  188. *
  189. * @returns {String}
  190. **/
  191. this.getTextRange = function(range) {
  192. return this.getLinesForRange(range).join(this.getNewLineCharacter());
  193. };
  194. /**
  195. * Returns all the text within `range` as an array of lines.
  196. * @param {Range} range The range to work with.
  197. *
  198. * @returns {Array}
  199. **/
  200. this.getLinesForRange = function(range) {
  201. var lines;
  202. if (range.start.row === range.end.row) {
  203. // Handle a single-line range.
  204. lines = [this.getLine(range.start.row).substring(range.start.column, range.end.column)];
  205. } else {
  206. // Handle a multi-line range.
  207. lines = this.getLines(range.start.row, range.end.row);
  208. lines[0] = (lines[0] || "").substring(range.start.column);
  209. var l = lines.length - 1;
  210. if (range.end.row - range.start.row == l)
  211. lines[l] = lines[l].substring(0, range.end.column);
  212. }
  213. return lines;
  214. };
  215. // Deprecated methods retained for backwards compatibility.
  216. this.insertLines = function(row, lines) {
  217. console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead.");
  218. return this.insertFullLines(row, lines);
  219. };
  220. this.removeLines = function(firstRow, lastRow) {
  221. console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead.");
  222. return this.removeFullLines(firstRow, lastRow);
  223. };
  224. this.insertNewLine = function(position) {
  225. console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead.");
  226. return this.insertMergedLines(position, ["", ""]);
  227. };
  228. /**
  229. * Inserts a block of `text` at the indicated `position`.
  230. * @param {Object} position The position to start inserting at; it's an object that looks like `{ row: row, column: column}`
  231. * @param {String} text A chunk of text to insert
  232. * @returns {Object} The position ({row, column}) of the last line of `text`. If the length of `text` is 0, this function simply returns `position`.
  233. *
  234. **/
  235. this.insert = function(position, text) {
  236. // Only detect new lines if the document has no line break yet.
  237. if (this.getLength() <= 1)
  238. this.$detectNewLine(text);
  239. return this.insertMergedLines(position, this.$split(text));
  240. };
  241. /**
  242. * Inserts `text` into the `position` at the current row. This method also triggers the `"change"` event.
  243. *
  244. * This differs from the `insert` method in two ways:
  245. * 1. This does NOT handle newline characters (single-line text only).
  246. * 2. This is faster than the `insert` method for single-line text insertions.
  247. *
  248. * @param {Object} position The position to insert at; it's an object that looks like `{ row: row, column: column}`
  249. * @param {String} text A chunk of text
  250. * @returns {Object} Returns an object containing the final row and column, like this:
  251. * ```
  252. * {row: endRow, column: 0}
  253. * ```
  254. **/
  255. this.insertInLine = function(position, text) {
  256. var start = this.clippedPos(position.row, position.column);
  257. var end = this.pos(position.row, position.column + text.length);
  258. this.applyDelta({
  259. start: start,
  260. end: end,
  261. action: "insert",
  262. lines: [text]
  263. }, true);
  264. return this.clonePos(end);
  265. };
  266. this.clippedPos = function(row, column) {
  267. var length = this.getLength();
  268. if (row === undefined) {
  269. row = length;
  270. } else if (row < 0) {
  271. row = 0;
  272. } else if (row >= length) {
  273. row = length - 1;
  274. column = undefined;
  275. }
  276. var line = this.getLine(row);
  277. if (column == undefined)
  278. column = line.length;
  279. column = Math.min(Math.max(column, 0), line.length);
  280. return {row: row, column: column};
  281. };
  282. this.clonePos = function(pos) {
  283. return {row: pos.row, column: pos.column};
  284. };
  285. this.pos = function(row, column) {
  286. return {row: row, column: column};
  287. };
  288. this.$clipPosition = function(position) {
  289. var length = this.getLength();
  290. if (position.row >= length) {
  291. position.row = Math.max(0, length - 1);
  292. position.column = this.getLine(length - 1).length;
  293. } else {
  294. position.row = Math.max(0, position.row);
  295. position.column = Math.min(Math.max(position.column, 0), this.getLine(position.row).length);
  296. }
  297. return position;
  298. };
  299. /**
  300. * Fires whenever the document changes.
  301. *
  302. * Several methods trigger different `"change"` events. Below is a list of each action type, followed by each property that's also available:
  303. *
  304. * * `"insert"`
  305. * * `range`: the [[Range]] of the change within the document
  306. * * `lines`: the lines being added
  307. * * `"remove"`
  308. * * `range`: the [[Range]] of the change within the document
  309. * * `lines`: the lines being removed
  310. *
  311. * @event change
  312. * @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.
  313. *
  314. **/
  315. /**
  316. * 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.
  317. * @param {Number} row The index of the row to insert at
  318. * @param {Array} lines An array of strings
  319. * @returns {Object} Contains the final row and column, like this:
  320. * ```
  321. * {row: endRow, column: 0}
  322. * ```
  323. * If `lines` is empty, this function returns an object containing the current row, and column, like this:
  324. * ```
  325. * {row: row, column: 0}
  326. * ```
  327. *
  328. **/
  329. this.insertFullLines = function(row, lines) {
  330. // Clip to document.
  331. // Allow one past the document end.
  332. row = Math.min(Math.max(row, 0), this.getLength());
  333. // Calculate insertion point.
  334. var column = 0;
  335. if (row < this.getLength()) {
  336. // Insert before the specified row.
  337. lines = lines.concat([""]);
  338. column = 0;
  339. } else {
  340. // Insert after the last row in the document.
  341. lines = [""].concat(lines);
  342. row--;
  343. column = this.$lines[row].length;
  344. }
  345. // Insert.
  346. this.insertMergedLines({row: row, column: column}, lines);
  347. };
  348. /**
  349. * Inserts the elements in `lines` into the document, starting at the position index given by `row`. This method also triggers the `"change"` event.
  350. * @param {Number} row The index of the row to insert at
  351. * @param {Array} lines An array of strings
  352. * @returns {Object} Contains the final row and column, like this:
  353. * ```
  354. * {row: endRow, column: 0}
  355. * ```
  356. * If `lines` is empty, this function returns an object containing the current row, and column, like this:
  357. * ```
  358. * {row: row, column: 0}
  359. * ```
  360. *
  361. **/
  362. this.insertMergedLines = function(position, lines) {
  363. var start = this.clippedPos(position.row, position.column);
  364. var end = {
  365. row: start.row + lines.length - 1,
  366. column: (lines.length == 1 ? start.column : 0) + lines[lines.length - 1].length
  367. };
  368. this.applyDelta({
  369. start: start,
  370. end: end,
  371. action: "insert",
  372. lines: lines
  373. });
  374. return this.clonePos(end);
  375. };
  376. /**
  377. * Removes the `range` from the document.
  378. * @param {Range} range A specified Range to remove
  379. * @returns {Object} Returns the new `start` property of the range, which contains `startRow` and `startColumn`. If `range` is empty, this function returns the unmodified value of `range.start`.
  380. *
  381. **/
  382. this.remove = function(range) {
  383. var start = this.clippedPos(range.start.row, range.start.column);
  384. var end = this.clippedPos(range.end.row, range.end.column);
  385. this.applyDelta({
  386. start: start,
  387. end: end,
  388. action: "remove",
  389. lines: this.getLinesForRange({start: start, end: end})
  390. });
  391. return this.clonePos(start);
  392. };
  393. /**
  394. * Removes the specified columns from the `row`. This method also triggers a `"change"` event.
  395. * @param {Number} row The row to remove from
  396. * @param {Number} startColumn The column to start removing at
  397. * @param {Number} endColumn The column to stop removing at
  398. * @returns {Object} Returns an object containing `startRow` and `startColumn`, indicating the new row and column values.<br/>If `startColumn` is equal to `endColumn`, this function returns nothing.
  399. *
  400. **/
  401. this.removeInLine = function(row, startColumn, endColumn) {
  402. var start = this.clippedPos(row, startColumn);
  403. var end = this.clippedPos(row, endColumn);
  404. this.applyDelta({
  405. start: start,
  406. end: end,
  407. action: "remove",
  408. lines: this.getLinesForRange({start: start, end: end})
  409. }, true);
  410. return this.clonePos(start);
  411. };
  412. /**
  413. * Removes a range of full lines. This method also triggers the `"change"` event.
  414. * @param {Number} firstRow The first row to be removed
  415. * @param {Number} lastRow The last row to be removed
  416. * @returns {[String]} Returns all the removed lines.
  417. *
  418. **/
  419. this.removeFullLines = function(firstRow, lastRow) {
  420. // Clip to document.
  421. firstRow = Math.min(Math.max(0, firstRow), this.getLength() - 1);
  422. lastRow = Math.min(Math.max(0, lastRow ), this.getLength() - 1);
  423. // Calculate deletion range.
  424. // Delete the ending new line unless we're at the end of the document.
  425. // If we're at the end of the document, delete the starting new line.
  426. var deleteFirstNewLine = lastRow == this.getLength() - 1 && firstRow > 0;
  427. var deleteLastNewLine = lastRow < this.getLength() - 1;
  428. var startRow = ( deleteFirstNewLine ? firstRow - 1 : firstRow );
  429. var startCol = ( deleteFirstNewLine ? this.getLine(startRow).length : 0 );
  430. var endRow = ( deleteLastNewLine ? lastRow + 1 : lastRow );
  431. var endCol = ( deleteLastNewLine ? 0 : this.getLine(endRow).length );
  432. var range = new Range(startRow, startCol, endRow, endCol);
  433. // Store delelted lines with bounding newlines ommitted (maintains previous behavior).
  434. var deletedLines = this.$lines.slice(firstRow, lastRow + 1);
  435. this.applyDelta({
  436. start: range.start,
  437. end: range.end,
  438. action: "remove",
  439. lines: this.getLinesForRange(range)
  440. });
  441. // Return the deleted lines.
  442. return deletedLines;
  443. };
  444. /**
  445. * Removes the new line between `row` and the row immediately following it. This method also triggers the `"change"` event.
  446. * @param {Number} row The row to check
  447. *
  448. **/
  449. this.removeNewLine = function(row) {
  450. if (row < this.getLength() - 1 && row >= 0) {
  451. this.applyDelta({
  452. start: this.pos(row, this.getLine(row).length),
  453. end: this.pos(row + 1, 0),
  454. action: "remove",
  455. lines: ["", ""]
  456. });
  457. }
  458. };
  459. /**
  460. * Replaces a range in the document with the new `text`.
  461. * @param {Range} range A specified Range to replace
  462. * @param {String} text The new text to use as a replacement
  463. * @returns {Object} Returns an object containing the final row and column, like this:
  464. * {row: endRow, column: 0}
  465. * If the text and range are empty, this function returns an object containing the current `range.start` value.
  466. * If the text is the exact same as what currently exists, this function returns an object containing the current `range.end` value.
  467. *
  468. **/
  469. this.replace = function(range, text) {
  470. if (!(range instanceof Range))
  471. range = Range.fromPoints(range.start, range.end);
  472. if (text.length === 0 && range.isEmpty())
  473. return range.start;
  474. // Shortcut: If the text we want to insert is the same as it is already
  475. // in the document, we don't have to replace anything.
  476. if (text == this.getTextRange(range))
  477. return range.end;
  478. this.remove(range);
  479. var end;
  480. if (text) {
  481. end = this.insert(range.start, text);
  482. }
  483. else {
  484. end = range.start;
  485. }
  486. return end;
  487. };
  488. /**
  489. * Applies all changes in `deltas` to the document.
  490. * @param {Array} deltas An array of delta objects (can include "insert" and "remove" actions)
  491. **/
  492. this.applyDeltas = function(deltas) {
  493. for (var i=0; i<deltas.length; i++) {
  494. this.applyDelta(deltas[i]);
  495. }
  496. };
  497. /**
  498. * Reverts all changes in `deltas` from the document.
  499. * @param {Array} deltas An array of delta objects (can include "insert" and "remove" actions)
  500. **/
  501. this.revertDeltas = function(deltas) {
  502. for (var i=deltas.length-1; i>=0; i--) {
  503. this.revertDelta(deltas[i]);
  504. }
  505. };
  506. /**
  507. * Applies `delta` to the document.
  508. * @param {Object} delta A delta object (can include "insert" and "remove" actions)
  509. **/
  510. this.applyDelta = function(delta, doNotValidate) {
  511. var isInsert = delta.action == "insert";
  512. // An empty range is a NOOP.
  513. if (isInsert ? delta.lines.length <= 1 && !delta.lines[0]
  514. : !Range.comparePoints(delta.start, delta.end)) {
  515. return;
  516. }
  517. if (isInsert && delta.lines.length > 20000) {
  518. this.$splitAndapplyLargeDelta(delta, 20000);
  519. }
  520. else {
  521. applyDelta(this.$lines, delta, doNotValidate);
  522. this._signal("change", delta);
  523. }
  524. };
  525. this.$safeApplyDelta = function(delta) {
  526. var docLength = this.$lines.length;
  527. // verify that delta is in the document to prevent applyDelta from corrupting lines array
  528. if (
  529. delta.action == "remove" && delta.start.row < docLength && delta.end.row < docLength
  530. || delta.action == "insert" && delta.start.row <= docLength
  531. ) {
  532. this.applyDelta(delta);
  533. }
  534. };
  535. this.$splitAndapplyLargeDelta = function(delta, MAX) {
  536. // Split large insert deltas. This is necessary because:
  537. // 1. We need to support splicing delta lines into the document via $lines.splice.apply(...)
  538. // 2. fn.apply() doesn't work for a large number of params. The smallest threshold is on chrome 40 ~42000.
  539. // we use 20000 to leave some space for actual stack
  540. //
  541. // To Do: Ideally we'd be consistent and also split 'delete' deltas. We don't do this now, because delete
  542. // delta handling is too slow. If we make delete delta handling faster we can split all large deltas
  543. // as shown in https://gist.github.com/aldendaniels/8367109#file-document-snippet-js
  544. // If we do this, update validateDelta() to limit the number of lines in a delete delta.
  545. var lines = delta.lines;
  546. var l = lines.length - MAX + 1;
  547. var row = delta.start.row;
  548. var column = delta.start.column;
  549. for (var from = 0, to = 0; from < l; from = to) {
  550. to += MAX - 1;
  551. var chunk = lines.slice(from, to);
  552. chunk.push("");
  553. this.applyDelta({
  554. start: this.pos(row + from, column),
  555. end: this.pos(row + to, column = 0),
  556. action: delta.action,
  557. lines: chunk
  558. }, true);
  559. }
  560. // Update remaining delta.
  561. delta.lines = lines.slice(from);
  562. delta.start.row = row + from;
  563. delta.start.column = column;
  564. this.applyDelta(delta, true);
  565. };
  566. /**
  567. * Reverts `delta` from the document.
  568. * @param {Object} delta A delta object (can include "insert" and "remove" actions)
  569. **/
  570. this.revertDelta = function(delta) {
  571. this.$safeApplyDelta({
  572. start: this.clonePos(delta.start),
  573. end: this.clonePos(delta.end),
  574. action: (delta.action == "insert" ? "remove" : "insert"),
  575. lines: delta.lines.slice()
  576. });
  577. };
  578. /**
  579. * Converts an index position in a document to a `{row, column}` object.
  580. *
  581. * Index refers to the "absolute position" of a character in the document. For example:
  582. *
  583. * ```javascript
  584. * var x = 0; // 10 characters, plus one for newline
  585. * var y = -1;
  586. * ```
  587. *
  588. * Here, `y` is an index 15: 11 characters for the first row, and 5 characters until `y` in the second.
  589. *
  590. * @param {Number} index An index to convert
  591. * @param {Number} startRow=0 The row from which to start the conversion
  592. * @returns {Object} A `{row, column}` object of the `index` position
  593. */
  594. this.indexToPosition = function(index, startRow) {
  595. var lines = this.$lines || this.getAllLines();
  596. var newlineLength = this.getNewLineCharacter().length;
  597. for (var i = startRow || 0, l = lines.length; i < l; i++) {
  598. index -= lines[i].length + newlineLength;
  599. if (index < 0)
  600. return {row: i, column: index + lines[i].length + newlineLength};
  601. }
  602. return {row: l-1, column: index + lines[l-1].length + newlineLength};
  603. };
  604. /**
  605. * Converts the `{row, column}` position in a document to the character's index.
  606. *
  607. * Index refers to the "absolute position" of a character in the document. For example:
  608. *
  609. * ```javascript
  610. * var x = 0; // 10 characters, plus one for newline
  611. * var y = -1;
  612. * ```
  613. *
  614. * Here, `y` is an index 15: 11 characters for the first row, and 5 characters until `y` in the second.
  615. *
  616. * @param {Object} pos The `{row, column}` to convert
  617. * @param {Number} startRow=0 The row from which to start the conversion
  618. * @returns {Number} The index position in the document
  619. */
  620. this.positionToIndex = function(pos, startRow) {
  621. var lines = this.$lines || this.getAllLines();
  622. var newlineLength = this.getNewLineCharacter().length;
  623. var index = 0;
  624. var row = Math.min(pos.row, lines.length);
  625. for (var i = startRow || 0; i < row; ++i)
  626. index += lines[i].length + newlineLength;
  627. return index + pos.column;
  628. };
  629. }).call(Document.prototype);
  630. exports.Document = Document;
  631. });