From acebad8a65c597a1723aa32b4ecd38e81f6cacc1 Mon Sep 17 00:00:00 2001 From: benweet Date: Sat, 29 Mar 2014 01:22:24 +0000 Subject: [PATCH] Implemented sync merge --- public/res/classes/Provider.js | 141 +++++++++++++++++- public/res/editor.js | 42 ++++-- .../res/extensions/markdownSectionParser.js | 10 +- .../res/extensions/yamlFrontMatterParser.js | 5 +- public/res/utils.js | 8 +- 5 files changed, 181 insertions(+), 25 deletions(-) diff --git a/public/res/classes/Provider.js b/public/res/classes/Provider.js index 114237c0..04009895 100644 --- a/public/res/classes/Provider.js +++ b/public/res/classes/Provider.js @@ -75,15 +75,78 @@ define([ }); 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 var localContentCRC = utils.crc32(localContent); var localTitleCRC = utils.crc32(localTitle); - var localDiscussionListCRC = utils.crc32(localDiscussionList); + var localDiscussionListCRC = utils.crc32(localDiscussionListJSON); var remoteContentCRC = utils.crc32(remoteContent); var remoteTitleCRC = utils.crc32(remoteTitle); - var remoteDiscussionListCRC = utils.crc32(remoteDiscussionList); + var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON); // Check content var contentChanged = localContent != remoteContent; @@ -99,7 +162,7 @@ define([ var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged; // Check discussionList - var discussionListChanged = localDiscussionList != remoteDiscussionList; + var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON; var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC; var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC; var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged; @@ -111,15 +174,79 @@ define([ (titleConflict && syncAttributes.title === 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.'); } else { - if(contentConflict === true) { - var patch = diffMatchPatch.patch_make(syncAttributes.content, localContent); + var updateDiscussionList = remoteDiscussionListChanged; + 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; diff --git a/public/res/editor.js b/public/res/editor.js index bdb5e46f..162e70cc 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -64,14 +64,22 @@ define([ }); var contentObserver; + var isWatching = false; function noWatch(cb) { - contentObserver.disconnect(); - cb(); - contentObserver.observe(editor.contentElt, { - childList: true, - subtree: true, - characterData: true - }); + if(isWatching === true) { + contentObserver.disconnect(); + isWatching = false; + cb(); + isWatching = true; + contentObserver.observe(editor.contentElt, { + childList: true, + subtree: true, + characterData: true + }); + } + else { + cb(); + } } var diffMatchPatch = new diff_match_patch(); @@ -147,10 +155,7 @@ define([ // Update editor noWatch(function() { if(previousTextContent != state.content) { - inputElt.value = state.content; - fileDesc.content = state.content; - eventMgr.onContentChanged(fileDesc, state.content); - previousTextContent = state.content; + inputElt.setValueSilently(state.content); } inputElt.setSelectionStartEnd(selectionStart, selectionEnd); var discussionListJSON = fileDesc.discussionListJSON; @@ -501,6 +506,7 @@ define([ editor.$marginElt = $(editor.marginElt); contentObserver = new MutationObserver(checkContentChange); + isWatching = true; contentObserver.observe(editor.contentElt, { childList: 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', { get: function () { return selectionStart; @@ -573,7 +591,7 @@ define([ configurable: true }); - inputElt.setSelectionStartEnd = function (start, end) { + inputElt.setSelectionStartEnd = function(start, end) { selectionStart = start; selectionEnd = end; fileDesc.editorStart = selectionStart; diff --git a/public/res/extensions/markdownSectionParser.js b/public/res/extensions/markdownSectionParser.js index caac9768..8ad922ae 100644 --- a/public/res/extensions/markdownSectionParser.js +++ b/public/res/extensions/markdownSectionParser.js @@ -40,8 +40,16 @@ define([ }); }; + var fileDesc; + markdownSectionParser.onFileSelected = function(fileDescParam) { + fileDesc = fileDescParam; + }; + var sectionCounter = 0; - function parseFileContent(fileDesc, content) { + function parseFileContent(fileDescParam, content) { + if(fileDescParam !== fileDesc) { + return; + } var frontMatter = (fileDesc.frontMatter || {})._frontMatter || ''; var text = content.substring(frontMatter.length); var tmpText = text + "\n\n"; diff --git a/public/res/extensions/yamlFrontMatterParser.js b/public/res/extensions/yamlFrontMatterParser.js index 62f11f1d..b749c2c6 100644 --- a/public/res/extensions/yamlFrontMatterParser.js +++ b/public/res/extensions/yamlFrontMatterParser.js @@ -18,7 +18,10 @@ define([ 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 yaml = results[2]; diff --git a/public/res/utils.js b/public/res/utils.js index 02d03f2d..93bf1aba 100644 --- a/public/res/utils.js +++ b/public/res/utils.js @@ -671,10 +671,10 @@ define([ ]; utils.crc32 = function(str) { var n = 0, crc = -1; - for ( var i = 0; i < str.length; i++) { - n = (crc ^ str.charCodeAt(i)) & 0xFF; + str.split('').forEach(function(char) { + n = (crc ^ char.charCodeAt(0)) & 0xFF; crc = (crc >>> 8) ^ mHash[n]; - } + }); crc = crc ^ (-1); if(crc < 0) { crc = 0xFFFFFFFF + crc + 1; @@ -688,7 +688,7 @@ define([ cb(); } console.log('Run 10,000 times in ' + (Date.now() - startTime) + 'ms'); - } + }; return utils; });