Skip to content

Commit b57b028

Browse files
committed
Debouncing, with multipleblocks supported
1 parent 04a0a11 commit b57b028

File tree

3 files changed

+197
-42
lines changed

3 files changed

+197
-42
lines changed

diff-area.js

Lines changed: 88 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,21 @@ var DiffArea = React.createClass({
2929

3030
var state = {};
3131

32+
// Create editors state
33+
state.leftState = editorStateFromText(left);
34+
state.rightState = editorStateFromText(right);
35+
3236
// Compute diff on whole texts
3337
var diffs = computeDiff(left, right);
34-
35-
// Make decorators
36-
var removedDecorator = createDiffsDecorator(diffs, DIFF.REMOVED);
37-
var insertedDecorator = createDiffsDecorator(diffs, DIFF.INSERTED);
38-
39-
// Create editors state
40-
state.leftState = editorStateFromText(left, removedDecorator);
41-
state.rightState = editorStateFromText(right, insertedDecorator);
38+
var mappingLeft = mapDiffsToBlocks(diffs, DIFF.REMOVED, state.leftState.getCurrentContent().getBlockMap());
39+
var mappingRight = mapDiffsToBlocks(diffs, DIFF.INSERTED, state.rightState.getCurrentContent().getBlockMap());
40+
// Update the decorators
41+
state.leftState = Draft.EditorState.set(state.leftState, {
42+
decorator: createDiffsDecorator(mappingLeft, DIFF.REMOVED)
43+
});
44+
state.rightState = Draft.EditorState.set(state.rightState, {
45+
decorator: createDiffsDecorator(mappingRight, DIFF.INSERTED)
46+
});
4247

4348
return state;
4449
},
@@ -68,12 +73,15 @@ var DiffArea = React.createClass({
6873

6974
var diffs = computeDiff(left, right);
7075

76+
var mappingLeft = mapDiffsToBlocks(diffs, DIFF.REMOVED, this.state.leftState.getCurrentContent().getBlockMap());
77+
var mappingRight = mapDiffsToBlocks(diffs, DIFF.INSERTED, this.state.rightState.getCurrentContent().getBlockMap());
78+
7179
// Update the decorators
7280
newState.leftState = Draft.EditorState.set(this.state.leftState, {
73-
decorator: createDiffsDecorator(diffs, DIFF.REMOVED)
81+
decorator: createDiffsDecorator(mappingLeft, DIFF.REMOVED)
7482
});
7583
newState.rightState = Draft.EditorState.set(this.state.rightState, {
76-
decorator: createDiffsDecorator(diffs, DIFF.INSERTED)
84+
decorator: createDiffsDecorator(mappingRight, DIFF.INSERTED)
7785
});
7886
this.setState(newState);
7987
},
@@ -97,26 +105,71 @@ var DiffArea = React.createClass({
97105
});
98106

99107
function editorStateFromText(text, decorator) {
100-
// For now, we can only work on a single content block.
101-
var content = Draft.convertFromRaw({
102-
blocks: [
103-
{
104-
text: text,
105-
type: 'unstyled'
106-
}
107-
],
108-
entityMap: {}
109-
});
108+
var content = Draft.ContentState.createFromText(text);
110109
return Draft.EditorState.createWithContent(content, decorator);
111110
}
112111

113112
function computeDiff(txt1, txt2) {
114-
var diffs = DMP.diff_main(txt1, txt2);
115-
// Simplify diffs a bit to make it human readable (but non optimal)
116-
DMP.diff_cleanupSemantic(diffs);
113+
var diffs = DMP.diff_wordMode(txt1, txt2);
117114
return diffs;
118115
}
119116

117+
/**
118+
* Returns the lists of highlighted ranges for each block of a blockMap
119+
* @returns {Immutable.Map} blockKey -> { text, ranges: Array<{start, end}}>
120+
*/
121+
function mapDiffsToBlocks(diffs, type, blockMap) {
122+
var charIndex = 0;
123+
var absoluteRanges = [];
124+
125+
diffs.forEach(function (diff) {
126+
var diffType = diff[0];
127+
var diffText = diff[1];
128+
if (diffType === DIFF.EQUAL) {
129+
// No highlight. Move to next difference
130+
charIndex += diffText.length;
131+
} else if (diffType === type) {
132+
// Highlight, and move to next difference
133+
absoluteRanges.push({
134+
start: charIndex,
135+
end: charIndex + diffText.length
136+
});
137+
charIndex += diffText.length;
138+
} else {
139+
// The diff text should not be in the contentBlock, so skip.
140+
return;
141+
}
142+
});
143+
144+
// `end` excluded
145+
function findRangesBetween(ranges, start, end) {
146+
var res = [];
147+
ranges.forEach(function (range) {
148+
if (range.start < end && range.end > start) {
149+
var intersectionStart = Math.max(range.start, start);
150+
var intersectionEnd = Math.min(range.end, end);
151+
// Push relative range
152+
res.push({
153+
start: intersectionStart - start,
154+
end: intersectionEnd - start
155+
});
156+
}
157+
});
158+
return res;
159+
}
160+
161+
var blockStartIndex = 0;
162+
return blockMap.map(function (block) {
163+
var ranges = findRangesBetween(absoluteRanges, blockStartIndex, blockStartIndex + block.getLength());
164+
blockStartIndex += block.getLength();
165+
return {
166+
text: block.getText(),
167+
key: block.getKey(),
168+
ranges: ranges
169+
};
170+
});
171+
}
172+
120173
// Decorators
121174

122175
var InsertedSpan = function (props) {
@@ -127,12 +180,11 @@ var RemovedSpan = function (props) {
127180
};
128181

129182
/**
130-
* @param diffs The diff_match_patch result.
131183
* @param type The type of diff to highlight
132184
*/
133-
function createDiffsDecorator(diffs, type) {
185+
function createDiffsDecorator(mappedRanges, type) {
134186
return new Draft.CompositeDecorator([{
135-
strategy: findDiff.bind(undefined, diffs, type),
187+
strategy: findDiff.bind(undefined, mappedRanges, type),
136188
component: type === DIFF.INSERTED ? InsertedSpan : RemovedSpan
137189
}]);
138190
}
@@ -141,23 +193,17 @@ function createDiffsDecorator(diffs, type) {
141193
* Applies the decorator callback to all differences in the single content block.
142194
* This needs to be cheap, because decorators are called often.
143195
*/
144-
function findDiff(diffs, type, contentBlock, callback) {
145-
var charIndex = 0;
146-
diffs.forEach(function (diff) {
147-
var diffType = diff[0];
148-
var diffText = diff[1];
149-
if (diffType === DIFF.EQUAL) {
150-
// No highlight. Move to next difference
151-
charIndex += diffText.length;
152-
} else if (diffType === type) {
153-
// Highlight, and move to next difference
154-
callback(charIndex, charIndex + diffText.length);
155-
charIndex += diffText.length;
156-
} else {
157-
// The diff text should not be in the contentBlock, so skip.
158-
return;
196+
function findDiff(mappedRanges, type, contentBlock, callback) {
197+
var mapping = mappedRanges.get(contentBlock.getKey());
198+
if (mapping && mapping.text === contentBlock.getText()) {
199+
mapping.ranges.forEach(function (range) {
200+
callback(range.start, range.end);
201+
});
202+
} else {
203+
if (mapping) {
204+
console.log('Content changed', mapping.key, contentBlock.getKey());
159205
}
160-
});
206+
}
161207
}
162208

163209

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
Molestiae optio et quam at labore voluptatem animi. Sed libero odio repellendus iure omnis eos. Alias sapiente placeat est provident repellendus. Earum minima vero nesciunt voluptatem ipsa. Sint vitae et hic eveniet sunt. Doloremque ea esse consequatur.
4646
</p>
4747

48+
<script type="text/javascript" src="lib/diff-word-mode.js"></script>
4849
<script type="text/babel" src="diff-area.js"></script>
4950
</body>
5051
</html>

lib/diff-word-mode.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// var diff_match_patch = require('diff-match-patch');
2+
3+
// Adapted from
4+
// https://code.google.com/p/google-diff-match-patch/wiki/LineOrWordDiffs
5+
6+
/**
7+
* Find the differences between two texts, word-wise.
8+
* @param {string} text1
9+
* @param {string} text2
10+
* @returns {Array<diff_match_patch.Diff>} Array of diff tuples
11+
*/
12+
diff_match_patch.prototype.diff_wordMode = function (text1, text2) {
13+
return this.diff_groupMode(text1, text2, /\s/);
14+
};
15+
16+
/**
17+
* Find the differences between two texts, grouping characters according to a regex
18+
* @param {string} text1
19+
* @param {string} text2
20+
* @param {RegExp} groupDelimiter
21+
* @returns {Array<diff_match_patch.Diff>} Array of diff tuples
22+
*/
23+
diff_match_patch.prototype.diff_groupMode = function (text1, text2, groupDelimiter) {
24+
// Convert groups to unique chars, to allow diffing at a group level
25+
var a = _diff_groupsToChars_(text1, text2, groupDelimiter);
26+
var lineText1 = a.chars1;
27+
var lineText2 = a.chars2;
28+
var groupArray = a.groupArray;
29+
30+
var diffs = this.diff_main(lineText1, lineText2, false);
31+
32+
this.diff_charsToLines_(diffs, groupArray);
33+
return diffs;
34+
};
35+
36+
/**
37+
* Split two texts into an array of strings. Reduce the texts to a string of
38+
* hashes where each Unicode character represents a unique group.
39+
* @param {string} text1 First string.
40+
* @param {string} text2 Second string.
41+
* @param {regex} delimiter
42+
* @return {{chars1: string, chars2: string, groupArray: !Array.<string>}}
43+
* An object containing the encoded text1, the encoded text2 and
44+
* the array of unique strings.
45+
* The zeroth element of the array of unique strings is intentionally blank.
46+
* @private
47+
*/
48+
// Copied from diff_match_patch.linesToChars. Adapted to accept a
49+
// delimiter, in order to make groups of line/words/anything.
50+
function _diff_groupsToChars_(text1, text2, delimiter) {
51+
var groupArray = []; // e.g. groupArray[4] == 'Hello\n' for a line delimiter /\n/
52+
var lineHash = {}; // e.g. lineHash['Hello\n'] == 4
53+
54+
// '\x00' is a valid character, but various debuggers don't like it.
55+
// So we'll insert a junk entry to avoid generating a null character.
56+
groupArray[0] = '';
57+
58+
/**
59+
* Split a text into an array of strings. Reduce the texts to a string of
60+
* hashes where each Unicode character represents one line.
61+
* Modifies linearray and linehash through being a closure.
62+
* @param {string} text String to encode.
63+
* @return {string} Encoded string.
64+
* @private
65+
*/
66+
function diff_groupsToCharsMunge_(text) {
67+
var chars = '';
68+
// Walk the text, pulling out a substring for each line.
69+
// text.split() would temporarily double our memory footprint.
70+
// Modifying text would create many large strings to garbage collect.
71+
var lineStart = 0;
72+
var lineEnd = -1;
73+
// Keeping our own length variable is faster than looking it up in JS
74+
var groupArrayLength = groupArray.length;
75+
while (lineEnd < text.length - 1) {
76+
lineEnd = regexIndexOf(text, delimiter, lineStart);
77+
if (lineEnd == -1) {
78+
lineEnd = text.length - 1;
79+
}
80+
var line = text.substring(lineStart, lineEnd + 1);
81+
lineStart = lineEnd + 1;
82+
83+
if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
84+
(lineHash[line] !== undefined)) {
85+
chars += String.fromCharCode(lineHash[line]);
86+
} else {
87+
chars += String.fromCharCode(groupArrayLength);
88+
lineHash[line] = groupArrayLength;
89+
groupArray[groupArrayLength++] = line;
90+
}
91+
}
92+
return chars;
93+
}
94+
95+
var chars1 = diff_groupsToCharsMunge_(text1);
96+
var chars2 = diff_groupsToCharsMunge_(text2);
97+
return {chars1: chars1, chars2: chars2, groupArray: groupArray};
98+
}
99+
100+
/**
101+
* Same as String.indexOf, but uses RegExp
102+
*/
103+
function regexIndexOf(str, regex, startpos) {
104+
var indexOf = str.substring(startpos || 0).search(regex);
105+
return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf;
106+
}
107+
108+

0 commit comments

Comments
 (0)