prosemirror-changeset 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ import { Mark, Node } from 'prosemirror-model';
2
+ import { StepMap } from 'prosemirror-transform';
3
+
4
+ /**
5
+ Stores metadata for a part of a change.
6
+ */
7
+ declare class Span<Data = any> {
8
+ /**
9
+ The length of this span.
10
+ */
11
+ readonly length: number;
12
+ /**
13
+ The data associated with this span.
14
+ */
15
+ readonly data: Data;
16
+ }
17
+ /**
18
+ A replaced range with metadata associated with it.
19
+ */
20
+ declare class Change<Data = any> {
21
+ /**
22
+ The start of the range deleted/replaced in the old document.
23
+ */
24
+ readonly fromA: number;
25
+ /**
26
+ The end of the range in the old document.
27
+ */
28
+ readonly toA: number;
29
+ /**
30
+ The start of the range inserted in the new document.
31
+ */
32
+ readonly fromB: number;
33
+ /**
34
+ The end of the range in the new document.
35
+ */
36
+ readonly toB: number;
37
+ /**
38
+ Data associated with the deleted content. The length of these
39
+ spans adds up to `this.toA - this.fromA`.
40
+ */
41
+ readonly deleted: readonly Span<Data>[];
42
+ /**
43
+ Data associated with the inserted content. Length adds up to
44
+ `this.toB - this.fromB`.
45
+ */
46
+ readonly inserted: readonly Span<Data>[];
47
+ /**
48
+ This merges two changesets (the end document of x should be the
49
+ start document of y) into a single one spanning the start of x to
50
+ the end of y.
51
+ */
52
+ static merge<Data>(x: readonly Change<Data>[], y: readonly Change<Data>[], combine: (dataA: Data, dataB: Data) => Data): readonly Change<Data>[];
53
+ }
54
+
55
+ /**
56
+ A token encoder can be passed when creating a `ChangeSet` in order
57
+ to influence the way the library runs its diffing algorithm. The
58
+ encoder determines how document tokens (such as nodes and
59
+ characters) are encoded and compared.
60
+
61
+ Note that both the encoding and the comparison may run a lot, and
62
+ doing non-trivial work in these functions could impact
63
+ performance.
64
+ */
65
+ interface TokenEncoder<T> {
66
+ /**
67
+ Encode a given character, with the given marks applied.
68
+ */
69
+ encodeCharacter(char: number, marks: readonly Mark[]): T;
70
+ /**
71
+ Encode the start of a node or, if this is a leaf node, the
72
+ entire node.
73
+ */
74
+ encodeNodeStart(node: Node): T;
75
+ /**
76
+ Encode the end token for the given node. It is valid to encode
77
+ every end token in the same way.
78
+ */
79
+ encodeNodeEnd(node: Node): T;
80
+ /**
81
+ Compare the given tokens. Should return true when they count as
82
+ equal.
83
+ */
84
+ compareTokens(a: T, b: T): boolean;
85
+ }
86
+
87
+ /**
88
+ Simplifies a set of changes for presentation. This makes the
89
+ assumption that having both insertions and deletions within a word
90
+ is confusing, and, when such changes occur without a word boundary
91
+ between them, they should be expanded to cover the entire set of
92
+ words (in the new document) they touch. An exception is made for
93
+ single-character replacements.
94
+ */
95
+ declare function simplifyChanges(changes: readonly Change[], doc: Node): Change<any>[];
96
+
97
+ /**
98
+ A change set tracks the changes to a document from a given point
99
+ in the past. It condenses a number of step maps down to a flat
100
+ sequence of replacements, and simplifies replacments that
101
+ partially undo themselves by comparing their content.
102
+ */
103
+ declare class ChangeSet<Data = any> {
104
+ /**
105
+ Replaced regions.
106
+ */
107
+ readonly changes: readonly Change<Data>[];
108
+ /**
109
+ Computes a new changeset by adding the given step maps and
110
+ metadata (either as an array, per-map, or as a single value to be
111
+ associated with all maps) to the current set. Will not mutate the
112
+ old set.
113
+
114
+ Note that due to simplification that happens after each add,
115
+ incrementally adding steps might create a different final set
116
+ than adding all those changes at once, since different document
117
+ tokens might be matched during simplification depending on the
118
+ boundaries of the current changed ranges.
119
+ */
120
+ addSteps(newDoc: Node, maps: readonly StepMap[], data: Data | readonly Data[]): ChangeSet<Data>;
121
+ /**
122
+ The starting document of the change set.
123
+ */
124
+ get startDoc(): Node;
125
+ /**
126
+ Map the span's data values in the given set through a function
127
+ and construct a new set with the resulting data.
128
+ */
129
+ map(f: (range: Span<Data>) => Data): ChangeSet<Data>;
130
+ /**
131
+ Compare two changesets and return the range in which they are
132
+ changed, if any. If the document changed between the maps, pass
133
+ the maps for the steps that changed it as second argument, and
134
+ make sure the method is called on the old set and passed the new
135
+ set. The returned positions will be in new document coordinates.
136
+ */
137
+ changedRange(b: ChangeSet, maps?: readonly StepMap[]): {
138
+ from: number;
139
+ to: number;
140
+ } | null;
141
+ /**
142
+ Create a changeset with the given base object and configuration.
143
+
144
+ The `combine` function is used to compare and combine metadata—it
145
+ should return null when metadata isn't compatible, and a combined
146
+ version for a merged range when it is.
147
+
148
+ When given, a token encoder determines how document tokens are
149
+ serialized and compared when diffing the content produced by
150
+ changes. The default is to just compare nodes by name and text
151
+ by character, ignoring marks and attributes.
152
+ */
153
+ static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>): ChangeSet<Data>;
154
+ }
155
+
156
+ export { Change, ChangeSet, Span, type TokenEncoder, simplifyChanges };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Node } from 'prosemirror-model';
1
+ import { Mark, Node } from 'prosemirror-model';
2
2
  import { StepMap } from 'prosemirror-transform';
