ot.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. define(["util"], function (util) {
  5. var ot = util.Module("ot");
  6. var assert = util.assert;
  7. var StringSet = util.Class({
  8. /* Set that only supports string items */
  9. constructor: function () {
  10. this._items = {};
  11. this._count = 0;
  12. },
  13. contains: function (k) {
  14. assert(typeof k == "string");
  15. return this._items.hasOwnProperty(k);
  16. },
  17. add: function (k) {
  18. assert(typeof k == "string");
  19. if (this.contains(k)) {
  20. return;
  21. }
  22. this._items[k] = null;
  23. this._count++;
  24. },
  25. remove: function (k) {
  26. assert(typeof k == "string");
  27. if (! this.contains(k)) {
  28. return;
  29. }
  30. delete this._items[k];
  31. this._count++;
  32. },
  33. isEmpty: function () {
  34. return ! this._count;
  35. }
  36. });
  37. var Queue = util.Class({
  38. constructor: function (size) {
  39. this._q = [];
  40. this._size = size;
  41. this._deleted = 0;
  42. },
  43. _trim: function () {
  44. if (this._size) {
  45. if (this._q.length > this._size) {
  46. this._q.splice(0, this._q.length - this._size);
  47. this._deleted += this._q.length - this._size;
  48. }
  49. }
  50. },
  51. push: function (item) {
  52. this._q.push(item);
  53. this._trim();
  54. },
  55. last: function () {
  56. return this._q[this._q.length-1];
  57. },
  58. walkBack: function (callback, context) {
  59. var result = true;
  60. for (var i=this._q.length-1; i >= 0; i--) {
  61. var item = this._q[i];
  62. result = callback.call(context, item, i + this._deleted);
  63. if (result === false) {
  64. return result;
  65. } else if (! result) {
  66. result = true;
  67. }
  68. }
  69. return result;
  70. },
  71. walkForward: function (index, callback, context) {
  72. var result = true;
  73. for (var i=index; i<this._q.length; i++) {
  74. var item = this._q[i-this._deleted];
  75. result = callback.call(context, item, i);
  76. if (result === false) {
  77. return result;
  78. } else if (! result) {
  79. result = true;
  80. }
  81. }
  82. return result;
  83. },
  84. insert: function (index, item) {
  85. this._q.splice(index-this._deleted, 0, item);
  86. }
  87. });
  88. var Change = util.Class({
  89. constructor: function (version, clientId, delta, known, outOfOrder) {
  90. this.version = version;
  91. this.clientId = clientId;
  92. this.delta = delta;
  93. this.known = known;
  94. this.outOfOrder = !! outOfOrder;
  95. assert(typeof version == "number" && typeof clientId == "string",
  96. "Bad Change():", version, clientId);
  97. },
  98. toString: function () {
  99. var s = "[Change " + this.version + "." + this.clientId + ": ";
  100. s += this.delta + " ";
  101. if (this.outOfOrder) {
  102. s += "(out of order) ";
  103. }
  104. var cids = [];
  105. for (var a in this.known) {
  106. if (this.known.hasOwnProperty(a)) {
  107. cids.push(a);
  108. }
  109. }
  110. cids.sort();
  111. s += "{";
  112. if (! cids.length) {
  113. s += "nothing known";
  114. } else {
  115. cids.forEach(function (a, index) {
  116. if (index) {
  117. s += ";";
  118. }
  119. s += a + ":" + this.known[a];
  120. }, this);
  121. }
  122. return s + "}]";
  123. },
  124. clone: function () {
  125. return Change(this.version, this.clientId, this.delta.clone(), util.extend(this.known), this.outOfOrder);
  126. },
  127. isBefore: function (otherChange) {
  128. assert(otherChange !== this, "Tried to compare a change to itself", this);
  129. return otherChange.version > this.version ||
  130. (otherChange.version == this.version && otherChange.clientId > this.clientId);
  131. },
  132. knowsAboutAll: function (versions) {
  133. for (var clientId in versions) {
  134. if (! versions.hasOwnProperty(clientId)) {
  135. continue;
  136. }
  137. if (! versions[clientId]) {
  138. continue;
  139. }
  140. if ((! this.known[clientId]) || this.known[clientId] < versions[clientId]) {
  141. return false;
  142. }
  143. }
  144. return true;
  145. },
  146. knowsAboutChange: function (change) {
  147. return change.clientId == this.clientId ||
  148. (this.known[change.clientId] && this.known[change.clientId] >= change.version);
  149. },
  150. knowsAboutVersion: function (version, clientId) {
  151. if ((! version) || clientId == this.clientId) {
  152. return true;
  153. }
  154. return this.known[clientId] && this.known[clientId] >= version;
  155. },
  156. maybeMissingChanges: function (mostRecentVersion, clientId) {
  157. if (! mostRecentVersion) {
  158. // No actual changes for clientId exist
  159. return false;
  160. }
  161. if (! this.known[clientId]) {
  162. // We don't even know about clientId, so we are definitely missing something
  163. return true;
  164. }
  165. if (this.known[clientId] >= mostRecentVersion) {
  166. // We know about all versions through mostRecentVersion
  167. return false;
  168. }
  169. if ((clientId > this.clientId && this.known[clientId] >= this.version-1) ||
  170. (clientId < this.clientId && this.known[clientId] == this.version)) {
  171. // We know about all versions from clientId that could exist before this
  172. // version
  173. return false;
  174. }
  175. // We may or may not be missing something
  176. return true;
  177. }
  178. });
  179. /* SimpleHistory synchronizes peers by relying on the server to serialize
  180. * the order of all updates. Each client maintains a queue of patches
  181. * which have not yet been 'committed' (by being echoed back from the
  182. * server). The client is responsible for transposing its own queue
  183. * if 'earlier' patches are heard from the server.
  184. *
  185. * Let's say that A's edit "1" and B's edit "2" occur and get put in
  186. * their respective SimpleHistory queues. The server happens to
  187. * handle 1 first, then 2, so those are the order that all peers
  188. * (both A and B) see the messages.
  189. *
  190. * A sees 1, and has 1 on its queue, so everything's fine. It
  191. * updates the 'committed' text to match its current text and drops
  192. * the patch from its queue. It then sees 2, but the basis number
  193. * for 2 no longer matches the committed basis, so it throws it
  194. * away.
  195. *
  196. * B sees 1, and has 2 on its queue. It does the OT transpose thing,
  197. * updating the committed text to include 1 and the 'current' text
  198. * to include 1+2. It updates its queue with the newly transposed
  199. * version of 2 (call it 2prime) and updates 2prime's basis
  200. * number. It them resends 2prime to the server. It then receives 2
  201. * (the original) but the basis number no longer matches the
  202. * committed basis, so it throws it away.
  203. *
  204. * Now the server sees 2prime and rebroadcasts it to both A and B.
  205. *
  206. * A is seeing it for the first time, and the basis number matches,
  207. * so it applies it to the current and committed text.
  208. *
  209. * B sees that 2prime matches what's on the start of its queue,
  210. * shifts it off, and updates the committed text to match the
  211. * current text.
  212. *
  213. * Note that no one tries to keep an entire history of changes,
  214. * which is the main difference with ot.History. Everyone applies
  215. * the same patches in the same order.
  216. */
  217. ot.SimpleHistory = util.Class({
  218. constructor: function(clientId, initState, initBasis) {
  219. this.clientId = clientId;
  220. this.committed = initState;
  221. this.current = initState;
  222. this.basis = initBasis;
  223. this.queue = [];
  224. this.deltaId = 1;
  225. this.selection = null;
  226. },
  227. // Use a fake change to represent the selection.
  228. // (This is the only bit that hard codes ot.TextReplace as the delta
  229. // representation; override this in a subclass (or don't set the
  230. // selection) if you are using a different delta representation.
  231. setSelection: function(selection) {
  232. if (selection) {
  233. this.selection = ot.TextReplace(selection[0],
  234. selection[1] - selection[0], '@');
  235. } else {
  236. this.selection = null;
  237. }
  238. },
  239. // Decode the fake change to reconstruct the updated selection.
  240. getSelection: function() {
  241. if (! this.selection) {
  242. return null;
  243. }
  244. return [this.selection.start, this.selection.start + this.selection.del];
  245. },
  246. // Add this delta to this client's queue.
  247. add: function(delta) {
  248. var change = {
  249. id: this.clientId + '.' + (this.deltaId++),
  250. delta: delta
  251. };
  252. if (! this.queue.length) {
  253. change.basis = this.basis;
  254. }
  255. this.queue.push(change);
  256. this.current = delta.apply(this.current);
  257. return !!change.basis;
  258. },
  259. // Apply a delta received from the server.
  260. // Return true iff the current text changed as a result.
  261. commit: function(change) {
  262. // ignore it if the basis doesn't match (this patch doesn't apply)
  263. // if so, this delta is out of order; we expect the original client
  264. // to retransmit an updated delta.
  265. if (change.basis !== this.basis) {
  266. return false; // 'current' text did not change
  267. }
  268. // is this the first thing on the queue?
  269. if (this.queue.length && this.queue[0].id === change.id) {
  270. assert(change.basis === this.queue[0].basis);
  271. // good, apply this to commit state & remove it from queue
  272. this.committed = this.queue.shift().delta.apply(this.committed);
  273. this.basis++;
  274. if (this.queue.length) {
  275. this.queue[0].basis = this.basis;
  276. }
  277. return false; // 'current' text did not change
  278. }
  279. // Transpose all bits on the queue to put this patch first.
  280. var inserted = change.delta;
  281. this.queue = this.queue.map(function(qchange) {
  282. var tt = qchange.delta.transpose(inserted);
  283. inserted = tt[1];
  284. return {
  285. id: qchange.id,
  286. delta: tt[0]
  287. };
  288. });
  289. if (this.selection) {
  290. // update the selection!
  291. this.selection = this.selection.transpose(inserted)[0];
  292. }
  293. this.committed = change.delta.apply(this.committed);
  294. this.basis++;
  295. if (this.queue.length) {
  296. this.queue[0].basis = this.basis;
  297. }
  298. // Update current by replaying queued changes starting from 'committed'
  299. this.current = this.committed;
  300. this.queue.forEach(function(qchange) {
  301. this.current = qchange.delta.apply(this.current);
  302. }.bind(this));
  303. return true; // The 'current' text changed.
  304. },
  305. // Return the next change to transmit to the server, or null if there
  306. // isn't one.
  307. getNextToSend: function() {
  308. var qchange = this.queue[0];
  309. if (! qchange) {
  310. /* nothing to send */
  311. return null;
  312. }
  313. if (qchange.sent) {
  314. /* already sent */
  315. return null;
  316. }
  317. assert(qchange.basis);
  318. qchange.sent = true;
  319. return qchange;
  320. }
  321. });
  322. ot.History = util.Class({
  323. constructor: function (clientId, initState) {
  324. this._history = Queue();
  325. this._history.push({
  326. clientId: "init", state: initState
  327. });
  328. this.clientId = clientId;
  329. this.known = {};
  330. this.mostRecentLocalChange = null;
  331. },
  332. add: function (change) {
  333. // Simplest cast, it is our change:
  334. if (change.clientId == this.clientId) {
  335. this._history.push(change);
  336. this.mostRecentLocalChange = change.version;
  337. return change.delta;
  338. }
  339. assert((! this.known[change.clientId]) || this.known[change.clientId] < change.version,
  340. "Got a change", change, "that appears older (or same as) a known change", this.known[change.clientId]);
  341. // Second simplest case, we get a change that we can add to our
  342. // history without modification:
  343. var last = this._history.last();
  344. if ((last.clientId == "init" || last.isBefore(change)) &&
  345. change.knowsAboutAll(this.known) &&
  346. change.knowsAboutVersion(this.mostRecentLocalChange, this.clientId)) {
  347. this._history.push(change);
  348. this.known[change.clientId] = change.version;
  349. return change.delta;
  350. }
  351. // We must do work!
  352. this.logHistory("//");
  353. // First we check if we need to modify this change because we
  354. // know about changes that it should know about (changes that
  355. // preceed it that are in our local history).
  356. var clientsToCheck = StringSet();
  357. for (var clientId in this.known) {
  358. if (! this.known.hasOwnProperty(clientId)) {
  359. continue;
  360. }
  361. if (change.maybeMissingChanges(this.known[clientId], clientId)) {
  362. clientsToCheck.add(clientId);
  363. }
  364. }
  365. if (change.maybeMissingChanges(this.mostRecentLocalChange, this.clientId)) {
  366. clientsToCheck.add(this.clientId);
  367. }
  368. if (! clientsToCheck.isEmpty()) {
  369. var indexToCheckFrom = null;
  370. this._history.walkBack(function (c, index) {
  371. indexToCheckFrom = index;
  372. if (c.clientId == "init") {
  373. return false;
  374. }
  375. if (clientsToCheck.contains(c.clientId) &&
  376. ! change.maybeMissingChanges(c.version, c.clientId)) {
  377. clientsToCheck.remove(c.clientId);
  378. if (clientsToCheck.isEmpty()) {
  379. return false;
  380. }
  381. }
  382. return true;
  383. }, this);
  384. this._history.walkForward(indexToCheckFrom, function (c, index) {
  385. if (c.clientId == "init") {
  386. return true;
  387. }
  388. if (change.isBefore(c)) {
  389. return false;
  390. }
  391. if (! change.knowsAboutChange(c)) {
  392. var presentDelta = this.promoteDelta(c.delta, index, change);
  393. if (! presentDelta.equals(c.delta)) {
  394. //console.log("->rebase delta rewrite", presentDelta+"");
  395. }
  396. this.logChange("->rebase", change, function () {
  397. var result = change.delta.transpose(presentDelta);
  398. change.delta = result[0];
  399. change.known[c.clientId] = c.version;
  400. }, "with:", c);
  401. }
  402. return true;
  403. }, this);
  404. }
  405. // Next we insert the change into its proper location
  406. var indexToInsert = null;
  407. this._history.walkBack(function (c, index) {
  408. if (c.clientId == "init" || c.isBefore(change)) {
  409. indexToInsert = index+1;
  410. return false;
  411. }
  412. return true;
  413. }, this);
  414. assert(indexToInsert);
  415. this._history.insert(indexToInsert, change);
  416. // Now we fix up any forward changes
  417. var fixupDelta = change.delta;
  418. this._history.walkForward(indexToInsert+1, function (c, index) {
  419. if (! c.knowsAboutChange(change)) {
  420. var origChange = c.clone();
  421. this.logChange("^^fix", c, function () {
  422. var fixupResult = c.delta.transpose(fixupDelta);
  423. console.log(" ^^real");
  424. var result = c.delta.transpose(fixupDelta);
  425. c.delta = result[0];
  426. c.known[change.clientId] = change.version;
  427. fixupDelta = fixupResult[1];
  428. }, "clone:", change.delta+"");
  429. console.log("(trans)", fixupDelta+"");
  430. assert(c.knowsAboutChange(change));
  431. }
  432. }, this);
  433. // Finally we return the transformed delta that represents
  434. // changes that should be made to the state:
  435. this.logHistory("!!");
  436. return fixupDelta;
  437. },
  438. promoteDelta: function (delta, deltaIndex, untilChange) {
  439. this._history.walkForward(deltaIndex+1, function (c, index) {
  440. if (untilChange.isBefore(c)) {
  441. return false;
  442. }
  443. // FIXME: not sure if this clientId check here is right. Maybe
  444. // if untilChange.knowsAbout(c)?
  445. if (untilChange.knowsAboutChange(c)) {
  446. var result = c.delta.transpose(delta);
  447. delta = result[1];
  448. }
  449. return true;
  450. });
  451. return delta;
  452. },
  453. logHistory: function (prefix) {
  454. prefix = prefix || "";
  455. var postfix = Array.prototype.slice.call(arguments, 1);
  456. console.log.apply(console, [prefix + "history", this.clientId, ":"].concat(postfix));
  457. console.log(prefix + " state:", JSON.stringify(this.getStateSafe()));
  458. var hstate;
  459. this._history.walkForward(0, function (c, index) {
  460. if (! index) {
  461. assert(c.clientId == "init");
  462. console.log(prefix + " init:", JSON.stringify(c.state));
  463. hstate = c.state;
  464. } else {
  465. try {
  466. hstate = c.delta.apply(hstate);
  467. } catch (e) {
  468. hstate = "Error: " + e;
  469. }
  470. console.log(prefix + " ", index, c+"", JSON.stringify(hstate));
  471. }
  472. });
  473. },
  474. logChange: function (prefix, change, callback) {
  475. prefix = prefix || "before";
  476. var postfix = Array.prototype.slice.call(arguments, 3);
  477. console.log.apply(
  478. console,
  479. [prefix, this.clientId, ":", change+""].concat(postfix).concat([JSON.stringify(this.getStateSafe(true))]));
  480. try {
  481. callback();
  482. } finally {
  483. console.log(prefix + " after:", change+"", JSON.stringify(this.getStateSafe()));
  484. }
  485. },
  486. addDelta: function (delta) {
  487. var version = this._createVersion();
  488. var change = Change(version, this.clientId, delta, util.extend(this.knownVersions));
  489. this.add(change);
  490. return change;
  491. },
  492. _createVersion: function () {
  493. var max = 1;
  494. for (var id in this.knownVersions) {
  495. max = Math.max(max, this.knownVersions[id]);
  496. }
  497. max = Math.max(max, this.mostRecentLocalChange);
  498. return max+1;
  499. },
  500. fault: function (change) {
  501. throw new Error('Fault');
  502. },
  503. getState: function () {
  504. var state;
  505. this._history.walkForward(0, function (c) {
  506. if (c.clientId == "init") {
  507. // Initialization, has the state
  508. state = c.state;
  509. } else {
  510. state = c.delta.apply(state);
  511. }
  512. }, this);
  513. return state;
  514. },
  515. getStateSafe: function () {
  516. try {
  517. return this.getState();
  518. } catch (e) {
  519. return 'Error: ' + e;
  520. }
  521. }
  522. });
  523. ot.TextReplace = util.Class({
  524. constructor: function (start, del, text) {
  525. assert(typeof start == "number" && typeof del == "number" && typeof text == "string", start, del, text);
  526. assert(start >=0 && del >= 0, start, del);
  527. this.start = start;
  528. this.del = del;
  529. this.text = text;
  530. },
  531. toString: function () {
  532. if (this.empty()) {
  533. return '[no-op]';
  534. }
  535. if (! this.del) {
  536. return '[insert ' + JSON.stringify(this.text) + ' @' + this.start + ']';
  537. } else if (! this.text) {
  538. return '[delete ' + this.del + ' chars @' + this.start + ']';
  539. } else {
  540. return '[replace ' + this.del + ' chars with ' + JSON.stringify(this.text) + ' @' + this.start + ']';
  541. }
  542. },
  543. equals: function (other) {
  544. return other.constructor === this.constructor &&
  545. other.del === this.del &&
  546. other.start === this.start &&
  547. other.text === this.text;
  548. },
  549. clone: function (start, del, text) {
  550. if (start === undefined) {
  551. start = this.start;
  552. }
  553. if (del === undefined) {
  554. del = this.del;
  555. }
  556. if (text === undefined) {
  557. text = this.text;
  558. }
  559. return ot.TextReplace(start, del, text);
  560. },
  561. empty: function () {
  562. return (! this.del) && (! this.text);
  563. },
  564. apply: function (text) {
  565. if (this.empty()) {
  566. return text;
  567. }
  568. if (this.start > text.length) {
  569. console.trace();
  570. throw new util.AssertionError("Start after end of text (" + JSON.stringify(text) + "/" + text.length + "): " + this);
  571. }
  572. if (this.start + this.del > text.length) {
  573. throw new util.AssertionError("Start+del after end of text (" + JSON.stringify(text) + "/" + text.length + "): " + this);
  574. }
  575. return text.substr(0, this.start) + this.text + text.substr(this.start+this.del);
  576. },
  577. transpose: function (delta) {
  578. /* Transform this delta as though the other delta had come before it.
  579. Returns a [new_version_of_this, transformed_delta], where transformed_delta
  580. satisfies:
  581. result1 = new_version_of_this.apply(delta.apply(text));
  582. result2 = transformed_delta.apply(this.apply(text));
  583. assert(result1 == result2);
  584. Does not modify this object.
  585. */
  586. var overlap;
  587. assert(delta instanceof ot.TextReplace, "Transposing with non-TextReplace:", delta);
  588. if (this.empty()) {
  589. //console.log(" =this is empty");
  590. return [this.clone(), delta.clone()];
  591. }
  592. if (delta.empty()) {
  593. //console.log(" =other is empty");
  594. return [this.clone(), delta.clone()];
  595. }
  596. if (delta.before(this)) {
  597. //console.log(" =this after other");
  598. return [this.clone(this.start + delta.text.length - delta.del),
  599. delta.clone()];
  600. } else if (this.before(delta)) {
  601. //console.log(" =this before other");
  602. return [this.clone(), delta.clone(delta.start + this.text.length - this.del)];
  603. } else if (delta.sameRange(this)) {
  604. //console.log(" =same range");
  605. return [this.clone(this.start+delta.text.length, 0),
  606. delta.clone(undefined, 0)];
  607. } else if (delta.contains(this)) {
  608. //console.log(" =other contains this");
  609. return [this.clone(delta.start+delta.text.length, 0, this.text),
  610. delta.clone(undefined, delta.del - this.del + this.text.length, delta.text + this.text)];
  611. } else if (this.contains(delta)) {
  612. //console.log(" =this contains other");
  613. return [this.clone(undefined, this.del - delta.del + delta.text.length, delta.text + this.text),
  614. delta.clone(this.start, 0, delta.text)];
  615. } else if (this.overlapsStart(delta)) {
  616. //console.log(" =this overlaps start of other");
  617. overlap = this.start + this.del - delta.start;
  618. return [this.clone(undefined, this.del - overlap),
  619. delta.clone(this.start + this.text.length, delta.del - overlap)];
  620. } else {
  621. //console.log(" =this overlaps end of other");
  622. assert(delta.overlapsStart(this), delta+"", "does not overlap start of", this+"", delta.before(this));
  623. overlap = delta.start + delta.del - this.start;
  624. return [this.clone(delta.start + delta.text.length, this.del - overlap),
  625. delta.clone(undefined, delta.del - overlap)];
  626. }
  627. throw 'Should not happen';
  628. },
  629. before: function (other) {
  630. return this.start + this.del <= other.start;
  631. },
  632. contains: function (other) {
  633. return other.start >= this.start && other.start + other.del < this.start + this.del;
  634. },
  635. sameRange: function (other) {
  636. return other.start == this.start && other.del == this.del;
  637. },
  638. overlapsStart: function (other) {
  639. return this.start < other.start && this.start + this.del > other.start;
  640. },
  641. classMethods: {
  642. /* Make a new ot.TextReplace that converts oldValue to newValue. */
  643. fromChange: function(oldValue, newValue) {
  644. assert(typeof oldValue == "string");
  645. assert(typeof newValue == "string");
  646. var commonStart = 0;
  647. while (commonStart < newValue.length &&
  648. newValue.charAt(commonStart) == oldValue.charAt(commonStart)) {
  649. commonStart++;
  650. }
  651. var commonEnd = 0;
  652. while (commonEnd < (newValue.length - commonStart) &&
  653. commonEnd < (oldValue.length - commonStart) &&
  654. newValue.charAt(newValue.length - commonEnd - 1) ==
  655. oldValue.charAt(oldValue.length - commonEnd - 1)) {
  656. commonEnd++;
  657. }
  658. var removed = oldValue.substr(commonStart, oldValue.length - commonStart - commonEnd);
  659. var inserted = newValue.substr(commonStart, newValue.length - commonStart - commonEnd);
  660. if (! (removed.length || inserted)) {
  661. return null;
  662. }
  663. return this(commonStart, removed.length, inserted);
  664. },
  665. random: function (source, generator) {
  666. var text, start, len;
  667. var ops = ["ins", "del", "repl"];
  668. if (! source.length) {
  669. ops = ["ins"];
  670. }
  671. switch (generator.pick(ops)) {
  672. case "ins":
  673. if (! generator.number(2)) {
  674. text = generator.string(1);
  675. } else {
  676. text = generator.string(generator.number(3)+1);
  677. }
  678. if (! generator.number(4)) {
  679. start = 0;
  680. } else if (! generator.number(3)) {
  681. start = source.length-1;
  682. } else {
  683. start = generator.number(source.length);
  684. }
  685. return this(start, 0, text);
  686. case "del":
  687. if (! generator.number(20)) {
  688. return this(0, source.length, "");
  689. }
  690. start = generator.number(source.length-1);
  691. if (! generator.number(2)) {
  692. len = 1;
  693. } else {
  694. len = generator.number(5)+1;
  695. }
  696. len = Math.min(len, source.length - start);
  697. return this(start, len, "");
  698. case "repl":
  699. start = generator.number(source.length-1);
  700. len = generator.number(5);
  701. len = Math.min(len, source.length - start);
  702. text = generator.string(generator.number(2)+1);
  703. return this(start, len, text);
  704. }
  705. throw 'Unreachable';
  706. }
  707. }
  708. });
  709. return ot;
  710. });