document.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  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(require, exports, module) {
  31. "use strict";
  32. var oop = require("./lib/oop");
  33. var EventEmitter = require("./lib/event_emitter").EventEmitter;
  34. var Range = require("./range").Range;
  35. var Anchor = require("./anchor").Anchor;
  36. /**
  37. * Contains the text of the document. Document can be attached to several [[EditSession `EditSession`]]s.
  38. *
  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(text) {
  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 (text.length == 0) {
  54. this.$lines = [""];
  55. } else if (Array.isArray(text)) {
  56. this._insertLines(0, text);
  57. } else {
  58. this.insert({row: 0, column:0}, text);
  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();
  70. this.remove(new Range(0, 0, len, this.getLine(len-1).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. this.$detectNewLine = function(text) {
  106. var match = text.match(/^.*?(\r\n|\r|\n)/m);
  107. this.$autoNewLine = match ? match[1] : "\n";
  108. };
  109. /**
  110. * Returns the newline character that's being used, depending on the value of `newLineMode`.
  111. * @returns {String} If `newLineMode == windows`, `\r\n` is returned.
  112. * If `newLineMode == unix`, `\n` is returned.
  113. * If `newLineMode == auto`, the value of `autoNewLine` is returned.
  114. *
  115. **/
  116. this.getNewLineCharacter = function() {
  117. switch (this.$newLineMode) {
  118. case "windows":
  119. return "\r\n";
  120. case "unix":
  121. return "\n";
  122. default:
  123. return this.$autoNewLine;
  124. }
  125. };
  126. this.$autoNewLine = "\n";
  127. this.$newLineMode = "auto";
  128. /**
  129. * [Sets the new line mode.]{: #Document.setNewLineMode.desc}
  130. * @param {String} newLineMode [The newline mode to use; can be either `windows`, `unix`, or `auto`]{: #Document.setNewLineMode.param}
  131. *
  132. **/
  133. this.setNewLineMode = function(newLineMode) {
  134. if (this.$newLineMode === newLineMode)
  135. return;
  136. this.$newLineMode = newLineMode;
  137. };
  138. /**
  139. * [Returns the type of newlines being used; either `windows`, `unix`, or `auto`]{: #Document.getNewLineMode}
  140. * @returns {String}
  141. **/
  142. this.getNewLineMode = function() {
  143. return this.$newLineMode;
  144. };
  145. /**
  146. * Returns `true` if `text` is a newline character (either `\r\n`, `\r`, or `\n`).
  147. * @param {String} text The text to check
  148. *
  149. **/
  150. this.isNewLine = function(text) {
  151. return (text == "\r\n" || text == "\r" || text == "\n");
  152. };
  153. /**
  154. * Returns a verbatim copy of the given line as it is in the document
  155. * @param {Number} row The row index to retrieve
  156. *
  157. **/
  158. this.getLine = function(row) {
  159. return this.$lines[row] || "";
  160. };
  161. /**
  162. * Returns an array of strings of the rows between `firstRow` and `lastRow`. This function is inclusive of `lastRow`.
  163. * @param {Number} firstRow The first row index to retrieve
  164. * @param {Number} lastRow The final row index to retrieve
  165. *
  166. **/
  167. this.getLines = function(firstRow, lastRow) {
  168. return this.$lines.slice(firstRow, lastRow + 1);
  169. };
  170. /**
  171. * Returns all lines in the document as string array.
  172. **/
  173. this.getAllLines = function() {
  174. return this.getLines(0, this.getLength());
  175. };
  176. /**
  177. * Returns the number of rows in the document.
  178. **/
  179. this.getLength = function() {
  180. return this.$lines.length;
  181. };
  182. /**
  183. * [Given a range within the document, this function returns all the text within that range as a single string.]{: #Document.getTextRange.desc}
  184. * @param {Range} range The range to work with
  185. *
  186. * @returns {String}
  187. **/
  188. this.getTextRange = function(range) {
  189. if (range.start.row == range.end.row) {
  190. return this.getLine(range.start.row)
  191. .substring(range.start.column, range.end.column);
  192. }
  193. var lines = this.getLines(range.start.row, range.end.row);
  194. lines[0] = (lines[0] || "").substring(range.start.column);
  195. var l = lines.length - 1;
  196. if (range.end.row - range.start.row == l)
  197. lines[l] = lines[l].substring(0, range.end.column);
  198. return lines.join(this.getNewLineCharacter());
  199. };
  200. this.$clipPosition = function(position) {
  201. var length = this.getLength();
  202. if (position.row >= length) {
  203. position.row = Math.max(0, length - 1);
  204. position.column = this.getLine(length-1).length;
  205. } else if (position.row < 0)
  206. position.row = 0;
  207. return position;
  208. };
  209. /**
  210. * Inserts a block of `text` at the indicated `position`.
  211. * @param {Object} position The position to start inserting at; it's an object that looks like `{ row: row, column: column}`
  212. * @param {String} text A chunk of text to insert
  213. * @returns {Object} The position ({row, column}) of the last line of `text`. If the length of `text` is 0, this function simply returns `position`.
  214. *
  215. **/
  216. this.insert = function(position, text) {
  217. if (!text || text.length === 0)
  218. return position;
  219. position = this.$clipPosition(position);
  220. // only detect new lines if the document has no line break yet
  221. if (this.getLength() <= 1)
  222. this.$detectNewLine(text);
  223. var lines = this.$split(text);
  224. var firstLine = lines.splice(0, 1)[0];
  225. var lastLine = lines.length == 0 ? null : lines.splice(lines.length - 1, 1)[0];
  226. position = this.insertInLine(position, firstLine);
  227. if (lastLine !== null) {
  228. position = this.insertNewLine(position); // terminate first line
  229. position = this._insertLines(position.row, lines);
  230. position = this.insertInLine(position, lastLine || "");
  231. }
  232. return position;
  233. };
  234. /**
  235. * Fires whenever the document changes.
  236. *
  237. * Several methods trigger different `"change"` events. Below is a list of each action type, followed by each property that's also available:
  238. *
  239. * * `"insertLines"` (emitted by [[Document.insertLines]])
  240. * * `range`: the [[Range]] of the change within the document
  241. * * `lines`: the lines in the document that are changing
  242. * * `"insertText"` (emitted by [[Document.insertNewLine]])
  243. * * `range`: the [[Range]] of the change within the document
  244. * * `text`: the text that's being added
  245. * * `"removeLines"` (emitted by [[Document.insertLines]])
  246. * * `range`: the [[Range]] of the change within the document
  247. * * `lines`: the lines in the document that were removed
  248. * * `nl`: the new line character (as defined by [[Document.getNewLineCharacter]])
  249. * * `"removeText"` (emitted by [[Document.removeInLine]] and [[Document.removeNewLine]])
  250. * * `range`: the [[Range]] of the change within the document
  251. * * `text`: the text that's being removed
  252. *
  253. * @event change
  254. * @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.
  255. *
  256. **/
  257. /**
  258. * Inserts the elements in `lines` into the document, starting at the row index given by `row`. This method also triggers the `'change'` event.
  259. * @param {Number} row The index of the row to insert at
  260. * @param {Array} lines An array of strings
  261. * @returns {Object} Contains the final row and column, like this:
  262. * ```
  263. * {row: endRow, column: 0}
  264. * ```
  265. * If `lines` is empty, this function returns an object containing the current row, and column, like this:
  266. * ```
  267. * {row: row, column: 0}
  268. * ```
  269. *
  270. **/
  271. this.insertLines = function(row, lines) {
  272. if (row >= this.getLength())
  273. return this.insert({row: row, column: 0}, "\n" + lines.join("\n"));
  274. return this._insertLines(Math.max(row, 0), lines);
  275. };
  276. this._insertLines = function(row, lines) {
  277. if (lines.length == 0)
  278. return {row: row, column: 0};
  279. // apply doesn't work for big arrays (smallest threshold is on safari 0xFFFF)
  280. // to circumvent that we have to break huge inserts into smaller chunks here
  281. if (lines.length > 0xFFFF) {
  282. var end = this._insertLines(row, lines.slice(0xFFFF));
  283. lines = lines.slice(0, 0xFFFF);
  284. }
  285. var args = [row, 0];
  286. args.push.apply(args, lines);
  287. this.$lines.splice.apply(this.$lines, args);
  288. var range = new Range(row, 0, row + lines.length, 0);
  289. var delta = {
  290. action: "insertLines",
  291. range: range,
  292. lines: lines
  293. };
  294. this._emit("change", { data: delta });
  295. return end || range.end;
  296. };
  297. /**
  298. * Inserts a new line into the document at the current row's `position`. This method also triggers the `'change'` event.
  299. * @param {Object} position The position to insert at
  300. * @returns {Object} Returns an object containing the final row and column, like this:<br/>
  301. * ```
  302. * {row: endRow, column: 0}
  303. * ```
  304. *
  305. **/
  306. this.insertNewLine = function(position) {
  307. position = this.$clipPosition(position);
  308. var line = this.$lines[position.row] || "";
  309. this.$lines[position.row] = line.substring(0, position.column);
  310. this.$lines.splice(position.row + 1, 0, line.substring(position.column, line.length));
  311. var end = {
  312. row : position.row + 1,
  313. column : 0
  314. };
  315. var delta = {
  316. action: "insertText",
  317. range: Range.fromPoints(position, end),
  318. text: this.getNewLineCharacter()
  319. };
  320. this._emit("change", { data: delta });
  321. return end;
  322. };
  323. /**
  324. * Inserts `text` into the `position` at the current row. This method also triggers the `'change'` event.
  325. * @param {Object} position The position to insert at; it's an object that looks like `{ row: row, column: column}`
  326. * @param {String} text A chunk of text
  327. * @returns {Object} Returns an object containing the final row and column, like this:
  328. * ```
  329. * {row: endRow, column: 0}
  330. * ```
  331. *
  332. **/
  333. this.insertInLine = function(position, text) {
  334. if (text.length == 0)
  335. return position;
  336. var line = this.$lines[position.row] || "";
  337. this.$lines[position.row] = line.substring(0, position.column) + text
  338. + line.substring(position.column);
  339. var end = {
  340. row : position.row,
  341. column : position.column + text.length
  342. };
  343. var delta = {
  344. action: "insertText",
  345. range: Range.fromPoints(position, end),
  346. text: text
  347. };
  348. this._emit("change", { data: delta });
  349. return end;
  350. };
  351. /**
  352. * Removes the `range` from the document.
  353. * @param {Range} range A specified Range to remove
  354. * @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`.
  355. *
  356. **/
  357. this.remove = function(range) {
  358. if (!range instanceof Range)
  359. range = Range.fromPoints(range.start, range.end);
  360. // clip to document
  361. range.start = this.$clipPosition(range.start);
  362. range.end = this.$clipPosition(range.end);
  363. if (range.isEmpty())
  364. return range.start;
  365. var firstRow = range.start.row;
  366. var lastRow = range.end.row;
  367. if (range.isMultiLine()) {
  368. var firstFullRow = range.start.column == 0 ? firstRow : firstRow + 1;
  369. var lastFullRow = lastRow - 1;
  370. if (range.end.column > 0)
  371. this.removeInLine(lastRow, 0, range.end.column);
  372. if (lastFullRow >= firstFullRow)
  373. this._removeLines(firstFullRow, lastFullRow);
  374. if (firstFullRow != firstRow) {
  375. this.removeInLine(firstRow, range.start.column, this.getLine(firstRow).length);
  376. this.removeNewLine(range.start.row);
  377. }
  378. }
  379. else {
  380. this.removeInLine(firstRow, range.start.column, range.end.column);
  381. }
  382. return range.start;
  383. };
  384. /**
  385. * Removes the specified columns from the `row`. This method also triggers the `'change'` event.
  386. * @param {Number} row The row to remove from
  387. * @param {Number} startColumn The column to start removing at
  388. * @param {Number} endColumn The column to stop removing at
  389. * @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.
  390. *
  391. **/
  392. this.removeInLine = function(row, startColumn, endColumn) {
  393. if (startColumn == endColumn)
  394. return;
  395. var range = new Range(row, startColumn, row, endColumn);
  396. var line = this.getLine(row);
  397. var removed = line.substring(startColumn, endColumn);
  398. var newLine = line.substring(0, startColumn) + line.substring(endColumn, line.length);
  399. this.$lines.splice(row, 1, newLine);
  400. var delta = {
  401. action: "removeText",
  402. range: range,
  403. text: removed
  404. };
  405. this._emit("change", { data: delta });
  406. return range.start;
  407. };
  408. /**
  409. * Removes a range of full lines. This method also triggers the `'change'` event.
  410. * @param {Number} firstRow The first row to be removed
  411. * @param {Number} lastRow The last row to be removed
  412. * @returns {[String]} Returns all the removed lines.
  413. *
  414. **/
  415. this.removeLines = function(firstRow, lastRow) {
  416. if (firstRow < 0 || lastRow >= this.getLength())
  417. return this.remove(new Range(firstRow, 0, lastRow + 1, 0));
  418. return this._removeLines(firstRow, lastRow);
  419. };
  420. this._removeLines = function(firstRow, lastRow) {
  421. var range = new Range(firstRow, 0, lastRow + 1, 0);
  422. var removed = this.$lines.splice(firstRow, lastRow - firstRow + 1);
  423. var delta = {
  424. action: "removeLines",
  425. range: range,
  426. nl: this.getNewLineCharacter(),
  427. lines: removed
  428. };
  429. this._emit("change", { data: delta });
  430. return removed;
  431. };
  432. /**
  433. * Removes the new line between `row` and the row immediately following it. This method also triggers the `'change'` event.
  434. * @param {Number} row The row to check
  435. *
  436. **/
  437. this.removeNewLine = function(row) {
  438. var firstLine = this.getLine(row);
  439. var secondLine = this.getLine(row+1);
  440. var range = new Range(row, firstLine.length, row+1, 0);
  441. var line = firstLine + secondLine;
  442. this.$lines.splice(row, 2, line);
  443. var delta = {
  444. action: "removeText",
  445. range: range,
  446. text: this.getNewLineCharacter()
  447. };
  448. this._emit("change", { data: delta });
  449. };
  450. /**
  451. * Replaces a range in the document with the new `text`.
  452. * @param {Range} range A specified Range to replace
  453. * @param {String} text The new text to use as a replacement
  454. * @returns {Object} Returns an object containing the final row and column, like this:
  455. * {row: endRow, column: 0}
  456. * If the text and range are empty, this function returns an object containing the current `range.start` value.
  457. * If the text is the exact same as what currently exists, this function returns an object containing the current `range.end` value.
  458. *
  459. **/
  460. this.replace = function(range, text) {
  461. if (!range instanceof Range)
  462. range = Range.fromPoints(range.start, range.end);
  463. if (text.length == 0 && range.isEmpty())
  464. return range.start;
  465. // Shortcut: If the text we want to insert is the same as it is already
  466. // in the document, we don't have to replace anything.
  467. if (text == this.getTextRange(range))
  468. return range.end;
  469. this.remove(range);
  470. if (text) {
  471. var end = this.insert(range.start, text);
  472. }
  473. else {
  474. end = range.start;
  475. }
  476. return end;
  477. };
  478. /**
  479. * Applies all the changes previously accumulated. These can be either `'includeText'`, `'insertLines'`, `'removeText'`, and `'removeLines'`.
  480. **/
  481. this.applyDeltas = function(deltas) {
  482. for (var i=0; i<deltas.length; i++) {
  483. var delta = deltas[i];
  484. var range = Range.fromPoints(delta.range.start, delta.range.end);
  485. if (delta.action == "insertLines")
  486. this.insertLines(range.start.row, delta.lines);
  487. else if (delta.action == "insertText")
  488. this.insert(range.start, delta.text);
  489. else if (delta.action == "removeLines")
  490. this._removeLines(range.start.row, range.end.row - 1);
  491. else if (delta.action == "removeText")
  492. this.remove(range);
  493. }
  494. };
  495. /**
  496. * Reverts any changes previously applied. These can be either `'includeText'`, `'insertLines'`, `'removeText'`, and `'removeLines'`.
  497. **/
  498. this.revertDeltas = function(deltas) {
  499. for (var i=deltas.length-1; i>=0; i--) {
  500. var delta = deltas[i];
  501. var range = Range.fromPoints(delta.range.start, delta.range.end);
  502. if (delta.action == "insertLines")
  503. this._removeLines(range.start.row, range.end.row - 1);
  504. else if (delta.action == "insertText")
  505. this.remove(range);
  506. else if (delta.action == "removeLines")
  507. this._insertLines(range.start.row, delta.lines);
  508. else if (delta.action == "removeText")
  509. this.insert(range.start, delta.text);
  510. }
  511. };
  512. /**
  513. * Converts an index position in a document to a `{row, column}` object.
  514. *
  515. * Index refers to the "absolute position" of a character in the document. For example:
  516. *
  517. * ```javascript
  518. * var x = 0; // 10 characters, plus one for newline
  519. * var y = -1;
  520. * ```
  521. *
  522. * Here, `y` is an index 15: 11 characters for the first row, and 5 characters until `y` in the second.
  523. *
  524. * @param {Number} index An index to convert
  525. * @param {Number} startRow=0 The row from which to start the conversion
  526. * @returns {Object} A `{row, column}` object of the `index` position
  527. */
  528. this.indexToPosition = function(index, startRow) {
  529. var lines = this.$lines || this.getAllLines();
  530. var newlineLength = this.getNewLineCharacter().length;
  531. for (var i = startRow || 0, l = lines.length; i < l; i++) {
  532. index -= lines[i].length + newlineLength;
  533. if (index < 0)
  534. return {row: i, column: index + lines[i].length + newlineLength};
  535. }
  536. return {row: l-1, column: lines[l-1].length};
  537. };
  538. /**
  539. * Converts the `{row, column}` position in a document to the character's index.
  540. *
  541. * Index refers to the "absolute position" of a character in the document. For example:
  542. *
  543. * ```javascript
  544. * var x = 0; // 10 characters, plus one for newline
  545. * var y = -1;
  546. * ```
  547. *
  548. * Here, `y` is an index 15: 11 characters for the first row, and 5 characters until `y` in the second.
  549. *
  550. * @param {Object} pos The `{row, column}` to convert
  551. * @param {Number} startRow=0 The row from which to start the conversion
  552. * @returns {Number} The index position in the document
  553. */
  554. this.positionToIndex = function(pos, startRow) {
  555. var lines = this.$lines || this.getAllLines();
  556. var newlineLength = this.getNewLineCharacter().length;
  557. var index = 0;
  558. var row = Math.min(pos.row, lines.length);
  559. for (var i = startRow || 0; i < row; ++i)
  560. index += lines[i].length + newlineLength;
  561. return index + pos.column;
  562. };
  563. }).call(Document.prototype);
  564. exports.Document = Document;
  565. });