3
3
 
4
4
  /**
@@ -41,7 +41,7 @@ declare class Change<Data = any> {
41
41
  readonly deleted: readonly Span<Data>[];
42
42
  /**
43
43
  Data associated with the inserted content. Length adds up to
44
- `this.toB - this.toA`.
44
+ `this.toB - this.fromB`.
45
45
  */
46
46
  readonly inserted: readonly Span<Data>[];
47
47
  /**
@@ -52,6 +52,38 @@ declare class Change<Data = any> {
52
52
  static merge<Data>(x: readonly Change<Data>[], y: readonly Change<Data>[], combine: (dataA: Data, dataB: Data) => Data): readonly Change<Data>[];
53
53
  }
54
54
 
55
+ /**
56
+ A token encoder can be passed when creating a `ChangeSet` in order
57
+ to influence the way the library runs its diffing algorithm. The
58
+ encoder determines how document tokens (such as nodes and
59
+ characters) are encoded and compared.
60
+
61
+ Note that both the encoding and the comparison may run a lot, and
62
+ doing non-trivial work in these functions could impact
63
+ performance.
64
+ */
65
+ interface TokenEncoder<T> {
66
+ /**
67
+ Encode a given character, with the given marks applied.
68
+ */
69
+ encodeCharacter(char: number, marks: readonly Mark[]): T;
70
+ /**
71
+ Encode the start of a node or, if this is a leaf node, the
72
+ entire node.
73
+ */
74
+ encodeNodeStart(node: Node): T;
75
+ /**
76
+ Encode the end token for the given node. It is valid to encode
77
+ every end token in the same way.
78
+ */
79
+ encodeNodeEnd(node: Node): T;
80
+ /**
81
+ Compare the given tokens. Should return true when they count as
82
+ equal.
83
+ */
84
+ compareTokens(a: T, b: T): boolean;
85
+ }
86
+
55
87
  /**
56
88
  Simplifies a set of changes for presentation. This makes the
57
89
  assumption that having both insertions and deletions within a word
@@ -108,11 +140,17 @@ declare class ChangeSet<Data = any> {
108
140
  } | null;
109
141
  /**
110
142
  Create a changeset with the given base object and configuration.
143
+
111
144
  The `combine` function is used to compare and combine metadata—it
112
145
  should return null when metadata isn't compatible, and a combined
113
146
  version for a merged range when it is.
147
+
148
+ When given, a token encoder determines how document tokens are
149
+ serialized and compared when diffing the content produced by
150
+ changes. The default is to just compare nodes by name and text
151
+ by character, ignoring marks and attributes.
114
152
  */
