Implemented sync merge

This commit is contained in:
benweet 2014-03-29 01:22:24 +00:00
parent 0e5f198270
commit acebad8a65
5 changed files with 181 additions and 25 deletions

View File

@ -75,15 +75,78 @@ define([
}); });
var merge = settings.conflictMode == 'merge'; var merge = settings.conflictMode == 'merge';
Provider.prototype.merge = function(localContent, remoteContent, localTitle, remoteTitle, localDiscussionList, remoteDiscussionList, syncAttributes) { Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionListJSON) {
var lineArray = [];
var lineHash = {};
function linesToChars(text) {
var chars = '';
var lineArrayLength = lineArray.length;
text.split('\n').forEach(function(line) {
if(lineHash.hasOwnProperty(line)) {
chars += String.fromCharCode(lineHash[line]);
} else {
chars += String.fromCharCode(lineArrayLength);
lineHash[line] = lineArrayLength;
lineArray[lineArrayLength++] = line;
}
});
return chars;
}
function moveComments(oldTextContent, newTextContent, discussionList) {
var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent);
var updateDiscussionList = false;
var startOffset = 0;
changes.forEach(function(change) {
var changeType = change[0];
var changeText = change[1];
if(changeType === 0) {
startOffset += changeText.length;
return;
}
var endOffset = startOffset;
var diffOffset = changeText.length;
if(changeType === -1) {
endOffset += diffOffset;
diffOffset = -diffOffset;
}
discussionList.forEach(function(discussion) {
// selectionEnd
if(discussion.selectionEnd >= endOffset) {
discussion.selectionEnd += diffOffset;
updateDiscussionList = true;
}
else if(discussion.selectionEnd > startOffset) {
discussion.selectionEnd = startOffset;
updateDiscussionList = true;
}
// selectionStart
if(discussion.selectionStart >= endOffset) {
discussion.selectionStart += diffOffset;
updateDiscussionList = true;
}
else if(discussion.selectionStart > startOffset) {
discussion.selectionStart = startOffset;
updateDiscussionList = true;
}
});
startOffset = endOffset;
});
return updateDiscussionList;
}
var localContent = fileDesc.content;
var localTitle = fileDesc.title;
var localDiscussionListJSON = fileDesc.discussionListJSON;
// Local/Remote CRCs // Local/Remote CRCs
var localContentCRC = utils.crc32(localContent); var localContentCRC = utils.crc32(localContent);
var localTitleCRC = utils.crc32(localTitle); var localTitleCRC = utils.crc32(localTitle);
var localDiscussionListCRC = utils.crc32(localDiscussionList); var localDiscussionListCRC = utils.crc32(localDiscussionListJSON);
var remoteContentCRC = utils.crc32(remoteContent); var remoteContentCRC = utils.crc32(remoteContent);
var remoteTitleCRC = utils.crc32(remoteTitle); var remoteTitleCRC = utils.crc32(remoteTitle);
var remoteDiscussionListCRC = utils.crc32(remoteDiscussionList); var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON);
// Check content // Check content
var contentChanged = localContent != remoteContent; var contentChanged = localContent != remoteContent;
@ -99,7 +162,7 @@ define([
var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged; var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged;
// Check discussionList // Check discussionList
var discussionListChanged = localDiscussionList != remoteDiscussionList; var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON;
var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC; var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC;
var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC; var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC;
var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged; var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged;
@ -111,15 +174,79 @@ define([
(titleConflict && syncAttributes.title === undefined) || (titleConflict && syncAttributes.title === undefined) ||
(discussionListConflict && syncAttributes.discussionList === undefined) (discussionListConflict && syncAttributes.discussionList === undefined)
) { ) {
fileMgr.createFile(localTitle + " (backup)", localContent); fileMgr.createFile(localTitle + " (backup)", localContent, localDiscussionListJSON);
eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.');
} }
else { else {
if(contentConflict === true) { var updateDiscussionList = remoteDiscussionListChanged;
var patch = diffMatchPatch.patch_make(syncAttributes.content, localContent); var localDiscussionList = fileDesc.discussionList;
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
var oldDiscussionList;
var patch, delta;
if(contentConflict) {
// Patch content (line mode)
var oldContentLines = linesToChars(syncAttributes.content);
var localContentLines = linesToChars(localContent);
var remoteContentLines = linesToChars(remoteContent);
patch = diffMatchPatch.patch_make(oldContentLines, localContentLines);
remoteContentLines = diffMatchPatch.patch_apply(patch, remoteContentLines)[0];
var newContent = remoteContentLines.split('').map(function(char) {
return lineArray[char.charCodeAt(0)];
}).join('\n');
// Whether we take the local discussionList into account
if(localDiscussionListChanged || !remoteDiscussionListChanged) {
// Move local discussion according to content patch
var localDiscussionArray = _.values(localDiscussionList);
fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion);
updateDiscussionList |= moveComments(localContent, newContent, localDiscussionArray);
}
if(remoteDiscussionListChanged) {
// Move remote discussion according to content patch
var remoteDiscussionArray = _.values(remoteDiscussionList);
moveComments(remoteContent, newContent, remoteDiscussionArray);
if(localDiscussionListChanged) {
// Patch remote discussionList with local modifications
oldDiscussionList = JSON.parse(syncAttributes.discussionList);
delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
jsonDiffPatch.patch(remoteDiscussionList, delta);
}
}
else {
remoteDiscussionList = localDiscussionList;
}
remoteContent = newContent;
}
else if(discussionListConflict) {
// Patch remote discussionList with local modifications
oldDiscussionList = JSON.parse(syncAttributes.discussionList);
delta = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList);
jsonDiffPatch.patch(remoteDiscussionList, delta);
}
if(titleConflict) {
// Patch title
patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle);
remoteTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0];
} }
} }
if(titleChanged && remoteTitleChanged) {
fileDesc.title = remoteTitle;
eventMgr.onTitleChanged(fileDesc);
eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + remoteTitle + '" on ' + this.providerName + '.');
}
if(contentChanged && remoteContentChanged === true) {
if(fileMgr.currentFile === fileDesc) {
document.getElementById('wmd-input').setValueSilently(remoteContent);
}
else {
fileDesc.content = remoteContent;
eventMgr.onContentChanged(fileDesc, remoteContent);
eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + this.providerName + '.');
}
}
}; };
return Provider; return Provider;

