diff --git a/public/res/classes/Provider.js b/public/res/classes/Provider.js index 31931f1f..a0c437bc 100644 --- a/public/res/classes/Provider.js +++ b/public/res/classes/Provider.js @@ -72,9 +72,6 @@ define([ objectHash: function(obj) { return JSON.stringify(obj); }, - arrays: { - detectMove: false, - }, textDiff: { minLength: 9999999 } @@ -82,25 +79,75 @@ define([ var merge = settings.conflictMode == 'merge'; 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; + function cleanupDiffs(diffs) { + var result = []; + var removeDiff = [-1, '']; + var addDiff = [1, '']; + var distance = 20; + diffs.forEach(function(diff) { + var diffType = diff[0]; + var diffText = diff[1]; + if(diffType === 0) { + if(diffText.length > distance) { + if(removeDiff[1] || addDiff[1]) { + var match = /\S+/.exec(diffText); + if(match) { + var prefixOffset = match.index + match[0].length; + var prefix = diffText.substring(0, prefixOffset); + diffText = diffText.substring(prefixOffset); + removeDiff[1] += prefix; + addDiff[1] += prefix; + } + } + if(diffText) { + var suffixOffset = diffText.length; + while(suffixOffset && /\S/.test(diffText[suffixOffset - 1])) { + suffixOffset--; + } + var suffix = diffText.substring(suffixOffset); + diffText = diffText.substring(0, suffixOffset); + if(diffText.length > distance) { + removeDiff[1] && result.push(removeDiff); + removeDiff = [-1, '']; + addDiff[1] && result.push(addDiff); + addDiff = [1, '']; + result.push([0, diffText]); + } + else { + removeDiff[1] += diffText; + addDiff[1] += diffText; + } + removeDiff[1] += suffix; + addDiff[1] += suffix; + } + } + else { + removeDiff[1] += diffText; + addDiff[1] += diffText; + } + } + else if(diffType === -1) { + removeDiff[1] += diffText; + } + else if(diffType === 1) { + addDiff[1] += diffText; } }); - return chars; + if(removeDiff[1] == addDiff[1]) { + result.push([0, addDiff[1]]); + } + else { + removeDiff[1] && result.push(removeDiff); + addDiff[1] && result.push(addDiff); + } + return result; } function moveComments(oldTextContent, newTextContent, discussionList) { + if(!discussionList.length) { + return; + } var changes = diffMatchPatch.diff_main(oldTextContent, newTextContent); var changed = false; var startOffset = 0; @@ -121,20 +168,20 @@ define([ // selectionEnd if(discussion.selectionEnd >= endOffset) { discussion.selectionEnd += diffOffset; - changed = true; + discussion.discussionIndex && (changed = true); } else if(discussion.selectionEnd > startOffset) { discussion.selectionEnd = startOffset; - changed = true; + discussion.discussionIndex && (changed = true); } // selectionStart if(discussion.selectionStart >= endOffset) { discussion.selectionStart += diffOffset; - changed = true; + discussion.discussionIndex && (changed = true); } else if(discussion.selectionStart > startOffset) { discussion.selectionStart = startOffset; - changed = true; + discussion.discussionIndex && (changed = true); } }); startOffset = endOffset; @@ -157,28 +204,32 @@ define([ var remoteDiscussionListCRC = utils.crc32(remoteDiscussionListJSON); // Check content - var contentChanged = localContent != remoteContent; var localContentChanged = syncAttributes.contentCRC != localContentCRC; var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; - var contentConflict = contentChanged && localContentChanged && remoteContentChanged; - contentChanged = contentChanged && remoteContentChanged; + var contentChanged = localContent != remoteContent && remoteContentChanged; + var contentConflict = contentChanged && localContentChanged; // Check title syncAttributes.titleCRC = syncAttributes.titleCRC || localTitleCRC; // Not synchronized with Dropbox - var titleChanged = localTitle != remoteTitle; var localTitleChanged = syncAttributes.titleCRC != localTitleCRC; var remoteTitleChanged = syncAttributes.titleCRC != remoteTitleCRC; - var titleConflict = titleChanged && localTitleChanged && remoteTitleChanged; - titleChanged = titleChanged && remoteTitleChanged; + var titleChanged = localTitle != remoteTitle && remoteTitleChanged; + var titleConflict = titleChanged && localTitleChanged; // Check discussionList - var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON; var localDiscussionListChanged = syncAttributes.discussionListCRC != localDiscussionListCRC; var remoteDiscussionListChanged = syncAttributes.discussionListCRC != remoteDiscussionListCRC; - var discussionListConflict = discussionListChanged && localDiscussionListChanged && remoteDiscussionListChanged; - discussionListChanged = discussionListChanged && remoteDiscussionListChanged; + var discussionListChanged = localDiscussionListJSON != remoteDiscussionListJSON && remoteDiscussionListChanged; + var discussionListConflict = discussionListChanged && localDiscussionListChanged; var conflictList = []; + var newContent = remoteContent; + var newTitle = remoteTitle; + var newDiscussionList = remoteDiscussionList; + var adjustLocalDiscussionList = false; + var adjustRemoteDiscussionList = false; + var mergeDiscussionList = false; + var diffs, patch; if( (!merge && (contentConflict || titleConflict || discussionListConflict)) || (contentConflict && syncAttributes.content === undefined) || @@ -189,25 +240,21 @@ define([ eventMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); } else { - var oldDiscussionList; - var patch, delta; if(contentConflict) { - // Patch content (line mode) + // Patch content var oldContent = syncAttributes.content; - /* - var oldContentLines = linesToChars(syncAttributes.content); - var localContentLines = linesToChars(localContent); - var remoteContentLines = linesToChars(remoteContent); - */ - patch = diffMatchPatch.patch_make(oldContent, localContent); + diffs = diffMatchPatch.diff_main(oldContent, localContent); + diffMatchPatch.diff_cleanupSemantic(diffs); + patch = diffMatchPatch.patch_make(oldContent, diffs); var patchResult = diffMatchPatch.patch_apply(patch, remoteContent); - var newContent = patchResult[0]; + newContent = patchResult[0]; if(patchResult[1].some(function(patchSuccess) { return !patchSuccess; })) { - // Conflicts (some modifications have not been applied properly) - var diffs = diffMatchPatch.diff_main(localContent, newContent); - diffMatchPatch.diff_cleanupSemantic(diffs); + // Remaining conflicts + diffs = diffMatchPatch.diff_main(localContent, newContent); + diffs = cleanupDiffs(diffs); + newContent = ''; var conflict; diffs.forEach(function(diff) { @@ -231,84 +278,105 @@ define([ conflictList.push(conflict); } } - /* - 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); - discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray); + if(contentChanged) { + if(localDiscussionListChanged) { + adjustLocalDiscussionList = true; } - 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); - } + adjustRemoteDiscussionList = true; } else { - remoteDiscussionList = localDiscussionList; + adjustLocalDiscussionList = true; + newDiscussionList = localDiscussionList; } - - if(conflictList.length) { - discussionListChanged = true; - // Add conflicts to discussionList - conflictList.forEach(function(conflict) { - // Create discussion index - var discussionIndex; - do { - discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision - } while(_.has(remoteDiscussionList, discussionIndex)); - conflict.discussionIndex = discussionIndex; - remoteDiscussionList[discussionIndex] = conflict; - }); - } - - 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(discussionListConflict) { + mergeDiscussionList = true; } + if(titleConflict) { // Patch title patch = diffMatchPatch.patch_make(syncAttributes.title, localTitle); - remoteTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0]; + newTitle = diffMatchPatch.patch_apply(patch, remoteTitle)[0]; } } + // Adjust local discussions offset + var editorSelection; + if(contentChanged) { + var localDiscussionArray = []; + // Adjust editor's cursor position and local discussions at the same time + if(fileMgr.currentFile === fileDesc) { + editorSelection = { + selectionStart: editor.inputElt.selectionStart, + selectionEnd: editor.inputElt.selectionEnd + }; + localDiscussionArray.push(editorSelection); + fileDesc.newDiscussion && localDiscussionArray.push(fileDesc.newDiscussion); + } + if(adjustLocalDiscussionList) { + localDiscussionArray = localDiscussionArray.concat(_.values(localDiscussionList)); + } + discussionListChanged |= moveComments(localContent, newContent, localDiscussionArray); + } + + // Adjust remote discussions offset + if(adjustRemoteDiscussionList) { + var remoteDiscussionArray = _.values(remoteDiscussionList); + moveComments(remoteContent, newContent, remoteDiscussionArray); + } + + // Patch remote discussionList with local modifications + if(mergeDiscussionList) { + var oldDiscussionList = JSON.parse(syncAttributes.discussionList); + diffs = jsonDiffPatch.diff(oldDiscussionList, localDiscussionList); + jsonDiffPatch.patch(remoteDiscussionList, diffs); + _.each(remoteDiscussionList, function(discussion, discussionIndex) { + if(!discussion) { + delete remoteDiscussionList[discussionIndex]; + } + }); + } + + if(conflictList.length) { + discussionListChanged = true; + // Add conflicts to discussionList + conflictList.forEach(function(conflict) { + // Create discussion index + var discussionIndex; + do { + discussionIndex = utils.randomString() + utils.randomString(); // Increased size to prevent collision + } while(_.has(newDiscussionList, discussionIndex)); + conflict.discussionIndex = discussionIndex; + newDiscussionList[discussionIndex] = conflict; + }); + } + if(titleChanged) { - fileDesc.title = remoteTitle; + fileDesc.title = newTitle; eventMgr.onTitleChanged(fileDesc); - eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + remoteTitle + '" on ' + this.providerName + '.'); + eventMgr.onMessage('"' + localTitle + '" has been renamed to "' + newTitle + '" on ' + this.providerName + '.'); } if(contentChanged || discussionListChanged) { var self = this; editor.watcher.noWatch(function() { if(contentChanged) { - if(!/\n$/.test(remoteContent)) { - remoteContent += '\n'; + if(!/\n$/.test(newContent)) { + newContent += '\n'; } if(fileMgr.currentFile === fileDesc) { - editor.setValueNoWatch(remoteContent); + editor.setValueNoWatch(newContent); + editorSelection && editor.inputElt.setSelectionStartEnd( + editorSelection.selectionStart, + editorSelection.selectionEnd + ); } - fileDesc.content = remoteContent; - eventMgr.onContentChanged(fileDesc, remoteContent); + fileDesc.content = newContent; + eventMgr.onContentChanged(fileDesc, newContent); } if(discussionListChanged) { fileDesc.discussionList = remoteDiscussionList; @@ -331,7 +399,7 @@ define([ editor.undoManager.saveState(); eventMgr.onMessage('"' + remoteTitle + '" has been updated from ' + self.providerName + '.'); if(conflictList.length) { - eventMgr.onMessage('"' + remoteTitle + '" contains conflicts you need to review.'); + eventMgr.onMessage('"' + remoteTitle + '" has conflicts that you have to review.'); } }); } diff --git a/public/res/editor.js b/public/res/editor.js index da50ddfc..082661ab 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -24,6 +24,10 @@ define([ var scrollTop = 0; var inputElt; var $inputElt; + var contentElt; + var $contentElt; + var marginElt; + var $marginElt; var previewElt; var pagedownEditor; var refreshPreviewLater = (function() { @@ -70,7 +74,7 @@ define([ this.startWatching = function() { this.isWatching = true; contentObserver = contentObserver || new MutationObserver(checkContentChange); - contentObserver.observe(editor.contentElt, { + contentObserver.observe(contentElt, { childList: true, subtree: true, characterData: true @@ -113,11 +117,14 @@ define([ } editor.setValueNoWatch = setValueNoWatch; - function setSelectionStartEnd(start, end) { + function setSelectionStartEnd(start, end, applySelection) { selectionStart = start; selectionEnd = end; fileDesc.editorStart = selectionStart; fileDesc.editorEnd = selectionEnd; + if(applySelection === false) { + return; + } var range = createRange(start, end); var selection = window.getSelection(); selection.removeAllRanges(); @@ -270,7 +277,7 @@ define([ }; this.currentMode = undefined; lastMode = undefined; - editor.contentElt.textContent = content; + contentElt.textContent = content; }; } var undoManager = new UndoManager(); @@ -393,7 +400,7 @@ define([ } function findOffset(offset) { - var walker = document.createTreeWalker(editor.contentElt, 4); + var walker = document.createTreeWalker(contentElt, 4); while(walker.nextNode()) { var text = walker.currentNode.nodeValue || ''; if (text.length > offset) { @@ -405,7 +412,7 @@ define([ offset -= text.length; } return { - element: editor.contentElt, + element: contentElt, offset: 0, error: true }; @@ -505,20 +512,26 @@ define([ editor.init = function(elt1, elt2) { inputElt = elt1; $inputElt = $(inputElt); + editor.inputElt = inputElt; + editor.$inputElt = $inputElt; + previewElt = elt2; - editor.contentElt = crel('div', { + contentElt = crel('div', { class: 'editor-content', contenteditable: true }); - inputElt.appendChild(editor.contentElt); - editor.$contentElt = $(editor.contentElt); + inputElt.appendChild(contentElt); + editor.contentElt = contentElt; + $contentElt = $(contentElt); + editor.$contentElt = $contentElt; - editor.marginElt = crel('div', { + marginElt = crel('div', { class: 'editor-margin' }); - inputElt.appendChild(editor.marginElt); - editor.$marginElt = $(editor.marginElt); + inputElt.appendChild(marginElt); + $marginElt = $(marginElt); + editor.$marginElt = $marginElt; watcher.startWatching(); @@ -535,14 +548,14 @@ define([ }); inputElt.focus = function() { - editor.$contentElt.focus(); + $contentElt.focus(); setSelectionStartEnd(selectionStart, selectionEnd); inputElt.scrollTop = scrollTop; }; - editor.$contentElt.focus(function() { + $contentElt.focus(function() { inputElt.focused = true; }); - editor.$contentElt.blur(function() { + $contentElt.blur(function() { inputElt.focused = false; }); @@ -585,7 +598,7 @@ define([ }; var clearNewline = false; - editor.$contentElt.on('keydown', function (evt) { + $contentElt.on('keydown', function (evt) { if( evt.which === 17 || // Ctrl evt.which === 91 || // Cmd @@ -742,7 +755,7 @@ define([ // Check modified section.textWithFrontMatter != newSection.textWithFrontMatter || // Check that section has not been detached or moved - section.elt.parentNode !== editor.contentElt || + section.elt.parentNode !== contentElt || // Check also the content since nodes can be injected in sections via copy/paste section.elt.textContent != newSection.textWithFrontMatter) { leftIndex = index; @@ -758,7 +771,7 @@ define([ // Check modified section.textWithFrontMatter != newSection.textWithFrontMatter || // Check that section has not been detached or moved - section.elt.parentNode !== editor.contentElt || + section.elt.parentNode !== contentElt || // Check also the content since nodes can be injected in sections via copy/paste section.elt.textContent != newSection.textWithFrontMatter) { rightIndex = -index; @@ -789,30 +802,30 @@ define([ }); watcher.noWatch(function() { if(fileChanged === true) { - editor.contentElt.innerHTML = ''; - editor.contentElt.appendChild(newSectionEltList); + contentElt.innerHTML = ''; + contentElt.appendChild(newSectionEltList); setSelectionStartEnd(selectionStart, selectionEnd); } else { // Remove outdated sections sectionsToRemove.forEach(function(section) { // section can be already removed - section.elt.parentNode === editor.contentElt && editor.contentElt.removeChild(section.elt); + section.elt.parentNode === contentElt && contentElt.removeChild(section.elt); }); if(insertBeforeSection !== undefined) { - editor.contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt); + contentElt.insertBefore(newSectionEltList, insertBeforeSection.elt); } else { - editor.contentElt.appendChild(newSectionEltList); + contentElt.appendChild(newSectionEltList); } // Remove unauthorized nodes (text nodes outside of sections or duplicated sections via copy/paste) - var childNode = editor.contentElt.firstChild; + var childNode = contentElt.firstChild; while(childNode) { var nextNode = childNode.nextSibling; if(!childNode.generated) { - editor.contentElt.removeChild(childNode); + contentElt.removeChild(childNode); } childNode = nextNode; } diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index b366cd91..9f2354d8 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -13,8 +13,8 @@ define([ var comments = new Extension("comments", 'Comments'); var commentTmpl = [ - '