115
- static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data): ChangeSet<Data>;
153
+ static create<Data = any>(doc: Node, combine?: (dataA: Data, dataB: Data) => Data, tokenEncoder?: TokenEncoder<any>): ChangeSet<Data>;
116
154
  }
117
155
 
118
- export { Change, ChangeSet, Span, simplifyChanges };
156
+ export { Change, ChangeSet, Span, type TokenEncoder, simplifyChanges };
package/dist/index.js CHANGED
@@ -1,24 +1,30 @@
1
+ const DefaultEncoder = {
2
+ encodeCharacter: char => char,
3
+ encodeNodeStart: node => node.type.name,
4
+ encodeNodeEnd: () => -1,
5
+ compareTokens: (a, b) => a === b
6
+ };
1
7
  // Convert the given range of a fragment to tokens, where node open
2
8
  // tokens are encoded as strings holding the node name, characters as
3
9
  // their character code, and node close tokens as -1.
4
- function tokens(frag, start, end, target) {
10
+ function tokens(frag, encoder, start, end, target) {
5
11
  for (let i = 0, off = 0; i < frag.childCount; i++) {
6
12
  let child = frag.child(i), endOff = off + child.nodeSize;
7
13
  let from = Math.max(off, start), to = Math.min(endOff, end);
8
14
  if (from < to) {
9
15
  if (child.isText) {
10
16
  for (let j = from; j < to; j++)
11
- target.push(child.text.charCodeAt(j - off));
17
+ target.push(encoder.encodeCharacter(child.text.charCodeAt(j - off), child.marks));
12
18
  }
13
19
  else if (child.isLeaf) {
14
- target.push(child.type.name);
20
+ target.push(encoder.encodeNodeStart(child));
15
21
  }
16
22
  else {
17
23
  if (from == off)
18
- target.push(child.type.name);
19
- tokens(child.content, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
24
+ target.push(encoder.encodeNodeStart(child));
25
+ tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target);
20
26
  if (to == endOff)
21
- target.push(-1);
27
+ target.push(encoder.encodeNodeEnd(child));
22
28
  }
23
29
  }
24
30
  off = endOff;
@@ -37,16 +43,17 @@ const MAX_DIFF_SIZE = 5000;
37
43
  function minUnchanged(sizeA, sizeB) {
38
44
  return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)));
39
45
  }
40
- function computeDiff(fragA, fragB, range) {
41
- let tokA = tokens(fragA, range.fromA, range.toA, []);
42
- let tokB = tokens(fragB, range.fromB, range.toB, []);
46
+ function computeDiff(fragA, fragB, range, encoder = DefaultEncoder) {
47
+ let tokA = tokens(fragA, encoder, range.fromA, range.toA, []);
48
+ let tokB = tokens(fragB, encoder, range.fromB, range.toB, []);
43
49
  // Scan from both sides to cheaply eliminate work
44
50
  let start = 0, endA = tokA.length, endB = tokB.length;
45
- while (start < tokA.length && start < tokB.length && tokA[start] === tokB[start])
51
+ let cmp = encoder.compareTokens;
52
+ while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start]))
46
53
  start++;
47
54
  if (start == tokA.length && start == tokB.length)
48
55
  return [];
49
- while (endA > start && endB > start && tokA[endA - 1] === tokB[endB - 1])
56
+ while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1]))
50
57
  endA--, endB--;
51
58
  // If the result is simple _or_ too big to cheaply compute, return
52
59
  // the remaining region as the diff
@@ -65,7 +72,7 @@ function computeDiff(fragA, fragB, range) {
65
72
  for (let diag = -size; diag <= size; diag += 2) {
66
73
  let next = frontier[diag + 1 + max], prev = frontier[diag - 1 + max];
67
74
  let x = next < prev ? prev : next + 1, y = x + diag;
68
- while (x < lenA && y < lenB && tokA[start + x] === tokB[start + y])
75
+ while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y]))
69
76
  x++, y++;