View File

@ -64,15 +64,23 @@ define([
}); });
var contentObserver; var contentObserver;
var isWatching = false;
function noWatch(cb) { function noWatch(cb) {
if(isWatching === true) {
contentObserver.disconnect(); contentObserver.disconnect();
isWatching = false;
cb(); cb();
isWatching = true;
contentObserver.observe(editor.contentElt, { contentObserver.observe(editor.contentElt, {
childList: true, childList: true,
subtree: true, subtree: true,
characterData: true characterData: true
}); });
} }
else {
cb();
}
}
var diffMatchPatch = new diff_match_patch(); var diffMatchPatch = new diff_match_patch();
var jsonDiffPatch = jsondiffpatch.create({ var jsonDiffPatch = jsondiffpatch.create({
@ -147,10 +155,7 @@ define([
// Update editor // Update editor
noWatch(function() { noWatch(function() {
if(previousTextContent != state.content) { if(previousTextContent != state.content) {
inputElt.value = state.content; inputElt.setValueSilently(state.content);
fileDesc.content = state.content;
eventMgr.onContentChanged(fileDesc, state.content);
previousTextContent = state.content;
} }
inputElt.setSelectionStartEnd(selectionStart, selectionEnd); inputElt.setSelectionStartEnd(selectionStart, selectionEnd);
var discussionListJSON = fileDesc.discussionListJSON; var discussionListJSON = fileDesc.discussionListJSON;
@ -501,6 +506,7 @@ define([
editor.$marginElt = $(editor.marginElt); editor.$marginElt = $(editor.marginElt);
contentObserver = new MutationObserver(checkContentChange); contentObserver = new MutationObserver(checkContentChange);
isWatching = true;
contentObserver.observe(editor.contentElt, { contentObserver.observe(editor.contentElt, {
childList: true, childList: true,
subtree: true, subtree: true,
@ -549,6 +555,18 @@ define([
} }
}); });
inputElt.setValueSilently = function(value) {
noWatch(function() {
if(!/\n$/.test(value)) {
value += '\n';
}
inputElt.value = value;
fileDesc.content = value;
previousTextContent = value;
eventMgr.onContentChanged(fileDesc, value);
});
};
Object.defineProperty(inputElt, 'selectionStart', { Object.defineProperty(inputElt, 'selectionStart', {
get: function () { get: function () {
return selectionStart; return selectionStart;

View File

@ -40,8 +40,16 @@ define([
}); });
}; };
var fileDesc;
markdownSectionParser.onFileSelected = function(fileDescParam) {
fileDesc = fileDescParam;
};
var sectionCounter = 0; var sectionCounter = 0;
function parseFileContent(fileDesc, content) { function parseFileContent(fileDescParam, content) {
if(fileDescParam !== fileDesc) {
return;
}
var frontMatter = (fileDesc.frontMatter || {})._frontMatter || ''; var frontMatter = (fileDesc.frontMatter || {})._frontMatter || '';
var text = content.substring(frontMatter.length); var text = content.substring(frontMatter.length);
var tmpText = text + "\n\n"; var tmpText = text + "\n\n";

View File

@ -18,7 +18,10 @@ define([
var regex = /^(\s*-{3}\s*\n([\w\W]+?)\n\s*-{3}\s*?\n)?([\w\W]*)$/; var regex = /^(\s*-{3}\s*\n([\w\W]+?)\n\s*-{3}\s*?\n)?([\w\W]*)$/;
function parseFrontMatter(fileDesc, content) { function parseFrontMatter(fileDescParam, content) {
if(fileDescParam !== fileDesc) {
return;
}
var results = regex.exec(content); var results = regex.exec(content);
var yaml = results[2]; var yaml = results[2];

View File

@ -671,10 +671,10 @@ define([
]; ];
utils.crc32 = function(str) { utils.crc32 = function(str) {
var n = 0, crc = -1; var n = 0, crc = -1;
for ( var i = 0; i < str.length; i++) { str.split('').forEach(function(char) {
n = (crc ^ str.charCodeAt(i)) & 0xFF; n = (crc ^ char.charCodeAt(0)) & 0xFF;
crc = (crc >>> 8) ^ mHash[n]; crc = (crc >>> 8) ^ mHash[n];
} });
crc = crc ^ (-1); crc = crc ^ (-1);
if(crc < 0) { if(crc < 0) {
crc = 0xFFFFFFFF + crc + 1; crc = 0xFFFFFFFF + crc + 1;
@ -688,7 +688,7 @@ define([
cb(); cb();
} }
console.log('Run 10,000 times in ' + (Date.now() - startTime) + 'ms'); console.log('Run 10,000 times in ' + (Date.now() - startTime) + 'ms');
} };
return utils; return utils;
}); });