70
77
  frontier[diag + max] = x;
71
78
  // Found a match
@@ -225,7 +232,7 @@ class Change {
225
232
  deleted,
226
233
  /**
227
234
  Data associated with the inserted content. Length adds up to
228
- `this.toB - this.toA`.
235
+ `this.toB - this.fromB`.
229
236
  */
230
237
  inserted) {
231
238
  this.fromA = fromA;
@@ -549,7 +556,7 @@ class ChangeSet {
549
556
  // Only look at changes that touch newly added changed ranges
550
557
  !newChanges.some(r => r.toB > change.fromB && r.fromB < change.toB))
551
558
  continue;
552
- let diff = computeDiff(this.config.doc.content, newDoc.content, change);
559
+ let diff = computeDiff(this.config.doc.content, newDoc.content, change, this.config.encoder);
553
560
  // Fast path: If they are completely different, don't do anything
554
561
  if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB)
555
562
  continue;
@@ -622,12 +629,18 @@ class ChangeSet {
622
629
  }
623
630
  /**
624
631
  Create a changeset with the given base object and configuration.
632
+
625
633
  The `combine` function is used to compare and combine metadata—it
626
634
  should return null when metadata isn't compatible, and a combined
627
635
  version for a merged range when it is.
636
+
637
+ When given, a token encoder determines how document tokens are
638
+ serialized and compared when diffing the content produced by
639
+ changes. The default is to just compare nodes by name and text
640
+ by character, ignoring marks and attributes.
628
641
  */
629
- static create(doc, combine = (a, b) => a === b ? a : null) {
630
- return new ChangeSet({ combine, doc }, []);
642
+ static create(doc, combine = (a, b) => a === b ? a : null, tokenEncoder = DefaultEncoder) {
643
+ return new ChangeSet({ combine, doc, encoder: tokenEncoder }, []);
631
644
  }
632
645
  }
633
646
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-changeset",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Distills a series of editing steps into deleted and added ranges",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -15,7 +15,7 @@
15
15
  "maintainers": [
16
16
  {
17
17
  "name": "Marijn Haverbeke",
18
- "email": "marijnh@gmail.com",
18
+ "email": "marijn@haverbeke.berlin",
19
19
  "web": "http://marijnhaverbeke.nl"
20
20
  }
21
21
  ],
@@ -29,11 +29,12 @@
29
29
  "devDependencies": {
30
30
  "@prosemirror/buildhelper": "^0.1.5",
31
31
  "prosemirror-model": "^1.0.0",
32
- "prosemirror-test-builder": "^1.0.0"
32
+ "prosemirror-test-builder": "^1.0.0",
33
+ "builddocs": "^1.0.8"
33
34
  },
34
35
  "scripts": {
35
36
  "test": "pm-runtests",
36
37
  "prepare": "pm-buildhelper src/changeset.ts",
37
- "build-readme": "FIXME"
38
+ "build-readme": "builddocs --format markdown --main src/README.md src/changeset.ts > README.md"
38
39
  }
39
40
  }
package/src/README.md CHANGED
@@ -25,4 +25,6 @@ it was made, or the step data necessary to invert it again.
25
25
 
26
26
  @ChangeSet
27
27
 
28
- @simplifyChanges
28
+ @simplifyChanges
29
+
30
+ @TokenEncoder
package/src/change.ts CHANGED
@@ -66,7 +66,7 @@ export class Change<Data = any> {
66
66
  /// spans adds up to `this.toA - this.fromA`.
67
67
  readonly deleted: readonly Span<Data>[],
68
68
  /// Data associated with the inserted content. Length adds up to
69
- /// `this.toB - this.toA`.
69
+ /// `this.toB - this.fromB`.
70
70
  readonly inserted: readonly Span<Data>[]
71
71
  ) {}
72
72
 
package/src/changeset.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import {Node} from "prosemirror-model"
2
2
  import {StepMap} from "prosemirror-transform"
3
- import {computeDiff} from "./diff"
3
+ import {computeDiff, TokenEncoder, DefaultEncoder} from "./diff"
4
4
  import {Change, Span} from "./change"
5
5
  export {Change, Span}
6
6
  export {simplifyChanges} from "./simplify"
7
+ export {TokenEncoder}
7
8
 
8
9
  /// A change set tracks the changes to a document from a given point
9
10
  /// in the past. It condenses a number of step maps down to a flat
@@ -13,7 +14,11 @@ export class ChangeSet<Data = any> {
13
14
  /// @internal
14
15
  constructor(
15
16
  /// @internal
16
- readonly config: {doc: Node, combine: (dataA: Data, dataB: Data) => Data},
17
+ readonly config: {
18
+ doc: Node,
19
+ combine: (dataA: Data, dataB: Data) => Data,
20
+ encoder: TokenEncoder<any>
21
+ },
17
22
  /// Replaced regions.
18
23
  readonly changes: readonly Change<Data>[]
19
24
  ) {}
@@ -69,7 +74,7 @@ export class ChangeSet<Data = any> {
69
74
  if (change.fromA == change.toA || change.fromB == change.toB ||
70
75
  // Only look at changes that touch newly added changed ranges
71
76
  !newChanges.some(r => r.toB > change.fromB && r.fromB < change.toB)) continue
72
- let diff = computeDiff(this.config.doc.content, newDoc.content, change)
77
+ let diff = computeDiff(this.config.doc.content, newDoc.content, change, this.config.encoder)
73
78
 
74
79
  // Fast path: If they are completely different, don't do anything
75
80
  if (diff.length == 1 && diff[0].fromB == 0 && diff[0].toB == change.toB - change.fromB)
@@ -132,11 +137,21 @@ export class ChangeSet<Data = any> {
132
137
  }
133
138
 
134
139
  /// Create a changeset with the given base object and configuration.
140
+ ///
135
141
  /// The `combine` function is used to compare and combine metadata—it
136
142
  /// should return null when metadata isn't compatible, and a combined
137
143
  /// version for a merged range when it is.
138
- static create<Data = any>(doc: Node, combine: (dataA: Data, dataB: Data) => Data = (a, b) => a === b ? a : null as any) {
139
- return new ChangeSet({combine, doc}, [])
144
+ ///
145
+ /// When given, a token encoder determines how document tokens are
146
+ /// serialized and compared when diffing the content produced by
147
+ /// changes. The default is to just compare nodes by name and text
148
+ /// by character, ignoring marks and attributes.
149
+ static create<Data = any>(
150
+ doc: Node,
151
+ combine: (dataA: Data, dataB: Data) => Data = (a, b) => a === b ? a : null as any,
152
+ tokenEncoder: TokenEncoder<any> = DefaultEncoder
153
+ ) {
154
+ return new ChangeSet({combine, doc, encoder: tokenEncoder}, [])
140
155
  }
141
156
 
142
157
  /// Exported for testing @internal
package/src/diff.ts CHANGED
@@ -1,22 +1,51 @@
1
- import {Fragment} from "prosemirror-model"
1
+ import {Fragment, Node, Mark} from "prosemirror-model"
2
2
  import {Change} from "./change"
3
3
 
4
+ /// A token encoder can be passed when creating a `ChangeSet` in order
5
+ /// to influence the way the library runs its diffing algorithm. The
6
+ /// encoder determines how document tokens (such as nodes and
7
+ /// characters) are encoded and compared.
8
+ ///
9
+ /// Note that both the encoding and the comparison may run a lot, and
10
+ /// doing non-trivial work in these functions could impact
11
+ /// performance.
12
+ export interface TokenEncoder<T> {
13
+ /// Encode a given character, with the given marks applied.
14
+ encodeCharacter(char: number, marks: readonly Mark[]): T
15
+ /// Encode the start of a node or, if this is a leaf node, the
16
+ /// entire node.
17
+ encodeNodeStart(node: Node): T
18
+ /// Encode the end token for the given node. It is valid to encode
19
+ /// every end token in the same way.
20
+ encodeNodeEnd(node: Node): T
21
+ /// Compare the given tokens. Should return true when they count as
22
+ /// equal.
23
+ compareTokens(a: T, b: T): boolean
24
+ }
25
+
26
+ export const DefaultEncoder: TokenEncoder<number | string> = {
27
+ encodeCharacter: char => char,
28
+ encodeNodeStart: node => node.type.name,
29
+ encodeNodeEnd: () => -1,
30
+ compareTokens: (a, b) => a === b
31
+ }
32
+
4
33
  // Convert the given range of a fragment to tokens, where node open
5
34
  // tokens are encoded as strings holding the node name, characters as
6
35
  // their character code, and node close tokens as -1.
7
- function tokens(frag: Fragment, start: number, end: number, target: (number | string)[]) {
36
+ function tokens<T>(frag: Fragment, encoder: TokenEncoder<T>, start: number, end: number, target: T[]) {
8
37
  for (let i = 0, off = 0; i < frag.childCount; i++) {
9
38
  let child = frag.child(i), endOff = off + child.nodeSize
10
39
  let from = Math.max(off, start), to = Math.min(endOff, end)
11
40
  if (from < to) {
12
41
  if (child.isText) {
13
- for (let j = from; j < to; j++) target.push(child.text!.charCodeAt(j - off))
42
+ for (let j = from; j < to; j++) target.push(encoder.encodeCharacter(child.text!.charCodeAt(j - off), child.marks))
14
43
  } else if (child.isLeaf) {
15
- target.push(child.type.name)
44
+ target.push(encoder.encodeNodeStart(child))
16
45
  } else {
17
- if (from == off) target.push(child.type.name)
18
- tokens(child.content, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target)
19
- if (to == endOff) target.push(-1)
46
+ if (from == off) target.push(encoder.encodeNodeStart(child))
47
+ tokens(child.content, encoder, Math.max(off + 1, from) - off - 1, Math.min(endOff - 1, to) - off - 1, target)
48
+ if (to == endOff) target.push(encoder.encodeNodeEnd(child))
20
49
  }
21
50
  }
22
51
  off = endOff
@@ -38,15 +67,16 @@ function minUnchanged(sizeA: number, sizeB: number) {
38
67
  return Math.min(15, Math.max(2, Math.floor(Math.max(sizeA, sizeB) / 10)))
39
68
  }
40
69
 
41
- export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change) {
42
- let tokA = tokens(fragA, range.fromA, range.toA, [])
43
- let tokB = tokens(fragB, range.fromB, range.toB, [])
70
+ export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change, encoder: TokenEncoder<any> = DefaultEncoder) {
71
+ let tokA = tokens(fragA, encoder, range.fromA, range.toA, [])
72
+ let tokB = tokens(fragB, encoder, range.fromB, range.toB, [])
44
73
 
45
74
  // Scan from both sides to cheaply eliminate work
46
75
  let start = 0, endA = tokA.length, endB = tokB.length
47
- while (start < tokA.length && start < tokB.length && tokA[start] === tokB[start]) start++
76
+ let cmp = encoder.compareTokens
77
+ while (start < tokA.length && start < tokB.length && cmp(tokA[start], tokB[start])) start++
48
78
  if (start == tokA.length && start == tokB.length) return []
49
- while (endA > start && endB > start && tokA[endA - 1] === tokB[endB - 1]) endA--, endB--
79
+ while (endA > start && endB > start && cmp(tokA[endA - 1], tokB[endB - 1])) endA--, endB--
50
80
  // If the result is simple _or_ too big to cheaply compute, return
51
81
  // the remaining region as the diff
52
82
  if (endA == start || endB == start || (endA == endB && endA == start + 1))
@@ -66,7 +96,7 @@ export function computeDiff(fragA: Fragment, fragB: Fragment, range: Change) {
66
96
  for (let diag = -size; diag <= size; diag += 2) {
67
97
  let next = frontier[diag + 1 + max], prev = frontier[diag - 1 + max]
68
98
  let x = next < prev ? prev : next + 1, y = x + diag
69
- while (x < lenA && y < lenB && tokA[start + x] === tokB[start + y]) x++, y++
99
+ while (x < lenA && y < lenB && cmp(tokA[start + x], tokB[start + y])) x++, y++
70
100
  frontier[diag + max] = x
71
101
  // Found a match
72
102
  if (x >= lenA && y >= lenB) {