From 1169156e550729b87f0c8f587f56c85483731849 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 2 Apr 2020 10:21:12 +0200 Subject: [PATCH 01/52] Replay history in order --- www/common/outer/async-store.js | 10 ++- www/common/sframe-common-history.js | 5 +- www/common/sframe-common-outer.js | 13 ++- www/debug/app-debug.less | 24 +++++- www/debug/inner.js | 119 +++++++++++++++++++++++++++- 5 files changed, 165 insertions(+), 6 deletions(-) diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 2a2fcaacf..8e2af5fb0 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1880,7 +1880,15 @@ define([ if (msg) { msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, ''); //var decryptedMsg = crypto.decrypt(msg, true); - msgs.push(msg); + if (data.debug) { + msgs.push({ + msg: msg, + author: parsed[1][1], + time: parsed[1][5] + }); + } else { + msgs.push(msg); + } } }; network.on('message', onMsg); diff --git a/www/common/sframe-common-history.js b/www/common/sframe-common-history.js index be2793128..0217fee7a 100644 --- a/www/common/sframe-common-history.js +++ b/www/common/sframe-common-history.js @@ -71,7 +71,10 @@ define([ lastKnownHash = data.lastKnownHash; isComplete = data.isFull; var messages = (data.messages || []).map(function (obj) { - return obj.msg; + if (!config.debug) { + return obj.msg; + } + return obj; }); if (config.debug) { console.log(data.messages); } Array.prototype.unshift.apply(allMessages, messages); // Destructive concat diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index f4f42707a..96ea7e564 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -835,17 +835,26 @@ define([ sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) { var crypto = Crypto.createEncryptor(secret.keys); Cryptpad.getFullHistory({ + debug: data && data.debug, channel: secret.channel, validateKey: secret.keys.validateKey }, function (encryptedMsgs) { var nt = nThen; var decryptedMsgs = []; var total = encryptedMsgs.length; - encryptedMsgs.forEach(function (msg, i) { + encryptedMsgs.forEach(function (_msg, i) { nt = nt(function (waitFor) { // The 3rd parameter "true" means we're going to skip signature validation. // We don't need it since the message is already validated serverside by hk - decryptedMsgs.push(crypto.decrypt(msg, true, true)); + if (typeof(_msg) === "object") { + decryptedMsgs.push({ + author: _msg.author, + time: _msg.time, + msg: crypto.decrypt(_msg.msg, true, true) + }); + } else { + decryptedMsgs.push(crypto.decrypt(_msg.msg, true, true)); + } setTimeout(waitFor(function () { sframeChan.event('EV_FULL_HISTORY_STATUS', (i+1)/total); })); diff --git a/www/debug/app-debug.less b/www/debug/app-debug.less index 52598cec8..697a46612 100644 --- a/www/debug/app-debug.less +++ b/www/debug/app-debug.less @@ -22,13 +22,14 @@ display: none; } #cp-app-debug-content { - margin: 50px; + margin: 10px 50px; flex-flow: column; align-items: center; justify-content: center; .cp-app-debug-content { flex: 1; min-height: 0; + justify-content: center; } .cp-app-debug-progress, .cp-app-debug-init { text-align: center; @@ -57,6 +58,27 @@ background-color: rgba(0,0,0,0.1); } } + .fa-chevron-left, .fa-chevron-right { + margin: 5px 20px; + cursor: pointer; + &:hover { + color: #777; + } + } + .cp-app-debug-progress { + display: flex; + flex: 1; + min-height: 0; + flex-flow: column; + } + pre.cp-debug-replay { + text-align: left; + white-space: pre-wrap; + word-break: break-word; + overflow: auto; + flex: 1; + min-height: 0; + } } } diff --git a/www/debug/inner.js b/www/debug/inner.js index 3a98b883e..853617c5c 100644 --- a/www/debug/inner.js +++ b/www/debug/inner.js @@ -389,6 +389,120 @@ define([ }, {timeout: 2147483647}); // Max 32-bit integer }; + var replayFullHistory = function () { + // Set spinner + var content = h('div#cp-app-debug-loading', [ + h('p', 'Loading history from the server...'), + h('span.fa.fa-circle-o-notch.fa-spin.fa-3x.fa-fw') + ]); + $('#cp-app-debug-content').html('').append(content); + var makeChainpad = function () { + return window.ChainPad.create({ + userName: 'debug', + initialState: '', + logLevel: 2, + validateContent: function (content) { + try { + JSON.parse(content); + return true; + } catch (e) { + console.log('Failed to parse, rejecting patch'); + return false; + } + }, + }); + }; + sframeChan.query('Q_GET_FULL_HISTORY', { + debug: true, + }, function (err, data) { + var replay, input, left, right; + var content = h('div.cp-app-debug-progress.cp-loading-progress', [ + h('p', [ + left = h('span.fa.fa-chevron-left'), + input = h('input', {type: 'number'}), + right = h('span.fa.fa-chevron-right'), + ]), + h('br'), + replay = h('pre.cp-debug-replay'), + ]); + var $input = $(input); + var $left = $(left); + var $right = $(right); + + $('#cp-app-debug-content').html('').append(content); + var chainpad = makeChainpad(); + console.warn(chainpad); + + var i = 0; + var messages = data.slice(); + var play = function (_i) { + if (_i < 1) { _i = 1; } + if (_i > data.length - 1) { _i = data.length - 1; } + if (_i < i) { + chainpad.abort(); + chainpad = makeChainpad(); + console.warn(chainpad); + i = 0; + } + var messages = data.slice(i, _i); + i = _i; + $input.val(i); + messages.forEach(function (obj) { + chainpad.message(obj); + }); + if (messages.length) { + var hashes = Object.keys(chainpad._.messages); + var currentHash = hashes[hashes.length - 1]; + var best = chainpad.getAuthBlock(); + var current = chainpad.getBlockForHash(currentHash); + if (best.hashOf === currentHash) { + console.log("Best", best); + } else { + console.warn("Current", current); + console.log("Best", best); + } + } + $(replay).text(JSON.stringify(JSON.parse(chainpad.getUserDoc()), 0, 2)); + }; + play(1); + $left.click(function () { + play(i-1); + }); + $right.click(function () { + play(i+1); + }); + $input.keydown(function (e) { + if (e.which === 37 || e.which === 40) { // Left or down + e.preventDefault(); + return; + } + if (e.which === 38 || e.which === 39) { // Up or right + e.preventDefault(); + return; + } + }); + $input.keyup(function (e) { + var val = Number($input.val()); + if (e.which === 37 || e.which === 40) { // Left or down + e.preventDefault(); + play(val - 1); + return; + } + if (e.which === 38 || e.which === 39) { // Up or right + e.preventDefault(); + play(val + 1); + return; + } + if (e.which !== 13) { return; } + if (!val) { + $input.val(1); + return; + } + play(Number(val)); + }); + }, {timeout: 2147483647}); // Max 32-bit integer + }; + var getContent = function () { if ($('#cp-app-debug-content').is(':visible')) { $('#cp-app-debug-content').hide(); @@ -402,11 +516,14 @@ define([ }; var setInitContent = function () { var button = h('button.btn.btn-success', 'Load history'); + var buttonReplay = h('button.btn.btn-success', 'Replay'); $(button).click(getFullHistory); + $(buttonReplay).click(replayFullHistory); var content = h('p.cp-app-debug-init', [ 'To get better debugging tools, we need to load the entire history of the document. This make take some time.', // TODO h('br'), - button + button, + buttonReplay ]); $('#cp-app-debug-content').html('').append(content); }; From 8a658336a701a59a4e27484e9dc574d6c77fce1b Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 3 Apr 2020 11:19:18 +0200 Subject: [PATCH 02/52] Fix issues with the new debugging code --- www/common/sframe-common-outer.js | 2 +- www/debug/inner.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 96ea7e564..b781ac95e 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -853,7 +853,7 @@ define([ msg: crypto.decrypt(_msg.msg, true, true) }); } else { - decryptedMsgs.push(crypto.decrypt(_msg.msg, true, true)); + decryptedMsgs.push(crypto.decrypt(_msg, true, true)); } setTimeout(waitFor(function () { sframeChan.event('EV_FULL_HISTORY_STATUS', (i+1)/total); diff --git a/www/debug/inner.js b/www/debug/inner.js index 853617c5c..8cd8a3ece 100644 --- a/www/debug/inner.js +++ b/www/debug/inner.js @@ -401,6 +401,7 @@ define([ userName: 'debug', initialState: '', logLevel: 2, + noPrune: true, validateContent: function (content) { try { JSON.parse(content); From c6fa00b14c86194c72cd6e16a1616d9515712473 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 3 Apr 2020 11:46:05 +0200 Subject: [PATCH 03/52] Add a start value to the new debug tool to simulate a user joining from a checkpoint --- www/debug/inner.js | 54 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/www/debug/inner.js b/www/debug/inner.js index 8cd8a3ece..83c7c29f0 100644 --- a/www/debug/inner.js +++ b/www/debug/inner.js @@ -420,12 +420,16 @@ define([ var content = h('div.cp-app-debug-progress.cp-loading-progress', [ h('p', [ left = h('span.fa.fa-chevron-left'), - input = h('input', {type: 'number'}), + h('label', 'Start'), + start = h('input', {type: 'number', value: 0}), + h('label', 'State'), + input = h('input', {type: 'number', min: 1}), right = h('span.fa.fa-chevron-right'), ]), h('br'), replay = h('pre.cp-debug-replay'), ]); + var $start = $(start); var $input = $(input); var $left = $(left); var $right = $(right); @@ -434,10 +438,11 @@ define([ var chainpad = makeChainpad(); console.warn(chainpad); + var start = 0; var i = 0; var messages = data.slice(); var play = function (_i) { - if (_i < 1) { _i = 1; } + if (_i < (start+1)) { _i = start + 1; } if (_i > data.length - 1) { _i = data.length - 1; } if (_i < i) { chainpad.abort(); @@ -447,6 +452,7 @@ define([ } var messages = data.slice(i, _i); i = _i; + $start.val(start); $input.val(i); messages.forEach(function (obj) { chainpad.message(obj); @@ -463,6 +469,10 @@ define([ console.log("Best", best); } } + if (!chainpad.getUserDoc()) { + $(replay).text(''); + return; + } $(replay).text(JSON.stringify(JSON.parse(chainpad.getUserDoc()), 0, 2)); }; play(1); @@ -472,14 +482,10 @@ define([ $right.click(function () { play(i+1); }); + $input.keydown(function (e) { - if (e.which === 37 || e.which === 40) { // Left or down - e.preventDefault(); - return; - } - if (e.which === 38 || e.which === 39) { // Up or right + if ([37, 38, 39, 40].indexOf(e.which) !== -1) { e.preventDefault(); - return; } }); $input.keyup(function (e) { @@ -501,6 +507,38 @@ define([ } play(Number(val)); }); + + // Initial state + $start.keydown(function (e) { + if ([37, 38, 39, 40].indexOf(e.which) !== -1) { + e.preventDefault(); + } + }); + $start.keyup(function (e) { + var val = Number($start.val()); + e.preventDefault(); + if ([37, 38, 39, 40, 13].indexOf(e.which) !== -1) { + chainpad.abort(); + chainpad = makeChainpad(); + } + if (e.which === 37 || e.which === 40) { // Left or down + start = Math.max(0, val - 1); + i = start; + play(i); + return; + } + if (e.which === 38 || e.which === 39) { // Up or right + start = Math.min(data.length - 1, val + 1); + i = start; + play(i); + return; + } + if (e.which !== 13) { return; } + start = Number(val); + if (!val) { start = 0; } + i = start; + play(i); + }); }, {timeout: 2147483647}); // Max 32-bit integer }; From 45b5eb7cac193478b97628dfe587ba93a78333b6 Mon Sep 17 00:00:00 2001 From: stoppegp Date: Tue, 7 Apr 2020 16:08:28 +0200 Subject: [PATCH 04/52] add basic author colors to code app --- www/code/inner.js | 52 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index 5b517c74c..e3e939a9a 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -300,6 +300,8 @@ define([ var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode); var markdownTb = mkMarkdownTb(editor, framework); + var authormarksUpdate = []; + var $print = $('#cp-app-code-print'); var $content = $('#cp-app-code-preview-content'); mkPrintButton(framework, $content, $print); @@ -321,6 +323,23 @@ define([ } else { CodeMirror.configureTheme(common); } + + // get user color for author marks + var authorcolor = framework._.sfCommon.getMetadataMgr().getUserData().color; + var authorcolor_r = parseInt("0x" + authorcolor.slice(1,3)); + var authorcolor_g = parseInt("0x" + authorcolor.slice(3,5)); + var authorcolor_b = parseInt("0x" + authorcolor.slice(5,7)); + var authorcolor_min = Math.min(authorcolor_r, authorcolor_g, authorcolor_b); + + // set minimal brightness for author marks and calculate color + tarMinColorVal = 180; + if (authorcolor_min < tarMinColorVal) { + facColor = (255-tarMinColorVal)/(255-authorcolor_min); + authorcolor_r = Math.floor(255-facColor*(255-authorcolor_r)); + authorcolor_g = Math.floor(255-facColor*(255-authorcolor_g)); + authorcolor_b = Math.floor(255-facColor*(255-authorcolor_b)); + authorcolor = "#" + authorcolor_r.toString(16) + authorcolor_g.toString(16) + authorcolor_b.toString(16); + } //// @@ -329,6 +348,10 @@ define([ if (highlightMode && highlightMode !== CodeMirror.highlightMode) { CodeMirror.setMode(highlightMode, evModeChange.fire); } + + // author marks will be updated in onChange-Handler + authormarksUpdate = newContent.authormarks; + CodeMirror.contentUpdate(newContent); previewPane.draw(); }); @@ -338,6 +361,18 @@ define([ var content = CodeMirror.getContent(); content.highlightMode = CodeMirror.highlightMode; previewPane.draw(); + + // get author marks + authormarks = []; + editor.getAllMarks().forEach(function (mark) { + pos = mark.find(); + css = mark.css; + if (pos != undefined && css != undefined) { + authormarks.push({from: {line: pos.from.line, ch: pos.from.ch}, to: {line: pos.to.line, ch: pos.to.ch}, color: css.replace("background-color:", "").trim()}); + } + }); + content.authormarks = authormarks; + return content; }); @@ -401,11 +436,24 @@ define([ framework.setNormalizer(function (c) { return { content: c.content, - highlightMode: c.highlightMode + highlightMode: c.highlightMode, + authormarks: c.authormarks }; }); - editor.on('change', framework.localChange); + editor.on('change', function( cm, change ) { + if (change.origin == "+input" || change.origin == "paste") { + // add new author mark if text is added. marks from removed text are removed automatically + editor.markText({line: change.from.line, ch: change.from.ch}, {line: change.from.line + change.text.length-1, ch: change.from.ch + change.text[change.text.length-1].length}, {css: "background-color: " + authorcolor}); + } else if (change.origin == "setValue") { + // on remote update: remove all marks, add new marks + editor.getAllMarks().forEach(marker => marker.clear()); + authormarksUpdate.forEach(function (mark) { + editor.markText({line: mark.from.line, ch: mark.from.ch}, {line: mark.to.line, ch: mark.to.ch}, {css: "background-color: " + mark.color}); + }); + } + framework.localChange(); + }); framework.start(); From 8696ecc692e30ff3ffd9b7f0ef618debf7a4e4c2 Mon Sep 17 00:00:00 2001 From: stoppegp Date: Tue, 7 Apr 2020 16:52:38 +0200 Subject: [PATCH 05/52] add 'remove authorcolors' button --- www/code/inner.js | 17 ++++++++++++++++- www/common/sframe-common-codemirror.js | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index e3e939a9a..545d954e4 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -300,6 +300,20 @@ define([ var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode); var markdownTb = mkMarkdownTb(editor, framework); + var $removeAuthorColorsButton = framework._.sfCommon.createButton('removeauthorcolors', true, {icon: 'fa-paint-brush', title: 'Autorenfarben entfernen'}); + framework._.toolbar.$rightside.append($removeAuthorColorsButton); + $removeAuthorColorsButton.click(function() { + selfrom = editor.getCursor("from"); + selto = editor.getCursor("to"); + if (selfrom == selto) { + editor.getAllMarks().forEach(marker => marker.clear()); + } else { + editor.findMarks(selfrom, selto).forEach(marker => marker.clear()); + } + framework.localChange(); + }); + + var authormarksLocal = []; var authormarksUpdate = []; var $print = $('#cp-app-code-print'); @@ -352,7 +366,7 @@ define([ // author marks will be updated in onChange-Handler authormarksUpdate = newContent.authormarks; - CodeMirror.contentUpdate(newContent); + CodeMirror.contentUpdate(newContent, authormarksUpdate, authormarksLocal); previewPane.draw(); }); @@ -372,6 +386,7 @@ define([ } }); content.authormarks = authormarks; + authormarksLocal = authormarks.slice(); return content; }); diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index 5ae5a4246..bedaa9191 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -418,12 +418,12 @@ define([ - exp.contentUpdate = function (newContent) { + exp.contentUpdate = function (newContent, authormarksUpdate, authormarksLocal) { var oldDoc = canonicalize(editor.getValue()); var remoteDoc = newContent.content; // setValueAndCursor triggers onLocal, even if we don't make any change to the content // and it may revert other changes (metadata) - if (oldDoc === remoteDoc) { return; } + if (oldDoc === remoteDoc && authormarksUpdate == authormarksLocal) { return; } exp.setValueAndCursor(oldDoc, remoteDoc); }; From 5f7bc9fca5203c7035da96aea0f3a57bb068f202 Mon Sep 17 00:00:00 2001 From: stoppegp Date: Tue, 7 Apr 2020 17:14:57 +0200 Subject: [PATCH 06/52] fix remove author color button when nothing is selected --- www/code/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/code/inner.js b/www/code/inner.js index 545d954e4..f155eee6a 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -305,7 +305,7 @@ define([ $removeAuthorColorsButton.click(function() { selfrom = editor.getCursor("from"); selto = editor.getCursor("to"); - if (selfrom == selto) { + if (!editor.somethingSelected() || selfrom == selto) { editor.getAllMarks().forEach(marker => marker.clear()); } else { editor.findMarks(selfrom, selto).forEach(marker => marker.clear()); From 79325b8cca475a84f99a3525e4ae44066c52d495 Mon Sep 17 00:00:00 2001 From: stoppegp Date: Tue, 7 Apr 2020 19:07:12 +0200 Subject: [PATCH 07/52] bugfix authorcolors: wrong end position on multiline edit --- www/code/inner.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/www/code/inner.js b/www/code/inner.js index f155eee6a..79b625154 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -459,7 +459,12 @@ define([ editor.on('change', function( cm, change ) { if (change.origin == "+input" || change.origin == "paste") { // add new author mark if text is added. marks from removed text are removed automatically - editor.markText({line: change.from.line, ch: change.from.ch}, {line: change.from.line + change.text.length-1, ch: change.from.ch + change.text[change.text.length-1].length}, {css: "background-color: " + authorcolor}); + if (change.text.length > 1) { + to_ch = change.text[change.text.length-1].length; + } else { + to_ch = change.from.ch + change.text[change.text.length-1].length; + } + editor.markText({line: change.from.line, ch: change.from.ch}, {line: change.from.line + change.text.length-1, ch: to_ch}, {css: "background-color: " + authorcolor}); } else if (change.origin == "setValue") { // on remote update: remove all marks, add new marks editor.getAllMarks().forEach(marker => marker.clear()); From 56031a5c14463420056af3dd1c93b464ff63d3cd Mon Sep 17 00:00:00 2001 From: stoppegp Date: Tue, 7 Apr 2020 20:30:55 +0200 Subject: [PATCH 08/52] authorcolor storage optimization, add undefined checks before access, code styling --- www/code/inner.js | 78 +++++++++++++++++++------- www/common/sframe-common-codemirror.js | 2 +- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index 79b625154..f213b13a8 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -303,12 +303,16 @@ define([ var $removeAuthorColorsButton = framework._.sfCommon.createButton('removeauthorcolors', true, {icon: 'fa-paint-brush', title: 'Autorenfarben entfernen'}); framework._.toolbar.$rightside.append($removeAuthorColorsButton); $removeAuthorColorsButton.click(function() { - selfrom = editor.getCursor("from"); - selto = editor.getCursor("to"); - if (!editor.somethingSelected() || selfrom == selto) { - editor.getAllMarks().forEach(marker => marker.clear()); + var selfrom = editor.getCursor("from"); + var selto = editor.getCursor("to"); + if (!editor.somethingSelected() || selfrom === selto) { + editor.getAllMarks().forEach(function (marker) { + marker.clear(); + }); } else { - editor.findMarks(selfrom, selto).forEach(marker => marker.clear()); + editor.findMarks(selfrom, selto).forEach(function (marker) { + marker.clear(); + }); } framework.localChange(); }); @@ -346,9 +350,9 @@ define([ var authorcolor_min = Math.min(authorcolor_r, authorcolor_g, authorcolor_b); // set minimal brightness for author marks and calculate color - tarMinColorVal = 180; + var tarMinColorVal = 180; if (authorcolor_min < tarMinColorVal) { - facColor = (255-tarMinColorVal)/(255-authorcolor_min); + var facColor = (255-tarMinColorVal)/(255-authorcolor_min); authorcolor_r = Math.floor(255-facColor*(255-authorcolor_r)); authorcolor_g = Math.floor(255-facColor*(255-authorcolor_g)); authorcolor_b = Math.floor(255-facColor*(255-authorcolor_b)); @@ -377,12 +381,21 @@ define([ previewPane.draw(); // get author marks - authormarks = []; + var authormarks = []; editor.getAllMarks().forEach(function (mark) { - pos = mark.find(); - css = mark.css; - if (pos != undefined && css != undefined) { - authormarks.push({from: {line: pos.from.line, ch: pos.from.ch}, to: {line: pos.to.line, ch: pos.to.ch}, color: css.replace("background-color:", "").trim()}); + var pos = mark.find(); + var css = mark.css; + if (pos !== undefined && css !== undefined) { + var color = css.replace("background-color:", "").trim(); + if (pos.from.line === pos.to.line) { + if ((pos.from.ch + 1) === pos.to.ch) { + authormarks.push([pos.from.line, pos.from.ch, color]); + } else { + authormarks.push([pos.from.line, pos.from.ch, pos.to.ch, color]); + } + } else { + authormarks.push([pos.from.line, pos.from.ch, pos.to.line, pos.to.ch, color]); + } } }); content.authormarks = authormarks; @@ -457,19 +470,46 @@ define([ }); editor.on('change', function( cm, change ) { - if (change.origin == "+input" || change.origin == "paste") { + if (change.origin !== undefined && change.text !== undefined && (change.origin === "+input" || change.origin === "paste")) { // add new author mark if text is added. marks from removed text are removed automatically + var to_ch_add; if (change.text.length > 1) { - to_ch = change.text[change.text.length-1].length; + to_ch_add = change.text[change.text.length-1].length; } else { - to_ch = change.from.ch + change.text[change.text.length-1].length; + to_ch_add = change.from.ch + change.text[change.text.length-1].length; } - editor.markText({line: change.from.line, ch: change.from.ch}, {line: change.from.line + change.text.length-1, ch: to_ch}, {css: "background-color: " + authorcolor}); - } else if (change.origin == "setValue") { + editor.markText({line: change.from.line, ch: change.from.ch}, {line: change.from.line + change.text.length-1, ch: to_ch_add}, {css: "background-color: " + authorcolor}); + } else if (change.origin === "setValue") { // on remote update: remove all marks, add new marks - editor.getAllMarks().forEach(marker => marker.clear()); + editor.getAllMarks().forEach(function (marker) { + marker.clear(); + }); authormarksUpdate.forEach(function (mark) { - editor.markText({line: mark.from.line, ch: mark.from.ch}, {line: mark.to.line, ch: mark.to.ch}, {css: "background-color: " + mark.color}); + var from_line; + var to_line; + var from_ch; + var to_ch; + var mark_color; + if (mark.length === 3) { + from_line = mark[0]; + to_line = mark[0]; + from_ch = mark[1]; + to_ch = mark[1]+1; + mark_color = mark[2]; + } else if (mark.length === 4) { + from_line = mark[0]; + to_line = mark[0]; + from_ch = mark[1]; + to_ch = mark[2]; + mark_color = mark[3]; + } else if (mark.length === 5) { + from_line = mark[0]; + to_line = mark[2]; + from_ch = mark[1]; + to_ch = mark[3]; + mark_color = mark[4]; + } + editor.markText({line: from_line, ch: from_ch}, {line: to_line, ch: to_ch}, {css: "background-color: " + mark_color}); }); } framework.localChange(); diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index bedaa9191..2dc0db495 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -423,7 +423,7 @@ define([ var remoteDoc = newContent.content; // setValueAndCursor triggers onLocal, even if we don't make any change to the content // and it may revert other changes (metadata) - if (oldDoc === remoteDoc && authormarksUpdate == authormarksLocal) { return; } + if (oldDoc === remoteDoc && (authormarksUpdate === undefined || authormarksLocal === undefined || JSON.stringify(authormarksUpdate) === JSON.stringify(authormarksLocal))) { return; } exp.setValueAndCursor(oldDoc, remoteDoc); }; From 272c1007dbf07c2fd0f3e83d63df0bbdb283f950 Mon Sep 17 00:00:00 2001 From: stoppegp Date: Tue, 7 Apr 2020 20:49:36 +0200 Subject: [PATCH 09/52] authorcolor storage optimizations --- www/code/inner.js | 49 ++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index f213b13a8..ae2fc5dd2 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -382,23 +382,29 @@ define([ // get author marks var authormarks = []; + var colorlist = []; editor.getAllMarks().forEach(function (mark) { var pos = mark.find(); var css = mark.css; if (pos !== undefined && css !== undefined) { var color = css.replace("background-color:", "").trim(); + var colorIndex = colorlist.indexOf(color); + if (colorIndex === -1) { + colorlist.push(color); + colorIndex = colorlist.length-1; + } if (pos.from.line === pos.to.line) { if ((pos.from.ch + 1) === pos.to.ch) { - authormarks.push([pos.from.line, pos.from.ch, color]); + authormarks.push([colorIndex, pos.from.line, pos.from.ch]); } else { - authormarks.push([pos.from.line, pos.from.ch, pos.to.ch, color]); + authormarks.push([colorIndex, pos.from.line, pos.from.ch, pos.to.ch]); } } else { - authormarks.push([pos.from.line, pos.from.ch, pos.to.line, pos.to.ch, color]); + authormarks.push([colorIndex, pos.from.line, pos.from.ch, pos.to.line, pos.to.ch]); } } }); - content.authormarks = authormarks; + content.authormarks = {marks: authormarks, colorlist: colorlist}; authormarksLocal = authormarks.slice(); return content; @@ -484,32 +490,31 @@ define([ editor.getAllMarks().forEach(function (marker) { marker.clear(); }); - authormarksUpdate.forEach(function (mark) { + authormarksUpdate.marks.forEach(function (mark) { var from_line; var to_line; var from_ch; var to_ch; - var mark_color; + var colorIndex = mark[0]; + if (authormarksUpdate.colorlist === undefined || (authormarksUpdate.colorlist.length < (colorIndex+1))) { return; } + var color = authormarksUpdate.colorlist[colorIndex]; if (mark.length === 3) { - from_line = mark[0]; - to_line = mark[0]; - from_ch = mark[1]; - to_ch = mark[1]+1; - mark_color = mark[2]; + from_line = mark[1]; + to_line = mark[1]; + from_ch = mark[2]; + to_ch = mark[2]+1; } else if (mark.length === 4) { - from_line = mark[0]; - to_line = mark[0]; - from_ch = mark[1]; - to_ch = mark[2]; - mark_color = mark[3]; - } else if (mark.length === 5) { - from_line = mark[0]; - to_line = mark[2]; - from_ch = mark[1]; + from_line = mark[1]; + to_line = mark[1]; + from_ch = mark[2]; to_ch = mark[3]; - mark_color = mark[4]; + } else if (mark.length === 5) { + from_line = mark[1]; + to_line = mark[3]; + from_ch = mark[2]; + to_ch = mark[4]; } - editor.markText({line: from_line, ch: from_ch}, {line: to_line, ch: to_ch}, {css: "background-color: " + mark_color}); + editor.markText({line: from_line, ch: from_ch}, {line: to_line, ch: to_ch}, {css: "background-color: " + color}); }); } framework.localChange(); From 62fde59a89cbc8693fb61583646e0c61ad8db292 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 9 Apr 2020 16:06:04 +0200 Subject: [PATCH 10/52] temp --- www/code/inner.js | 293 ++++++++++++++++++++----- www/common/metadata-manager.js | 27 +-- www/common/sframe-app-framework.js | 2 +- www/common/sframe-common-codemirror.js | 13 +- 4 files changed, 250 insertions(+), 85 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index ae2fc5dd2..adca461f4 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -12,6 +12,8 @@ define([ '/common/TypingTests.js', '/customize/messages.js', 'cm/lib/codemirror', + '/bower_components/chainpad/chainpad.dist.js', + 'css!cm/lib/codemirror.css', 'css!cm/addon/dialog/dialog.css', @@ -54,7 +56,8 @@ define([ Visible, TypingTest, Messages, - CMeditor) + CMeditor, + ChainPad) { window.CodeMirror = CMeditor; @@ -292,6 +295,18 @@ define([ ///////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////// + var authorUid = function (existing) { + if (!Array.isArray(existing)) { existing = []; } + var n; + var i = 0; + while (!n || existing.indexOf(n) !== -1 && i++ < 1000) { + n = Math.floor(Math.random() * 1000000); + } + // If we can't find a valid number in 1000 iterations, use 0... + if (existing.indexOf(n) !== -1) { n = 0; } + return n; + }; + var andThen2 = function (editor, CodeMirror, framework, isPresentMode) { var common = framework._.sfCommon; @@ -300,7 +315,7 @@ define([ var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode); var markdownTb = mkMarkdownTb(editor, framework); - var $removeAuthorColorsButton = framework._.sfCommon.createButton('removeauthorcolors', true, {icon: 'fa-paint-brush', title: 'Autorenfarben entfernen'}); + var $removeAuthorColorsButton = framework._.sfCommon.createButton('removeauthorcolors', true, {icon: 'fa-paint-brush', title: 'Autorenfarben entfernen'}); // XXX framework._.toolbar.$rightside.append($removeAuthorColorsButton); $removeAuthorColorsButton.click(function() { var selfrom = editor.getCursor("from"); @@ -309,6 +324,8 @@ define([ editor.getAllMarks().forEach(function (marker) { marker.clear(); }); + authormarks.authors = {}; + authormarks.marks = []; } else { editor.findMarks(selfrom, selto).forEach(function (marker) { marker.clear(); @@ -317,8 +334,39 @@ define([ framework.localChange(); }); + var authormarks = { + marks: [], + authors: {} + }; var authormarksLocal = []; - var authormarksUpdate = []; + var myAuthorId = 0; + + var MARK_OPACITY = 90; + + var addMark = function (from, to, uid) { + var author = authormarks.authors[uid] || {}; + editor.markText(from, to, { + inclusiveLeft: uid === myAuthorId, + inclusiveRight: uid === myAuthorId, + css: "background-color: " + author.color + MARK_OPACITY, + attributes: { + 'data-type': 'authormark', + 'data-uid': uid + } + }); + }; + var sortMarks = function (a, b) { + if (!Array.isArray(b)) { return -1; } + if (!Array.isArray(a)) { return 1; } + // Check line + if (a[1] < b[1]) { return -1; } + if (a[1] > b[1]) { return 1; } + // Same line: check start offset + if (a[2] < b[2]) { return -1; } + if (a[2] > b[2]) { return 1; } + return 0; + }; + var $print = $('#cp-app-code-print'); var $content = $('#cp-app-code-preview-content'); @@ -341,25 +389,55 @@ define([ } else { CodeMirror.configureTheme(common); } - - // get user color for author marks - var authorcolor = framework._.sfCommon.getMetadataMgr().getUserData().color; - var authorcolor_r = parseInt("0x" + authorcolor.slice(1,3)); - var authorcolor_g = parseInt("0x" + authorcolor.slice(3,5)); - var authorcolor_b = parseInt("0x" + authorcolor.slice(5,7)); - var authorcolor_min = Math.min(authorcolor_r, authorcolor_g, authorcolor_b); - - // set minimal brightness for author marks and calculate color - var tarMinColorVal = 180; - if (authorcolor_min < tarMinColorVal) { - var facColor = (255-tarMinColorVal)/(255-authorcolor_min); - authorcolor_r = Math.floor(255-facColor*(255-authorcolor_r)); - authorcolor_g = Math.floor(255-facColor*(255-authorcolor_g)); - authorcolor_b = Math.floor(255-facColor*(255-authorcolor_b)); - authorcolor = "#" + authorcolor_r.toString(16) + authorcolor_g.toString(16) + authorcolor_b.toString(16); - } - //// + var checkAuthors = function (userDoc) { + var chainpad = framework._.cpNfInner.chainpad; + var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); + if (!authDoc.content || !userDoc.content) { return; } + if (!authormarks || !Array.isArray(authormarks.marks)) { return; } + var oldDoc = CodeMirror.canonicalize(editor.getValue()); + var theirOps = ChainPad.Diff.diff(oldDoc, userDoc.content); + var myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content); + // If I have uncommited content when receiving a remote patch, and they have + // pushed content to the same line as me, I need to update all the authormarks + // after their changes to push them by the length of the text I added + var changed = false; + console.log(JSON.stringify(authDoc.authormarks)); + console.log(JSON.stringify(authormarks)); + console.warn(myOps); + console.warn(theirOps); + myOps.forEach(function (op) { + var pos = SFCodeMirror.posToCursor(op.offset, authDoc.content); + var size = (op.toInsert.length - op.toRemove); + + // If the remote change includes an operation on the same line, + // fix the offsets and continue to my next operation + // NOTE: we need to fix all the marks that are **after** the change with + // the bigger offset + theirOps.some(function (_op) { + var _pos = SFCodeMirror.posToCursor(_op.offset, oldDoc); + if (_pos.line !== pos.line) { return; } + + var ch = Math.max(_pos.ch, pos.ch); + // Get the marks from this line and check offsets after the change + authormarks.marks.forEach(function (array) { + if (array[1] !== pos.line) { return; } + // Move the end position if it's on the same line and after the change + if (!array[4] && array[3] >= ch) { + array[3] += size; + changed = true; + } + // Move the start position if it's after the change + if (array[2] >= ch) { + array[2] += size; + changed = true; + } + }); + return true; + }); + }); + framework.localChange(); + }; framework.onContentUpdate(function (newContent) { var highlightMode = newContent.highlightMode; @@ -367,10 +445,20 @@ define([ CodeMirror.setMode(highlightMode, evModeChange.fire); } - // author marks will be updated in onChange-Handler - authormarksUpdate = newContent.authormarks; + if (newContent.authormarks) { + authormarks = newContent.authormarks; + if (!authormarks.marks) { authormarks.marks = []; } + if (!authormarks.authors) { authormarks.authors = {}; } + } - CodeMirror.contentUpdate(newContent, authormarksUpdate, authormarksLocal); + var chainpad = framework._.cpNfInner.chainpad; + var ops = ChainPad.Diff.diff(chainpad.getAuthDoc(), chainpad.getUserDoc()); + if (ops.length) { + console.error(ops); + } + checkAuthors(newContent); + + CodeMirror.contentUpdate(newContent); //, authormarks.marks, authormarksLocal); previewPane.draw(); }); @@ -380,32 +468,53 @@ define([ content.highlightMode = CodeMirror.highlightMode; previewPane.draw(); + var colorlist = content.colorlist || {}; + // get author marks - var authormarks = []; - var colorlist = []; + var authors = authormarks.authors || {}; + var _marks = []; + var previous; editor.getAllMarks().forEach(function (mark) { var pos = mark.find(); - var css = mark.css; - if (pos !== undefined && css !== undefined) { - var color = css.replace("background-color:", "").trim(); - var colorIndex = colorlist.indexOf(color); - if (colorIndex === -1) { - colorlist.push(color); - colorIndex = colorlist.length-1; - } - if (pos.from.line === pos.to.line) { - if ((pos.from.ch + 1) === pos.to.ch) { - authormarks.push([colorIndex, pos.from.line, pos.from.ch]); - } else { - authormarks.push([colorIndex, pos.from.line, pos.from.ch, pos.to.ch]); - } - } else { - authormarks.push([colorIndex, pos.from.line, pos.from.ch, pos.to.line, pos.to.ch]); + var attributes = mark.attributes || {}; + if (!pos || attributes['data-type'] !== 'authormark') { return; } + + var uid = attributes['data-uid'] || 0; + var author = authors[uid] || {}; + + // Check if we need to merge + if (previous && previous.data && previous.data[0] === uid) { + if (previous.pos.to.line === pos.from.line + && previous.pos.to.ch === pos.from.ch) { + // Merge the marks + previous.mark.clear(); + mark.clear(); + addMark(previous.pos.from, pos.to, uid); + // Remove the data for the previous one + _marks.pop(); + // Update the position to create the new data + pos.from = previous.pos.from; } } + + var array = [uid, pos.from.line, pos.from.ch]; + if (pos.from.line === pos.to.line && pos.to.ch > (pos.from.ch+1)) { + // If there is more than 1 character, add the "to" character + array.push(pos.to.ch); + } else if (pos.from.line !== pos.to.line) { + // If the mark is on more than one line, add the "to" line data + Array.prototype.push.apply(array, [pos.to.line, pos.to.ch]); + } + _marks.push(array); + previous = { + pos: pos, + mark: mark, + data: array + }; }); - content.authormarks = {marks: authormarks, colorlist: colorlist}; - authormarksLocal = authormarks.slice(); + _marks.sort(sortMarks); + content.authormarks = {marks: _marks, authors: authormarks.authors}; + //authormarksLocal = _marks.slice(); return content; }); @@ -428,6 +537,22 @@ define([ framework.setTitleRecommender(CodeMirror.getHeadingText); + var getMyAuthorId = function () { + var existing = Object.keys(authormarks.authors || {}); + if (!common.isLoggedIn()) { return authorUid(existing); } + + var userData = common.getMetadataMgr().getUserData(); + var uid; + existing.some(function (id) { + var author = authormarks.authors[id] || {}; + if (author.curvePublic !== userData.curvePublic) { return; } + uid = Number(id); + return true; + }); + // XXX update my color? + return uid || authorUid(existing); + }; + framework.onReady(function (newPad) { editor.focus(); @@ -436,6 +561,9 @@ define([ //console.log("%s => %s", CodeMirror.highlightMode, CodeMirror.$language.val()); } + myAuthorId = getMyAuthorId(); + console.warn(myAuthorId); + var fmConfig = { dropArea: $('.CodeMirror'), body: $('body'), @@ -476,28 +604,79 @@ define([ }); editor.on('change', function( cm, change ) { - if (change.origin !== undefined && change.text !== undefined && (change.origin === "+input" || change.origin === "paste")) { + if (change.text !== undefined && (change.origin === "+input" || change.origin === "paste")) { // add new author mark if text is added. marks from removed text are removed automatically - var to_ch_add; + + // If my text is inside an existing mark: + // * if it's my mark, do nothing + // * if it's someone else's mark, break it + // We can only have one author mark at a given position, but there may be + // another mark (cursor selection...) at this position so we use ".some" + var toSplit, abort; + editor.findMarksAt(change.from).some(function (mark) { + if (!mark.attributes) { return; } + if (mark.attributes['data-type'] !== 'authormark') { return; } + if (mark.attributes['data-uid'] !== myAuthorId) { + toSplit = { + mark: mark, + uid: mark.attributes['data-uid'] + }; + } else { + // This is our mark: abort to avoid making a new one + abort = true; + } + + return true; + }); + if (abort) { return void framework.localChange(); } + + // Add my data to the doc if it's missing + if (!authormarks.authors[myAuthorId]) { + var userData = common.getMetadataMgr().getUserData(); + authormarks.authors[myAuthorId] = { + name: userData.name, + curvePublic: userData.curvePublic, + color: userData.color + } + } + + var to_add = { + line: change.from.line + change.text.length-1, + }; if (change.text.length > 1) { - to_ch_add = change.text[change.text.length-1].length; + // Multiple lines => take the length of the text added to the last line + to_add.ch = change.text[change.text.length-1].length; } else { - to_ch_add = change.from.ch + change.text[change.text.length-1].length; + // Single line => use the "from" position and add the length of the text + to_add.ch = change.from.ch + change.text[change.text.length-1].length; + } + + if (toSplit && toSplit.mark && typeof(toSplit.uid) !== "undefined") { + // Break the other user's mark if needed + var _pos = toSplit.mark.find(); + toSplit.mark.clear(); + addMark(_pos.from, change.from, toSplit.uid); // their mark, 1st part + addMark(change.from, to_add, myAuthorId); // my mark + addMark(to_add, _pos.to, toSplit.uid); // their mark, 2nd part + } else { + // Add my mark + addMark(change.from, to_add, myAuthorId); } - editor.markText({line: change.from.line, ch: change.from.ch}, {line: change.from.line + change.text.length-1, ch: to_ch_add}, {css: "background-color: " + authorcolor}); } else if (change.origin === "setValue") { // on remote update: remove all marks, add new marks editor.getAllMarks().forEach(function (marker) { - marker.clear(); + if (marker.attributes && marker.attributes['data-type'] === 'authormark') { + marker.clear(); + } }); - authormarksUpdate.marks.forEach(function (mark) { + authormarks.marks.forEach(function (mark) { var from_line; var to_line; var from_ch; var to_ch; - var colorIndex = mark[0]; - if (authormarksUpdate.colorlist === undefined || (authormarksUpdate.colorlist.length < (colorIndex+1))) { return; } - var color = authormarksUpdate.colorlist[colorIndex]; + var uid = mark[0]; + if (!authormarks.authors || !authormarks.authors[uid]) { return; } + var data = authormarks.authors[uid]; if (mark.length === 3) { from_line = mark[1]; to_line = mark[1]; @@ -514,7 +693,11 @@ define([ from_ch = mark[2]; to_ch = mark[4]; } - editor.markText({line: from_line, ch: from_ch}, {line: to_line, ch: to_ch}, {css: "background-color: " + color}); + addMark({ + line: from_line, ch: from_ch + }, { + line: to_line, ch: to_ch + }, uid); }); } framework.localChange(); diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 717705d84..70e9263e5 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -48,7 +48,6 @@ define(['json.sortify'], function (Sortify) { //title: meta.doc.defaultTitle, type: meta.doc.type, users: {}, - authors: {} }; metadataLazyObj = JSON.parse(JSON.stringify(metadataObj)); } @@ -69,6 +68,10 @@ define(['json.sortify'], function (Sortify) { } metadataObj.users = mdo; + // Clean old data + delete metadataObj.authors; + delete metadataLazyObj.authors; + // Always update the userlist in the lazy object, otherwise it may be outdated // and metadataMgr.updateMetadata() won't do anything, and so we won't push events // to the userlist UI ==> phantom viewers @@ -96,27 +99,6 @@ define(['json.sortify'], function (Sortify) { checkUpdate(lazy); }); }; - var addAuthor = function () { - if (!meta.user || !meta.user.netfluxId || !priv || !priv.edPublic) { return; } - var authors = metadataObj.authors || {}; - var old = Sortify(authors); - if (!authors[priv.edPublic]) { - authors[priv.edPublic] = { - nId: [meta.user.netfluxId], - name: meta.user.name - }; - } else { - authors[priv.edPublic].name = meta.user.name; - if (authors[priv.edPublic].nId.indexOf(meta.user.netfluxId) === -1) { - authors[priv.edPublic].nId.push(meta.user.netfluxId); - } - } - if (Sortify(authors) !== old) { - metadataObj.authors = authors; - metadataLazyObj.authors = JSON.parse(JSON.stringify(authors)); - change(); - } - }; var netfluxId; var isReady = false; @@ -225,7 +207,6 @@ define(['json.sortify'], function (Sortify) { if (isReady) { return void f(); } readyHandlers.push(f); }, - addAuthor: addAuthor, }); }; return Object.freeze({ create: create }); diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 171c58b0e..e3adfd6c7 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -290,7 +290,7 @@ define([ } if (padChange && hasChanged(content)) { - cpNfInner.metadataMgr.addAuthor(); + //cpNfInner.metadataMgr.addAuthor(); } oldContent = content; diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index e4b9e028d..bdbaa51b0 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -12,7 +12,7 @@ define([ ], function ($, Modes, Themes, Messages, UIElements, MT, Hash, Util, TextCursor, ChainPad) { var module = {}; - var cursorToPos = function(cursor, oldText) { + var cursorToPos = module.cursorToPos = function(cursor, oldText) { var cLine = cursor.line; var cCh = cursor.ch; var pos = 0; @@ -28,7 +28,7 @@ define([ return pos; }; - var posToCursor = function(position, newText) { + var posToCursor = module.posToCursor = function(position, newText) { var cursor = { line: 0, ch: 0 @@ -58,6 +58,7 @@ define([ editor.save(); var ops = ChainPad.Diff.diff(oldDoc, remoteDoc); + console.log(ops); var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { return TextCursor.transformCursor(oldCursor[attr], ops); }); @@ -415,16 +416,16 @@ define([ ///// - var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); }; + var canonicalize = exp.canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); }; - - exp.contentUpdate = function (newContent, authormarksUpdate, authormarksLocal) { + exp.contentUpdate = function (newContent) { var oldDoc = canonicalize(editor.getValue()); var remoteDoc = newContent.content; // setValueAndCursor triggers onLocal, even if we don't make any change to the content // and it may revert other changes (metadata) - if (oldDoc === remoteDoc && (authormarksUpdate === undefined || authormarksLocal === undefined || JSON.stringify(authormarksUpdate) === JSON.stringify(authormarksLocal))) { return; } + + if (oldDoc === remoteDoc) { return; } exp.setValueAndCursor(oldDoc, remoteDoc); }; From a70233d492b542462333dda37afd730d4fa469a7 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 10 Apr 2020 14:41:51 +0200 Subject: [PATCH 11/52] Fix OT errors --- www/code/inner.js | 202 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 153 insertions(+), 49 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index adca461f4..e5fe025c7 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -345,7 +345,8 @@ define([ var addMark = function (from, to, uid) { var author = authormarks.authors[uid] || {}; - editor.markText(from, to, { + uid = Number(uid); + return editor.markText(from, to, { inclusiveLeft: uid === myAuthorId, inclusiveRight: uid === myAuthorId, css: "background-color: " + author.color + MARK_OPACITY, @@ -366,7 +367,17 @@ define([ if (a[2] > b[2]) { return 1; } return 0; }; - + var parseMark = function (array) { + if (!Array.isArray(array)) { return {}; } + var multiline = typeof(array[4]) !== "undefined"; + var singleChar = typeof(array[3]) === "undefined"; + return { + startLine: array[1], + startCh: array[2], + endLine: multiline ? array[3] : array[1], + endCh: singleChar ? (array[2]+1) : array[3] + }; + }; var $print = $('#cp-app-code-print'); var $content = $('#cp-app-code-preview-content'); @@ -390,53 +401,132 @@ define([ CodeMirror.configureTheme(common); } + var oldMarks = authormarks; + // Remove marks added by OT and fix the incorrect ones + // first: data about the change with the lowest offset + // last: data about the change with the latest offset + // in the comments, "I" am "first" + var fixMarks = function (first, last) { + var toKeepEnd = []; + var toKeepStart = []; + // Get their start position compared to the authdoc + var lastOldOffset = last.offset - first.size; // (their offset minus my size) + var lastOldPos = SFCodeMirror.posToCursor(lastOldOffset, last.doc); + // Keep their changes in the marks (after their offset) + last.marks.some(function (array, i) { + var p = parseMark(array); + // End of the mark before offset? ignore + if (p.endLine < lastOldPos.line) { return; } + // Take everything from the first mark ending after the pos + if (p.endLine > lastOldPos.line || p.endCh >= lastOldPos.ch) { + toKeepEnd = last.marks.slice(i); + return true; + } + }); + // Keep my marks (based on currentDoc) before their changes + first.marks.some(function (array, i) { + var p = parseMark(array); + // End of the mark before offset? ignore + if (p.endLine < last.startLine) { return; } + // Take everything from the first mark ending after the pos + if (p.endLine > last.startLine || p.endCh >= last.startCh) { + toKeepStart = first.marks.slice(0,i); + return true; + } + }); + + console.info('to keep'); + console.info(JSON.stringify(toKeepStart)); + console.info(JSON.stringify(toKeepEnd)); + + // Fix their offset + var addLine = first.endLine - first.startLine; + var addCh = first.endCh - first.startCh; + toKeepEnd.forEach(function (array) { + // Push to correct lines + array[1] += addLine; + if (typeof(array[4]) !== "undefined") { array[3] += addLine; } + // If they have markers on my end line, push their "ch" + if (array[1] === first.endLine) { + array[2] += addCh; + // If they have no end line, it means end line === start line, + // so we also push their end offset + if (!array[4] && array[3]) { array[3] += addCh; } + } + }); + Array.prototype.push.apply(toKeepStart, toKeepEnd); + authormarks.marks = toKeepStart; + }; var checkAuthors = function (userDoc) { var chainpad = framework._.cpNfInner.chainpad; var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); if (!authDoc.content || !userDoc.content) { return; } if (!authormarks || !Array.isArray(authormarks.marks)) { return; } - var oldDoc = CodeMirror.canonicalize(editor.getValue()); - var theirOps = ChainPad.Diff.diff(oldDoc, userDoc.content); + var localDoc = CodeMirror.canonicalize(editor.getValue()); + + // Their changes are the diff between my local doc (my local changes only) + // and the userDoc (my local changes + their changes pushed to the authdoc) + var theirOps = ChainPad.Diff.diff(localDoc, userDoc.content); + // My changes are the diff between my userDoc (my local changes + their changes) + // and the authDoc (their changes only) var myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content); + // If I have uncommited content when receiving a remote patch, and they have // pushed content to the same line as me, I need to update all the authormarks // after their changes to push them by the length of the text I added - var changed = false; console.log(JSON.stringify(authDoc.authormarks)); console.log(JSON.stringify(authormarks)); console.warn(myOps); console.warn(theirOps); + var marks = authormarks.marks; + myOps.forEach(function (op) { - var pos = SFCodeMirror.posToCursor(op.offset, authDoc.content); + var pos = SFCodeMirror.posToCursor(op.offset, userDoc.content); var size = (op.toInsert.length - op.toRemove); + var pos2 = SFCodeMirror.posToCursor(op.offset+size, userDoc.content); + + var me = { + offset: op.offset, + size: size, + startLine: pos.line, + startCh: pos.ch, + endLine: pos2.line, + endCh: pos2.ch, + marks: (oldMarks && oldMarks.marks) || [], + doc: localDoc + }; - // If the remote change includes an operation on the same line, - // fix the offsets and continue to my next operation - // NOTE: we need to fix all the marks that are **after** the change with - // the bigger offset theirOps.some(function (_op) { - var _pos = SFCodeMirror.posToCursor(_op.offset, oldDoc); - if (_pos.line !== pos.line) { return; } - - var ch = Math.max(_pos.ch, pos.ch); - // Get the marks from this line and check offsets after the change - authormarks.marks.forEach(function (array) { - if (array[1] !== pos.line) { return; } - // Move the end position if it's on the same line and after the change - if (!array[4] && array[3] >= ch) { - array[3] += size; - changed = true; - } - // Move the start position if it's after the change - if (array[2] >= ch) { - array[2] += size; - changed = true; - } - }); + // XXX we need the take the first operation after my changes and the one just before my change + // XXX if they have multiple operations and one of them (not the first) + // is multiline... + + var _size = (_op.toInsert.length - _op.toRemove); + var _pos = SFCodeMirror.posToCursor(_op.offset, userDoc.content); + var _pos2 = SFCodeMirror.posToCursor(_op.offset+_size, userDoc.content); + + var them = { + offset: _op.offset, + size: _size, + startLine: _pos.line, + startCh: _pos.ch, + endLine: _pos2.line, + endCh: _pos2.ch, + marks: (authDoc.authormarks && authDoc.authormarks.marks) || [], + doc: authDoc.content + }; + + if (_op.offset > op.offset) { + console.error('me first', me, them); + fixMarks(me, them); + } else { + console.error('them first', me, them); + fixMarks(them, me); + } + console.warn(JSON.stringify(authormarks.marks)); return true; }); }); - framework.localChange(); }; framework.onContentUpdate(function (newContent) { @@ -446,6 +536,7 @@ define([ } if (newContent.authormarks) { + oldMarks = authormarks; authormarks = newContent.authormarks; if (!authormarks.marks) { authormarks.marks = []; } if (!authormarks.authors) { authormarks.authors = {}; } @@ -460,6 +551,7 @@ define([ CodeMirror.contentUpdate(newContent); //, authormarks.marks, authormarksLocal); previewPane.draw(); + framework.localChange(); }); framework.setContentGetter(function () { @@ -468,34 +560,43 @@ define([ content.highlightMode = CodeMirror.highlightMode; previewPane.draw(); - var colorlist = content.colorlist || {}; - // get author marks var authors = authormarks.authors || {}; var _marks = []; - var previous; + var all = []; + + var i = 0; editor.getAllMarks().forEach(function (mark) { var pos = mark.find(); var attributes = mark.attributes || {}; if (!pos || attributes['data-type'] !== 'authormark') { return; } - var uid = attributes['data-uid'] || 0; + var uid = Number(attributes['data-uid']) || 0; var author = authors[uid] || {}; - // Check if we need to merge - if (previous && previous.data && previous.data[0] === uid) { - if (previous.pos.to.line === pos.from.line - && previous.pos.to.ch === pos.from.ch) { - // Merge the marks - previous.mark.clear(); + all.forEach(function (obj,i) { + if (obj.uid !== uid) { return; } + if (obj.removed) { return; } + // Merge left + if (obj.pos.to.line === pos.from.line && obj.pos.to.ch === pos.from.ch) { + obj.removed = true; + _marks[obj.index] = undefined; + obj.mark.clear(); mark.clear(); - addMark(previous.pos.from, pos.to, uid); - // Remove the data for the previous one - _marks.pop(); - // Update the position to create the new data - pos.from = previous.pos.from; + mark = addMark(obj.pos.from, pos.to, uid); + pos.from = obj.pos.from; + return; } - } + // Merge right + if (obj.pos.from.line === pos.to.line && obj.pos.from.ch === pos.to.ch) { + obj.removed = true; + _marks[obj.index] = undefined; + obj.mark.clear(); + mark.clear(); + mark = addMark(pos.from, obj.pos.to, uid); + pos.to = obj.pos.to; + } + }); var array = [uid, pos.from.line, pos.from.ch]; if (pos.from.line === pos.to.line && pos.to.ch > (pos.from.ch+1)) { @@ -506,14 +607,17 @@ define([ Array.prototype.push.apply(array, [pos.to.line, pos.to.ch]); } _marks.push(array); - previous = { + all.push({ + uid: uid, pos: pos, mark: mark, - data: array - }; + index: i + }); + i++; }); _marks.sort(sortMarks); - content.authormarks = {marks: _marks, authors: authormarks.authors}; + authormarks.marks = _marks.filter(Boolean); + content.authormarks = authormarks; //authormarksLocal = _marks.slice(); return content; From d41c362d468eeb39d465d324d0e53f1b739cd4cb Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 14 Apr 2020 17:11:47 +0200 Subject: [PATCH 12/52] Fix more errors after ot --- www/code/inner.js | 227 ++++++++++++++------ www/common/sframe-chainpad-netflux-inner.js | 2 + 2 files changed, 161 insertions(+), 68 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index e5fe025c7..a000817a6 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -375,7 +375,7 @@ define([ startLine: array[1], startCh: array[2], endLine: multiline ? array[3] : array[1], - endCh: singleChar ? (array[2]+1) : array[3] + endCh: singleChar ? (array[2]+1) : (multiline ? array[4] : array[3]) }; }; @@ -406,127 +406,214 @@ define([ // first: data about the change with the lowest offset // last: data about the change with the latest offset // in the comments, "I" am "first" - var fixMarks = function (first, last) { - var toKeepEnd = []; - var toKeepStart = []; - // Get their start position compared to the authdoc - var lastOldOffset = last.offset - first.size; // (their offset minus my size) - var lastOldPos = SFCodeMirror.posToCursor(lastOldOffset, last.doc); + var fixMarks = function (first, last, content, toKeepEnd) { + console.log(first, last); + var toKeep = []; + + // Get their start position compared to the authDoc + var lastAuthOffset = last.offset + last.total; + var lastAuthPos = SFCodeMirror.posToCursor(lastAuthOffset, last.doc); + // Get their start position compared to the localDoc + var lastLocalOffset = last.offset + first.total; + var lastLocalPos = SFCodeMirror.posToCursor(lastLocalOffset, first.doc); + + console.log(lastAuthPos, lastAuthOffset); + console.log(lastLocalPos, lastLocalOffset); // Keep their changes in the marks (after their offset) last.marks.some(function (array, i) { var p = parseMark(array); // End of the mark before offset? ignore - if (p.endLine < lastOldPos.line) { return; } + if (p.endLine < lastAuthPos.line) { return; } // Take everything from the first mark ending after the pos - if (p.endLine > lastOldPos.line || p.endCh >= lastOldPos.ch) { - toKeepEnd = last.marks.slice(i); + if (p.endLine > lastAuthPos.line || p.endCh >= lastAuthPos.ch) { + toKeep = last.marks.slice(i); + last.marks.splice(i); return true; } }); // Keep my marks (based on currentDoc) before their changes + var toJoin = {}; first.marks.some(function (array, i) { var p = parseMark(array); // End of the mark before offset? ignore - if (p.endLine < last.startLine) { return; } + if (p.endLine < lastLocalPos.line) { return; } // Take everything from the first mark ending after the pos - if (p.endLine > last.startLine || p.endCh >= last.startCh) { - toKeepStart = first.marks.slice(0,i); + if (p.endLine > lastLocalPos.line || p.endCh >= lastLocalPos.ch) { + first.marks.splice(i); return true; } }); + if (first.marks.length) { + var toJoinMark = first.marks[first.marks.length - 1].slice(); + toJoin = parseMark(toJoinMark); + } console.info('to keep'); - console.info(JSON.stringify(toKeepStart)); - console.info(JSON.stringify(toKeepEnd)); - - // Fix their offset - var addLine = first.endLine - first.startLine; - var addCh = first.endCh - first.startCh; + console.info(JSON.stringify(toKeep)); + + // Add the new markers to the result + Array.prototype.unshift.apply(toKeepEnd, toKeep); + + // Fix their offset: compute added lines and added characters on the last line + // using the chainpad operation data (toInsert and toRemove) + var pos = SFCodeMirror.posToCursor(first.offset, content); + var removed = content.slice(first.offset, first.offset + first.toRemove); + var removedS = removed.split('\n'); + var addedS = first.toInsert.split('\n'); + var addLine = addedS.length - removedS.length; + var addCh = addedS[addedS.length - 1].length - removedS[removedS.length - 1].length; + if (addLine > 0) { addCh -= pos.ch; } toKeepEnd.forEach(function (array) { // Push to correct lines array[1] += addLine; if (typeof(array[4]) !== "undefined") { array[3] += addLine; } // If they have markers on my end line, push their "ch" - if (array[1] === first.endLine) { + if (array[1] === toJoin[1]) { array[2] += addCh; // If they have no end line, it means end line === start line, // so we also push their end offset if (!array[4] && array[3]) { array[3] += addCh; } } }); - Array.prototype.push.apply(toKeepStart, toKeepEnd); - authormarks.marks = toKeepStart; + + if (toKeep.length && toJoin) { + // Make sure the marks are joined correctly: + // fix the start position of the marks to keep + toKeepEnd[0][1] = toJoin.endLine; + toKeepEnd[0][2] = toJoin.endCh; + } + + console.info(JSON.stringify(toJoin)); +console.info(JSON.stringify(first.marks)); +console.info(JSON.stringify(last.marks)); + console.info(JSON.stringify(toKeepEnd)); }; var checkAuthors = function (userDoc) { var chainpad = framework._.cpNfInner.chainpad; var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); if (!authDoc.content || !userDoc.content) { return; } + if (authDoc.content === userDoc.content) { return; } // No uncommitted work if (!authormarks || !Array.isArray(authormarks.marks)) { return; } var localDoc = CodeMirror.canonicalize(editor.getValue()); + var commonParent = chainpad.getAuthBlock().getParent().getContent().doc; + console.log(chainpad); + console.log(commonParent); + var content = JSON.parse(commonParent || '{}').content || ''; + // Their changes are the diff between my local doc (my local changes only) // and the userDoc (my local changes + their changes pushed to the authdoc) - var theirOps = ChainPad.Diff.diff(localDoc, userDoc.content); + //var theirOps = ChainPad.Diff.diff(localDoc, userDoc.content); + var theirOps = ChainPad.Diff.diff(content, authDoc.content); // My changes are the diff between my userDoc (my local changes + their changes) // and the authDoc (their changes only) - var myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content); + //var myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content); + var myOps = ChainPad.Diff.diff(content, localDoc); + + if (!myOps.length || !theirOps.length) { return; } // If I have uncommited content when receiving a remote patch, and they have // pushed content to the same line as me, I need to update all the authormarks // after their changes to push them by the length of the text I added - console.log(JSON.stringify(authDoc.authormarks)); - console.log(JSON.stringify(authormarks)); + console.log(JSON.stringify(oldMarks.marks)); + console.log(JSON.stringify(authDoc.authormarks.marks)); + console.log(JSON.stringify(authormarks.marks)); console.warn(myOps); console.warn(theirOps); var marks = authormarks.marks; - myOps.forEach(function (op) { - var pos = SFCodeMirror.posToCursor(op.offset, userDoc.content); - var size = (op.toInsert.length - op.toRemove); - var pos2 = SFCodeMirror.posToCursor(op.offset+size, userDoc.content); - - var me = { - offset: op.offset, - size: size, - startLine: pos.line, - startCh: pos.ch, - endLine: pos2.line, - endCh: pos2.ch, - marks: (oldMarks && oldMarks.marks) || [], - doc: localDoc - }; - - theirOps.some(function (_op) { - // XXX we need the take the first operation after my changes and the one just before my change - // XXX if they have multiple operations and one of them (not the first) - // is multiline... - - var _size = (_op.toInsert.length - _op.toRemove); - var _pos = SFCodeMirror.posToCursor(_op.offset, userDoc.content); - var _pos2 = SFCodeMirror.posToCursor(_op.offset+_size, userDoc.content); - - var them = { - offset: _op.offset, - size: _size, - startLine: _pos.line, - startCh: _pos.ch, - endLine: _pos2.line, - endCh: _pos2.ch, - marks: (authDoc.authormarks && authDoc.authormarks.marks) || [], - doc: authDoc.content + var ops = {}; + + var myTotal = 0; + var theirTotal = 0; + var parseOp = function (me) { + return function (op) { + var size = (op.toInsert.length - op.toRemove); + /* + var pos = SFCodeMirror.posToCursor(op.offset, content); + var pos2 = SFCodeMirror.posToCursor(op.offset+size, content); + */ + + ops[op.offset] = { + me: me, + offset: op.offset, + toInsert: op.toInsert, + toRemove: op.toRemove, + size: size, + /* + size: size, + startLine: pos.line, + startCh: pos.ch, + endLine: pos2.line, + endCh: pos2.ch, + addLine: pos2.line - pos.line, + addCh: pos2.ch - pos.ch, + */ + marks: (me ? (oldMarks && oldMarks.marks) + : (authDoc.authormarks && authDoc.authormarks.marks)) || [], + doc: me ? localDoc : authDoc.content }; - if (_op.offset > op.offset) { - console.error('me first', me, them); - fixMarks(me, them); + if (me) { + myTotal += size; } else { - console.error('them first', me, them); - fixMarks(them, me); + theirTotal += size; } - console.warn(JSON.stringify(authormarks.marks)); - return true; - }); + }; + }; + myOps.forEach(parseOp(true)); + theirOps.forEach(parseOp(false)); +console.error(myTotal, theirTotal); + + /* + theirOps.map(function (_op) { + var _pos = SFCodeMirror.posToCursor(_op.offset, content); + var _size = (_op.toInsert.length - _op.toRemove); + var _pos2 = SFCodeMirror.posToCursor(_op.offset+_size, content); + + ops[_op.offset] = { + me: false, + offset: _op.offset, + size: _size, + startLine: _pos.line, + startCh: _pos.ch, + endLine: _pos2.line, + endCh: _pos2.ch, + marks: (authDoc.authormarks && authDoc.authormarks.marks) || [], + doc: authDoc.content + }; + + theirTotal += _size; }); + */ + var sorted = Object.keys(ops).map(Number); + sorted.sort().reverse(); + + console.log(sorted); + // We start from the end so that we don't have to fix the offsets everytime + var prev; + var toKeepEnd = []; + sorted.forEach(function (offset) { + var op = ops[offset]; + + // Not the same author? fix! + if (prev && prev.me !== op.me) { + prev.total = prev.me ? myTotal : theirTotal; + op.total = op.me ? myTotal : theirTotal; + fixMarks(op, prev, content, toKeepEnd); + } + + if (op.me) { myTotal -= op.size } + else { theirTotal -= op.size } + prev = op; + }); + var first = ops[sorted[sorted.length - 1]]; + console.error(JSON.stringify(first.marks)); + if (first) { + Array.prototype.unshift.apply(toKeepEnd, first.marks); + } + console.error(JSON.stringify(toKeepEnd)); + authormarks.marks = toKeepEnd; }; framework.onContentUpdate(function (newContent) { @@ -535,6 +622,10 @@ define([ CodeMirror.setMode(highlightMode, evModeChange.fire); } + var chainpad = framework._.cpNfInner.chainpad; + console.error(chainpad._.authDoc); + console.warn(chainpad._.uncommitted); + console.error(authormarks.marks, oldMarks.marks); if (newContent.authormarks) { oldMarks = authormarks; authormarks = newContent.authormarks; diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index b6f4c6aaf..f87c92d36 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -65,6 +65,7 @@ define([ sframeChan.query('Q_RT_MESSAGE', message, function (_err, obj) { var err = _err || (obj && obj.error); if (!err) { evPatchSent.fire(); } + console.error('cb', message); cb(err); }, { timeout: -1 }); }); @@ -137,6 +138,7 @@ define([ if (isReady) { onLocal(true); // should be onBeforeMessage } + console.error('received', content); chainpad.message(content); if (isHistory && updateLoadingProgress) { updateLoadingProgress({ From 88760fde6af7449b25c4b4af0974d61abac64d54 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 15 Apr 2020 13:22:46 +0200 Subject: [PATCH 13/52] Clean the code --- www/code/inner.js | 491 +------------------- www/code/markers.js | 471 +++++++++++++++++++ www/common/sframe-chainpad-netflux-inner.js | 2 - www/common/sframe-common-codemirror.js | 1 - 4 files changed, 494 insertions(+), 471 deletions(-) create mode 100644 www/code/markers.js diff --git a/www/code/inner.js b/www/code/inner.js index a000817a6..11f0fbf7e 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -7,12 +7,12 @@ define([ '/common/sframe-common-codemirror.js', '/common/common-util.js', '/common/common-hash.js', + '/code/markers.js', '/common/modes.js', '/common/visible.js', '/common/TypingTests.js', '/customize/messages.js', 'cm/lib/codemirror', - '/bower_components/chainpad/chainpad.dist.js', 'css!cm/lib/codemirror.css', @@ -52,12 +52,12 @@ define([ SFCodeMirror, Util, Hash, + Markers, Modes, Visible, TypingTest, Messages, - CMeditor, - ChainPad) + CMeditor) { window.CodeMirror = CMeditor; @@ -295,18 +295,6 @@ define([ ///////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////// - var authorUid = function (existing) { - if (!Array.isArray(existing)) { existing = []; } - var n; - var i = 0; - while (!n || existing.indexOf(n) !== -1 && i++ < 1000) { - n = Math.floor(Math.random() * 1000000); - } - // If we can't find a valid number in 1000 iterations, use 0... - if (existing.indexOf(n) !== -1) { n = 0; } - return n; - }; - var andThen2 = function (editor, CodeMirror, framework, isPresentMode) { var common = framework._.sfCommon; @@ -315,6 +303,13 @@ define([ var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode); var markdownTb = mkMarkdownTb(editor, framework); + var markers = Markers.create({ + common: common, + framework: framework, + CodeMirror: CodeMirror, + editor: editor + }); + var $removeAuthorColorsButton = framework._.sfCommon.createButton('removeauthorcolors', true, {icon: 'fa-paint-brush', title: 'Autorenfarben entfernen'}); // XXX framework._.toolbar.$rightside.append($removeAuthorColorsButton); $removeAuthorColorsButton.click(function() { @@ -334,51 +329,6 @@ define([ framework.localChange(); }); - var authormarks = { - marks: [], - authors: {} - }; - var authormarksLocal = []; - var myAuthorId = 0; - - var MARK_OPACITY = 90; - - var addMark = function (from, to, uid) { - var author = authormarks.authors[uid] || {}; - uid = Number(uid); - return editor.markText(from, to, { - inclusiveLeft: uid === myAuthorId, - inclusiveRight: uid === myAuthorId, - css: "background-color: " + author.color + MARK_OPACITY, - attributes: { - 'data-type': 'authormark', - 'data-uid': uid - } - }); - }; - var sortMarks = function (a, b) { - if (!Array.isArray(b)) { return -1; } - if (!Array.isArray(a)) { return 1; } - // Check line - if (a[1] < b[1]) { return -1; } - if (a[1] > b[1]) { return 1; } - // Same line: check start offset - if (a[2] < b[2]) { return -1; } - if (a[2] > b[2]) { return 1; } - return 0; - }; - var parseMark = function (array) { - if (!Array.isArray(array)) { return {}; } - var multiline = typeof(array[4]) !== "undefined"; - var singleChar = typeof(array[3]) === "undefined"; - return { - startLine: array[1], - startCh: array[2], - endLine: multiline ? array[3] : array[1], - endCh: singleChar ? (array[2]+1) : (multiline ? array[4] : array[3]) - }; - }; - var $print = $('#cp-app-code-print'); var $content = $('#cp-app-code-preview-content'); mkPrintButton(framework, $content, $print); @@ -401,247 +351,22 @@ define([ CodeMirror.configureTheme(common); } - var oldMarks = authormarks; - // Remove marks added by OT and fix the incorrect ones - // first: data about the change with the lowest offset - // last: data about the change with the latest offset - // in the comments, "I" am "first" - var fixMarks = function (first, last, content, toKeepEnd) { - console.log(first, last); - var toKeep = []; - - // Get their start position compared to the authDoc - var lastAuthOffset = last.offset + last.total; - var lastAuthPos = SFCodeMirror.posToCursor(lastAuthOffset, last.doc); - // Get their start position compared to the localDoc - var lastLocalOffset = last.offset + first.total; - var lastLocalPos = SFCodeMirror.posToCursor(lastLocalOffset, first.doc); - - console.log(lastAuthPos, lastAuthOffset); - console.log(lastLocalPos, lastLocalOffset); - // Keep their changes in the marks (after their offset) - last.marks.some(function (array, i) { - var p = parseMark(array); - // End of the mark before offset? ignore - if (p.endLine < lastAuthPos.line) { return; } - // Take everything from the first mark ending after the pos - if (p.endLine > lastAuthPos.line || p.endCh >= lastAuthPos.ch) { - toKeep = last.marks.slice(i); - last.marks.splice(i); - return true; - } - }); - // Keep my marks (based on currentDoc) before their changes - var toJoin = {}; - first.marks.some(function (array, i) { - var p = parseMark(array); - // End of the mark before offset? ignore - if (p.endLine < lastLocalPos.line) { return; } - // Take everything from the first mark ending after the pos - if (p.endLine > lastLocalPos.line || p.endCh >= lastLocalPos.ch) { - first.marks.splice(i); - return true; - } - }); - if (first.marks.length) { - var toJoinMark = first.marks[first.marks.length - 1].slice(); - toJoin = parseMark(toJoinMark); - } - - console.info('to keep'); - console.info(JSON.stringify(toKeep)); - - // Add the new markers to the result - Array.prototype.unshift.apply(toKeepEnd, toKeep); - - // Fix their offset: compute added lines and added characters on the last line - // using the chainpad operation data (toInsert and toRemove) - var pos = SFCodeMirror.posToCursor(first.offset, content); - var removed = content.slice(first.offset, first.offset + first.toRemove); - var removedS = removed.split('\n'); - var addedS = first.toInsert.split('\n'); - var addLine = addedS.length - removedS.length; - var addCh = addedS[addedS.length - 1].length - removedS[removedS.length - 1].length; - if (addLine > 0) { addCh -= pos.ch; } - toKeepEnd.forEach(function (array) { - // Push to correct lines - array[1] += addLine; - if (typeof(array[4]) !== "undefined") { array[3] += addLine; } - // If they have markers on my end line, push their "ch" - if (array[1] === toJoin[1]) { - array[2] += addCh; - // If they have no end line, it means end line === start line, - // so we also push their end offset - if (!array[4] && array[3]) { array[3] += addCh; } - } - }); - - if (toKeep.length && toJoin) { - // Make sure the marks are joined correctly: - // fix the start position of the marks to keep - toKeepEnd[0][1] = toJoin.endLine; - toKeepEnd[0][2] = toJoin.endCh; - } - - console.info(JSON.stringify(toJoin)); -console.info(JSON.stringify(first.marks)); -console.info(JSON.stringify(last.marks)); - console.info(JSON.stringify(toKeepEnd)); - }; - var checkAuthors = function (userDoc) { - var chainpad = framework._.cpNfInner.chainpad; - var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); - if (!authDoc.content || !userDoc.content) { return; } - if (authDoc.content === userDoc.content) { return; } // No uncommitted work - if (!authormarks || !Array.isArray(authormarks.marks)) { return; } - var localDoc = CodeMirror.canonicalize(editor.getValue()); - - var commonParent = chainpad.getAuthBlock().getParent().getContent().doc; - console.log(chainpad); - console.log(commonParent); - var content = JSON.parse(commonParent || '{}').content || ''; - - // Their changes are the diff between my local doc (my local changes only) - // and the userDoc (my local changes + their changes pushed to the authdoc) - //var theirOps = ChainPad.Diff.diff(localDoc, userDoc.content); - var theirOps = ChainPad.Diff.diff(content, authDoc.content); - // My changes are the diff between my userDoc (my local changes + their changes) - // and the authDoc (their changes only) - //var myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content); - var myOps = ChainPad.Diff.diff(content, localDoc); - - if (!myOps.length || !theirOps.length) { return; } - - // If I have uncommited content when receiving a remote patch, and they have - // pushed content to the same line as me, I need to update all the authormarks - // after their changes to push them by the length of the text I added - console.log(JSON.stringify(oldMarks.marks)); - console.log(JSON.stringify(authDoc.authormarks.marks)); - console.log(JSON.stringify(authormarks.marks)); - console.warn(myOps); - console.warn(theirOps); - var marks = authormarks.marks; - - var ops = {}; - - var myTotal = 0; - var theirTotal = 0; - var parseOp = function (me) { - return function (op) { - var size = (op.toInsert.length - op.toRemove); - /* - var pos = SFCodeMirror.posToCursor(op.offset, content); - var pos2 = SFCodeMirror.posToCursor(op.offset+size, content); - */ - - ops[op.offset] = { - me: me, - offset: op.offset, - toInsert: op.toInsert, - toRemove: op.toRemove, - size: size, - /* - size: size, - startLine: pos.line, - startCh: pos.ch, - endLine: pos2.line, - endCh: pos2.ch, - addLine: pos2.line - pos.line, - addCh: pos2.ch - pos.ch, - */ - marks: (me ? (oldMarks && oldMarks.marks) - : (authDoc.authormarks && authDoc.authormarks.marks)) || [], - doc: me ? localDoc : authDoc.content - }; - - if (me) { - myTotal += size; - } else { - theirTotal += size; - } - }; - }; - myOps.forEach(parseOp(true)); - theirOps.forEach(parseOp(false)); -console.error(myTotal, theirTotal); - - /* - theirOps.map(function (_op) { - var _pos = SFCodeMirror.posToCursor(_op.offset, content); - var _size = (_op.toInsert.length - _op.toRemove); - var _pos2 = SFCodeMirror.posToCursor(_op.offset+_size, content); - - ops[_op.offset] = { - me: false, - offset: _op.offset, - size: _size, - startLine: _pos.line, - startCh: _pos.ch, - endLine: _pos2.line, - endCh: _pos2.ch, - marks: (authDoc.authormarks && authDoc.authormarks.marks) || [], - doc: authDoc.content - }; - - theirTotal += _size; - }); - */ - var sorted = Object.keys(ops).map(Number); - sorted.sort().reverse(); - - console.log(sorted); - // We start from the end so that we don't have to fix the offsets everytime - var prev; - var toKeepEnd = []; - sorted.forEach(function (offset) { - var op = ops[offset]; - - // Not the same author? fix! - if (prev && prev.me !== op.me) { - prev.total = prev.me ? myTotal : theirTotal; - op.total = op.me ? myTotal : theirTotal; - fixMarks(op, prev, content, toKeepEnd); - } - - if (op.me) { myTotal -= op.size } - else { theirTotal -= op.size } - prev = op; - }); - var first = ops[sorted[sorted.length - 1]]; - console.error(JSON.stringify(first.marks)); - if (first) { - Array.prototype.unshift.apply(toKeepEnd, first.marks); - } - console.error(JSON.stringify(toKeepEnd)); - authormarks.marks = toKeepEnd; - }; - framework.onContentUpdate(function (newContent) { var highlightMode = newContent.highlightMode; if (highlightMode && highlightMode !== CodeMirror.highlightMode) { CodeMirror.setMode(highlightMode, evModeChange.fire); } - var chainpad = framework._.cpNfInner.chainpad; - console.error(chainpad._.authDoc); - console.warn(chainpad._.uncommitted); - console.error(authormarks.marks, oldMarks.marks); - if (newContent.authormarks) { - oldMarks = authormarks; - authormarks = newContent.authormarks; - if (!authormarks.marks) { authormarks.marks = []; } - if (!authormarks.authors) { authormarks.authors = {}; } - } + // Fix the markers offsets + markers.checkAuthors(newContent); - var chainpad = framework._.cpNfInner.chainpad; - var ops = ChainPad.Diff.diff(chainpad.getAuthDoc(), chainpad.getUserDoc()); - if (ops.length) { - console.error(ops); - } - checkAuthors(newContent); - - CodeMirror.contentUpdate(newContent); //, authormarks.marks, authormarksLocal); + // Apply the text content + CodeMirror.contentUpdate(newContent); previewPane.draw(); + + // Apply the markers + markers.setMarks(); + framework.localChange(); }); @@ -651,65 +376,8 @@ console.error(myTotal, theirTotal); content.highlightMode = CodeMirror.highlightMode; previewPane.draw(); - // get author marks - var authors = authormarks.authors || {}; - var _marks = []; - var all = []; - - var i = 0; - editor.getAllMarks().forEach(function (mark) { - var pos = mark.find(); - var attributes = mark.attributes || {}; - if (!pos || attributes['data-type'] !== 'authormark') { return; } - - var uid = Number(attributes['data-uid']) || 0; - var author = authors[uid] || {}; - - all.forEach(function (obj,i) { - if (obj.uid !== uid) { return; } - if (obj.removed) { return; } - // Merge left - if (obj.pos.to.line === pos.from.line && obj.pos.to.ch === pos.from.ch) { - obj.removed = true; - _marks[obj.index] = undefined; - obj.mark.clear(); - mark.clear(); - mark = addMark(obj.pos.from, pos.to, uid); - pos.from = obj.pos.from; - return; - } - // Merge right - if (obj.pos.from.line === pos.to.line && obj.pos.from.ch === pos.to.ch) { - obj.removed = true; - _marks[obj.index] = undefined; - obj.mark.clear(); - mark.clear(); - mark = addMark(pos.from, obj.pos.to, uid); - pos.to = obj.pos.to; - } - }); - - var array = [uid, pos.from.line, pos.from.ch]; - if (pos.from.line === pos.to.line && pos.to.ch > (pos.from.ch+1)) { - // If there is more than 1 character, add the "to" character - array.push(pos.to.ch); - } else if (pos.from.line !== pos.to.line) { - // If the mark is on more than one line, add the "to" line data - Array.prototype.push.apply(array, [pos.to.line, pos.to.ch]); - } - _marks.push(array); - all.push({ - uid: uid, - pos: pos, - mark: mark, - index: i - }); - i++; - }); - _marks.sort(sortMarks); - authormarks.marks = _marks.filter(Boolean); - content.authormarks = authormarks; - //authormarksLocal = _marks.slice(); + markers.updateAuthorMarks(); + content.authormarks = markers.getAuthorMarks(); return content; }); @@ -732,22 +400,6 @@ console.error(myTotal, theirTotal); framework.setTitleRecommender(CodeMirror.getHeadingText); - var getMyAuthorId = function () { - var existing = Object.keys(authormarks.authors || {}); - if (!common.isLoggedIn()) { return authorUid(existing); } - - var userData = common.getMetadataMgr().getUserData(); - var uid; - existing.some(function (id) { - var author = authormarks.authors[id] || {}; - if (author.curvePublic !== userData.curvePublic) { return; } - uid = Number(id); - return true; - }); - // XXX update my color? - return uid || authorUid(existing); - }; - framework.onReady(function (newPad) { editor.focus(); @@ -756,8 +408,7 @@ console.error(myTotal, theirTotal); //console.log("%s => %s", CodeMirror.highlightMode, CodeMirror.$language.val()); } - myAuthorId = getMyAuthorId(); - console.warn(myAuthorId); + markers.ready(); var fmConfig = { dropArea: $('.CodeMirror'), @@ -776,7 +427,7 @@ console.error(myTotal, theirTotal); }); framework.onDefaultContentNeeded(function () { - editor.setValue(''); //Messages.codeInitialState); + editor.setValue(''); }); framework.setFileExporter(CodeMirror.getContentExtension, CodeMirror.fileExporter); @@ -799,103 +450,7 @@ console.error(myTotal, theirTotal); }); editor.on('change', function( cm, change ) { - if (change.text !== undefined && (change.origin === "+input" || change.origin === "paste")) { - // add new author mark if text is added. marks from removed text are removed automatically - - // If my text is inside an existing mark: - // * if it's my mark, do nothing - // * if it's someone else's mark, break it - // We can only have one author mark at a given position, but there may be - // another mark (cursor selection...) at this position so we use ".some" - var toSplit, abort; - editor.findMarksAt(change.from).some(function (mark) { - if (!mark.attributes) { return; } - if (mark.attributes['data-type'] !== 'authormark') { return; } - if (mark.attributes['data-uid'] !== myAuthorId) { - toSplit = { - mark: mark, - uid: mark.attributes['data-uid'] - }; - } else { - // This is our mark: abort to avoid making a new one - abort = true; - } - - return true; - }); - if (abort) { return void framework.localChange(); } - - // Add my data to the doc if it's missing - if (!authormarks.authors[myAuthorId]) { - var userData = common.getMetadataMgr().getUserData(); - authormarks.authors[myAuthorId] = { - name: userData.name, - curvePublic: userData.curvePublic, - color: userData.color - } - } - - var to_add = { - line: change.from.line + change.text.length-1, - }; - if (change.text.length > 1) { - // Multiple lines => take the length of the text added to the last line - to_add.ch = change.text[change.text.length-1].length; - } else { - // Single line => use the "from" position and add the length of the text - to_add.ch = change.from.ch + change.text[change.text.length-1].length; - } - - if (toSplit && toSplit.mark && typeof(toSplit.uid) !== "undefined") { - // Break the other user's mark if needed - var _pos = toSplit.mark.find(); - toSplit.mark.clear(); - addMark(_pos.from, change.from, toSplit.uid); // their mark, 1st part - addMark(change.from, to_add, myAuthorId); // my mark - addMark(to_add, _pos.to, toSplit.uid); // their mark, 2nd part - } else { - // Add my mark - addMark(change.from, to_add, myAuthorId); - } - } else if (change.origin === "setValue") { - // on remote update: remove all marks, add new marks - editor.getAllMarks().forEach(function (marker) { - if (marker.attributes && marker.attributes['data-type'] === 'authormark') { - marker.clear(); - } - }); - authormarks.marks.forEach(function (mark) { - var from_line; - var to_line; - var from_ch; - var to_ch; - var uid = mark[0]; - if (!authormarks.authors || !authormarks.authors[uid]) { return; } - var data = authormarks.authors[uid]; - if (mark.length === 3) { - from_line = mark[1]; - to_line = mark[1]; - from_ch = mark[2]; - to_ch = mark[2]+1; - } else if (mark.length === 4) { - from_line = mark[1]; - to_line = mark[1]; - from_ch = mark[2]; - to_ch = mark[3]; - } else if (mark.length === 5) { - from_line = mark[1]; - to_line = mark[3]; - from_ch = mark[2]; - to_ch = mark[4]; - } - addMark({ - line: from_line, ch: from_ch - }, { - line: to_line, ch: to_ch - }, uid); - }); - } - framework.localChange(); + markers.localChange(change, framework.localChange); }); framework.start(); diff --git a/www/code/markers.js b/www/code/markers.js new file mode 100644 index 000000000..2092df781 --- /dev/null +++ b/www/code/markers.js @@ -0,0 +1,471 @@ +define([ + '/common/common-util.js', + '/common/sframe-common-codemirror.js', + '/customize/messages.js', + '/bower_components/chainpad/chainpad.dist.js', +], function (Util, SFCodeMirror, Messages, ChainPad) { + var Markers = {}; + + Messages.cba_writtenBy = 'Written by {0}'; + + var MARK_OPACITY = 90; + var addMark = function (Env, from, to, uid) { + var author = Env.authormarks.authors[uid] || {}; + uid = Number(uid); + var name = Util.fixHTML(author.name || Messages.anonymous); + return Env.editor.markText(from, to, { + inclusiveLeft: uid === Env.myAuthorId, + inclusiveRight: uid === Env.myAuthorId, + css: "background-color: " + author.color + MARK_OPACITY, + attributes: { + title: Messages._getKey('cba_writtenBy', [name]), + 'data-type': 'authormark', + 'data-uid': uid + } + }); + }; + var sortMarks = function (a, b) { + if (!Array.isArray(b)) { return -1; } + if (!Array.isArray(a)) { return 1; } + // Check line + if (a[1] < b[1]) { return -1; } + if (a[1] > b[1]) { return 1; } + // Same line: check start offset + if (a[2] < b[2]) { return -1; } + if (a[2] > b[2]) { return 1; } + return 0; + }; + var parseMark = Markers.parseMark = function (array) { + if (!Array.isArray(array)) { return {}; } + var multiline = typeof(array[4]) !== "undefined"; + var singleChar = typeof(array[3]) === "undefined"; + return { + startLine: array[1], + startCh: array[2], + endLine: multiline ? array[3] : array[1], + endCh: singleChar ? (array[2]+1) : (multiline ? array[4] : array[3]) + }; + }; + + var setAuthorMarks = function (Env, authormarks) { + authormarks = authormarks || {}; + if (!authormarks.marks) { authormarks.marks = []; } + if (!authormarks.authors) { authormarks.authors = {}; } + Env.oldMarks = Env.authormarks; + Env.authormarks = authormarks; + }; + + var getAuthorMarks = function (Env) { + return Env.authormarks; + }; + + var updateAuthorMarks = function (Env) { + // get author marks + var _marks = []; + var all = []; + + var i = 0; + Env.editor.getAllMarks().forEach(function (mark) { + var pos = mark.find(); + var attributes = mark.attributes || {}; + if (!pos || attributes['data-type'] !== 'authormark') { return; } + + var uid = Number(attributes['data-uid']) || 0; + + all.forEach(function (obj) { + if (obj.uid !== uid) { return; } + if (obj.removed) { return; } + // Merge left + if (obj.pos.to.line === pos.from.line && obj.pos.to.ch === pos.from.ch) { + obj.removed = true; + _marks[obj.index] = undefined; + obj.mark.clear(); + mark.clear(); + mark = addMark(Env, obj.pos.from, pos.to, uid); + pos.from = obj.pos.from; + return; + } + // Merge right + if (obj.pos.from.line === pos.to.line && obj.pos.from.ch === pos.to.ch) { + obj.removed = true; + _marks[obj.index] = undefined; + obj.mark.clear(); + mark.clear(); + mark = addMark(Env, pos.from, obj.pos.to, uid); + pos.to = obj.pos.to; + } + }); + + var array = [uid, pos.from.line, pos.from.ch]; + if (pos.from.line === pos.to.line && pos.to.ch > (pos.from.ch+1)) { + // If there is more than 1 character, add the "to" character + array.push(pos.to.ch); + } else if (pos.from.line !== pos.to.line) { + // If the mark is on more than one line, add the "to" line data + Array.prototype.push.apply(array, [pos.to.line, pos.to.ch]); + } + _marks.push(array); + all.push({ + uid: uid, + pos: pos, + mark: mark, + index: i + }); + i++; + }); + _marks.sort(sortMarks); + Env.authormarks.marks = _marks.filter(Boolean); + }; + + // Remove marks added by OT and fix the incorrect ones + // first: data about the change with the lowest offset + // last: data about the change with the latest offset + // in the comments, "I" am "first" + var fixMarks = function (first, last, content, toKeepEnd) { + var toKeep = []; + + // Get their start position compared to the authDoc + var lastAuthOffset = last.offset + last.total; + var lastAuthPos = SFCodeMirror.posToCursor(lastAuthOffset, last.doc); + // Get their start position compared to the localDoc + var lastLocalOffset = last.offset + first.total; + var lastLocalPos = SFCodeMirror.posToCursor(lastLocalOffset, first.doc); + + // Keep their changes in the marks (after their offset) + last.marks.some(function (array, i) { + var p = parseMark(array); + // End of the mark before offset? ignore + if (p.endLine < lastAuthPos.line) { return; } + // Take everything from the first mark ending after the pos + if (p.endLine > lastAuthPos.line || p.endCh >= lastAuthPos.ch) { + toKeep = last.marks.slice(i); + last.marks.splice(i); + return true; + } + }); + // Keep my marks (based on currentDoc) before their changes + var toJoin = {}; + first.marks.some(function (array, i) { + var p = parseMark(array); + // End of the mark before offset? ignore + if (p.endLine < lastLocalPos.line) { return; } + // Take everything from the first mark ending after the pos + if (p.endLine > lastLocalPos.line || p.endCh >= lastLocalPos.ch) { + first.marks.splice(i); + return true; + } + }); + + // If we still have markers in "first", store the last one so that we can "join" + // everything at the end + if (first.marks.length) { + var toJoinMark = first.marks[first.marks.length - 1].slice(); + toJoin = parseMark(toJoinMark); + } + + // Add the new markers to the result + Array.prototype.unshift.apply(toKeepEnd, toKeep); + + // Fix their offset: compute added lines and added characters on the last line + // using the chainpad operation data (toInsert and toRemove) + var pos = SFCodeMirror.posToCursor(first.offset, content); + var removed = content.slice(first.offset, first.offset + first.toRemove).split('\n'); + var added = first.toInsert.split('\n'); + var addLine = added.length - removed.length; + var addCh = added[added.length - 1].length - removed[removed.length - 1].length; + if (addLine > 0) { addCh -= pos.ch; } + toKeepEnd.forEach(function (array) { + // Push to correct lines + array[1] += addLine; + if (typeof(array[4]) !== "undefined") { array[3] += addLine; } + // If they have markers on my end line, push their "ch" + if (array[1] === toJoin[1]) { + array[2] += addCh; + // If they have no end line, it means end line === start line, + // so we also push their end offset + if (!array[4] && array[3]) { array[3] += addCh; } + } + }); + + if (toKeep.length && toJoin) { + // Make sure the marks are joined correctly: + // fix the start position of the marks to keep + toKeepEnd[0][1] = toJoin.endLine; + toKeepEnd[0][2] = toJoin.endCh; + } + }; + + var checkAuthors = function (Env, userDoc) { + var chainpad = Env.framework._.cpNfInner.chainpad; + var editor = Env.editor; + var CodeMirror = Env.CodeMirror; + var oldMarks = Env.oldMarks; + + setAuthorMarks(Env, userDoc.authormarks); + + var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); + if (!authDoc.content || !userDoc.content) { return; } + if (authDoc.content === userDoc.content) { return; } // No uncommitted work + + if (!userDoc.authormarks || !Array.isArray(userDoc.authormarks.marks)) { return; } + + var localDoc = CodeMirror.canonicalize(editor.getValue()); + + var commonParent = chainpad.getAuthBlock().getParent().getContent().doc; + var content = JSON.parse(commonParent || '{}').content || ''; + + var theirOps = ChainPad.Diff.diff(content, authDoc.content); + var myOps = ChainPad.Diff.diff(content, localDoc); + + if (!myOps.length || !theirOps.length) { return; } + + // If I have uncommited content when receiving a remote patch, all the operations + // placed after someone else's changes will create marker issues. We have to fix it + var ops = {}; + + var myTotal = 0; + var theirTotal = 0; + var parseOp = function (me) { + return function (op) { + var size = (op.toInsert.length - op.toRemove); + + ops[op.offset] = { + me: me, + offset: op.offset, + toInsert: op.toInsert, + toRemove: op.toRemove, + size: size, + marks: (me ? (oldMarks && oldMarks.marks) + : (authDoc.authormarks && authDoc.authormarks.marks)) || [], + doc: me ? localDoc : authDoc.content + }; + + if (me) { myTotal += size; } + else { theirTotal += size; } + }; + }; + myOps.forEach(parseOp(true)); + theirOps.forEach(parseOp(false)); + + var sorted = Object.keys(ops).map(Number); + sorted.sort().reverse(); + + // We start from the end so that we don't have to fix the offsets everytime + var prev; + var toKeepEnd = []; + sorted.forEach(function (offset) { + var op = ops[offset]; + + // Not the same author? fix! + if (prev && prev.me !== op.me) { + // Provide the new "totals" + prev.total = prev.me ? myTotal : theirTotal; + op.total = op.me ? myTotal : theirTotal; + // Fix the markers + fixMarks(op, prev, content, toKeepEnd); + } + + if (op.me) { myTotal -= op.size; } + else { theirTotal -= op.size; } + prev = op; + }); + + // We now have all the markers located after the first operation (ordered by offset). + // Prepend the markers placed before this operation + var first = ops[sorted[sorted.length - 1]]; + if (first) { Array.prototype.unshift.apply(toKeepEnd, first.marks); } + + // Commit our new markers + Env.authormarks.marks = toKeepEnd; + }; + + var setMarks = function (Env) { + // on remote update: remove all marks, add new marks + Env.editor.getAllMarks().forEach(function (marker) { + if (marker.attributes && marker.attributes['data-type'] === 'authormark') { + marker.clear(); + } + }); + var authormarks = Env.authormarks; + authormarks.marks.forEach(function (mark) { + var uid = mark[0]; + if (!authormarks.authors || !authormarks.authors[uid]) { return; } + var from = {}; + var to = {}; + from.line = mark[1]; + from.ch = mark[2]; + if (mark.length === 3) { + to.line = mark[1]; + to.ch = mark[2]+1; + } else if (mark.length === 4) { + to.line = mark[1]; + to.ch = mark[3]; + } else if (mark.length === 5) { + to.line = mark[3]; + to.ch = mark[4]; + } + + // Remove marks that are placed under this one + Env.editor.findMarks(from, to).forEach(function (mark) { + if (mark.attributes['data-type'] !== 'authormark') { return; } + mark.clear(); + }); + + addMark(Env, from, to, uid); + }); + }; + + var setMyData = function (Env) { + var userData = Env.common.getMetadataMgr().getUserData(); + var old = Env.authormarks.authors[Env.myAuthorId]; + if (!old || (old.name === userData.data && old.color === userData.color)) { return; } + Env.authormarks.authors[Env.myAuthorId] = { + name: userData.name, + curvePublic: userData.curvePublic, + color: userData.color + }; + return true; + }; + + var localChange = function (Env, change, cb) { + cb = cb || function () {}; + + if (change.origin === "setValue") { + // If the content is changed from a remote patch, we call localChange + // in "onContentUpdate" directly + return; + } + if (change.text === undefined || ['+input', 'paste'].indexOf(change.origin) === -1) { + return void cb(); + } + + // add new author mark if text is added. marks from removed text are removed automatically + + // change.to is not always correct, fix it! + var to_add = { + line: change.from.line + change.text.length-1, + }; + if (change.text.length > 1) { + // Multiple lines => take the length of the text added to the last line + to_add.ch = change.text[change.text.length-1].length; + } else { + // Single line => use the "from" position and add the length of the text + to_add.ch = change.from.ch + change.text[change.text.length-1].length; + } + + // If my text is inside an existing mark: + // * if it's my mark, do nothing + // * if it's someone else's mark, break it + // We can only have one author mark at a given position, but there may be + // another mark (cursor selection...) at this position so we use ".some" + var toSplit, abort; + + Env.editor.findMarks(change.from, to_add).some(function (mark) { + if (!mark.attributes) { return; } + if (mark.attributes['data-type'] !== 'authormark') { return; } + if (mark.attributes['data-uid'] !== Env.myAuthorId) { + toSplit = { + mark: mark, + uid: mark.attributes['data-uid'] + }; + } else { + // This is our mark: abort to avoid making a new one + abort = true; + } + + return true; + }); + if (abort) { return void cb(); } + + // Add my data to the doc if it's missing + if (!Env.authormarks.authors[Env.myAuthorId]) { + setMyData(Env); + } + + if (toSplit && toSplit.mark && typeof(toSplit.uid) !== "undefined") { + // Break the other user's mark if needed + var _pos = toSplit.mark.find(); + toSplit.mark.clear(); + addMark(Env, _pos.from, change.from, toSplit.uid); // their mark, 1st part + addMark(Env, change.from, to_add, Env.myAuthorId); // my mark + addMark(Env, to_add, _pos.to, toSplit.uid); // their mark, 2nd part + } else { + // Add my mark + addMark(Env, change.from, to_add, Env.myAuthorId); + } + + cb(); + }; + + var authorUid = function (existing) { + if (!Array.isArray(existing)) { existing = []; } + var n; + var i = 0; + while (!n || existing.indexOf(n) !== -1 && i++ < 1000) { + n = Math.floor(Math.random() * 1000000); + } + // If we can't find a valid number in 1000 iterations, use 0... + if (existing.indexOf(n) !== -1) { n = 0; } + return n; + }; + var getAuthorId = function (Env) { + var existing = Object.keys(Env.authormarks.authors || {}); + if (!Env.common.isLoggedIn()) { return authorUid(existing); } + + var userData = Env.common.getMetadataMgr().getUserData(); + var uid; + existing.some(function (id) { + var author = Env.authormarks.authors[id] || {}; + if (author.curvePublic !== userData.curvePublic) { return; } + uid = Number(id); + return true; + }); + return uid || authorUid(existing); + }; + + var ready = function (Env) { + Env.myAuthorId = getAuthorId(Env); + var changed = setMyData(Env); + if (changed) { + Env.framework.localChange(); + setMarks(Env); + } + }; + + Markers.create = function (config) { + var Env = config; + setAuthorMarks(Env); + Env.myAuthorId = 0; + + var metadataMgr = Env.common.getMetadataMgr(); + metadataMgr.onChange(function () { + // Update my data + var changed = setMyData(Env); + if (changed) { + Env.framework.localChange(); + setMarks(Env); + } + }); + + var call = function (f) { + return function () { + [].unshift.call(arguments, Env); + return f.apply(null, arguments); + }; + }; + + + return { + addMark: call(addMark), + setAuthorMarks: call(setAuthorMarks), + getAuthorMarks: call(getAuthorMarks), + updateAuthorMarks: call(updateAuthorMarks), + checkAuthors: call(checkAuthors), + setMarks: call(setMarks), + localChange: call(localChange), + ready: call(ready), + }; + }; + + return Markers; +}); diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index f87c92d36..b6f4c6aaf 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -65,7 +65,6 @@ define([ sframeChan.query('Q_RT_MESSAGE', message, function (_err, obj) { var err = _err || (obj && obj.error); if (!err) { evPatchSent.fire(); } - console.error('cb', message); cb(err); }, { timeout: -1 }); }); @@ -138,7 +137,6 @@ define([ if (isReady) { onLocal(true); // should be onBeforeMessage } - console.error('received', content); chainpad.message(content); if (isHistory && updateLoadingProgress) { updateLoadingProgress({ diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index bdbaa51b0..40e820d41 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -58,7 +58,6 @@ define([ editor.save(); var ops = ChainPad.Diff.diff(oldDoc, remoteDoc); - console.log(ops); var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { return TextCursor.transformCursor(oldCursor[attr], ops); }); From 87fc1f8dafd06c9329d5a5885aab09b2855276cf Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 15 Apr 2020 15:20:04 +0200 Subject: [PATCH 14/52] Show/hide and enable/disable cba --- www/code/inner.js | 25 +++------ www/code/markers.js | 95 ++++++++++++++++++++++++++++------ www/common/inner/properties.js | 45 ++++++++++++++++ 3 files changed, 131 insertions(+), 34 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index 11f0fbf7e..fbb170e53 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -310,24 +310,11 @@ define([ editor: editor }); - var $removeAuthorColorsButton = framework._.sfCommon.createButton('removeauthorcolors', true, {icon: 'fa-paint-brush', title: 'Autorenfarben entfernen'}); // XXX - framework._.toolbar.$rightside.append($removeAuthorColorsButton); - $removeAuthorColorsButton.click(function() { - var selfrom = editor.getCursor("from"); - var selto = editor.getCursor("to"); - if (!editor.somethingSelected() || selfrom === selto) { - editor.getAllMarks().forEach(function (marker) { - marker.clear(); - }); - authormarks.authors = {}; - authormarks.marks = []; - } else { - editor.findMarks(selfrom, selto).forEach(function (marker) { - marker.clear(); - }); - } - framework.localChange(); - }); + var $showAuthorColorsButton = framework._.sfCommon.createButton('', true, { + icon: 'fa-paint-brush', + }).hide(); + framework._.toolbar.$rightside.append($showAuthorColorsButton); + markers.setButton($showAuthorColorsButton); var $print = $('#cp-app-code-print'); var $content = $('#cp-app-code-preview-content'); @@ -358,7 +345,7 @@ define([ } // Fix the markers offsets - markers.checkAuthors(newContent); + markers.checkMarks(newContent); // Apply the text content CodeMirror.contentUpdate(newContent); diff --git a/www/code/markers.js b/www/code/markers.js index 2092df781..459a6a856 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -6,19 +6,21 @@ define([ ], function (Util, SFCodeMirror, Messages, ChainPad) { var Markers = {}; - Messages.cba_writtenBy = 'Written by {0}'; - var MARK_OPACITY = 90; + + Messages.cba_writtenBy = 'Written by {0}'; // XXX + var addMark = function (Env, from, to, uid) { + if (!Env.enabled) { return; } var author = Env.authormarks.authors[uid] || {}; uid = Number(uid); var name = Util.fixHTML(author.name || Messages.anonymous); return Env.editor.markText(from, to, { inclusiveLeft: uid === Env.myAuthorId, inclusiveRight: uid === Env.myAuthorId, - css: "background-color: " + author.color + MARK_OPACITY, + css: "background-color: " + author.color + Env.opacity, attributes: { - title: Messages._getKey('cba_writtenBy', [name]), + title: Env.opacity ? Messages._getKey('cba_writtenBy', [name]) : undefined, 'data-type': 'authormark', 'data-uid': uid } @@ -60,6 +62,8 @@ define([ }; var updateAuthorMarks = function (Env) { + if (!Env.enabled) { return; } + // get author marks var _marks = []; var all = []; @@ -195,7 +199,8 @@ define([ } }; - var checkAuthors = function (Env, userDoc) { + var checkMarks = function (Env, userDoc) { + var chainpad = Env.framework._.cpNfInner.chainpad; var editor = Env.editor; var CodeMirror = Env.CodeMirror; @@ -203,6 +208,8 @@ define([ setAuthorMarks(Env, userDoc.authormarks); + if (!Env.enabled) { return; } + var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); if (!authDoc.content || !userDoc.content) { return; } if (authDoc.content === userDoc.content) { return; } // No uncommitted work @@ -280,12 +287,14 @@ define([ }; var setMarks = function (Env) { - // on remote update: remove all marks, add new marks + // on remote update: remove all marks, add new marks if colors are enabled Env.editor.getAllMarks().forEach(function (marker) { if (marker.attributes && marker.attributes['data-type'] === 'authormark') { marker.clear(); } }); + + if (!Env.enabled) { return; } var authormarks = Env.authormarks; authormarks.marks.forEach(function (mark) { var uid = mark[0]; @@ -316,20 +325,24 @@ define([ }; var setMyData = function (Env) { + if (!Env.enabled) { return; } + var userData = Env.common.getMetadataMgr().getUserData(); var old = Env.authormarks.authors[Env.myAuthorId]; - if (!old || (old.name === userData.data && old.color === userData.color)) { return; } Env.authormarks.authors[Env.myAuthorId] = { name: userData.name, curvePublic: userData.curvePublic, color: userData.color }; + if (!old || (old.name === userData.name && old.color === userData.color)) { return; } return true; }; var localChange = function (Env, change, cb) { cb = cb || function () {}; + if (!Env.enabled) { return void cb(); } + if (change.origin === "setValue") { // If the content is changed from a remote patch, we call localChange // in "onContentUpdate" directly @@ -397,6 +410,31 @@ define([ cb(); }; + Messages.cba_show = "Show user colors"; // XXX + Messages.cba_hide = "Hide user colors"; // XXX + var setButton = function (Env, $button) { + var toggle = function () { + var tippy = $button[0] && $button[0]._tippy; + if (Env.opacity) { + Env.opacity = 0; + if (tippy) { tippy.title = Messages.cba_show; } + else { $button.attr('title', Messages.cba_show); } + $button.removeClass("cp-toolbar-button-active"); + } else { + Env.opacity = MARK_OPACITY; + if (tippy) { tippy.title = Messages.cba_hide; } + else { $button.attr('title', Messages.cba_hide); } + $button.addClass("cp-toolbar-button-active"); + } + }; + toggle(); + Env.$button = $button; + $button.click(function() { + toggle(); + setMarks(Env); + }); + }; + var authorUid = function (existing) { if (!Array.isArray(existing)) { existing = []; } var n; @@ -422,28 +460,55 @@ define([ }); return uid || authorUid(existing); }; - var ready = function (Env) { + var metadataMgr = Env.common.getMetadataMgr(); + var md = metadataMgr.getMetadata(); + Env.ready = true; Env.myAuthorId = getAuthorId(Env); - var changed = setMyData(Env); - if (changed) { - Env.framework.localChange(); + Env.enabled = md.enableColors; + + if (Env.enabled) { + if (Env.$button) { Env.$button.show(); } setMarks(Env); } }; Markers.create = function (config) { var Env = config; - setAuthorMarks(Env); + Env.authormarks = { + authors: {}, + marks: [] + }; + Env.enabled = false; Env.myAuthorId = 0; var metadataMgr = Env.common.getMetadataMgr(); metadataMgr.onChange(function () { + var md = metadataMgr.getMetadata(); + // If the state has changed in the pad, change the Env too + if (Env.enabled !== md.enableColors) { + Env.enabled = md.enableColors; + if (!Env.enabled) { + // Reset marks + Env.authormarks = { + authors: {}, + marks: [] + }; + setMarks(Env); + if (Env.$button) { Env.$button.hide(); } + } else { + Env.myAuthorId = getAuthorId(Env); + if (Env.$button) { Env.$button.show(); } + } + if (Env.ready) { Env.framework.localChange(); } + } + + if (!Env.enabled) { return; } // Update my data var changed = setMyData(Env); if (changed) { - Env.framework.localChange(); setMarks(Env); + Env.framework.localChange(); } }); @@ -457,13 +522,13 @@ define([ return { addMark: call(addMark), - setAuthorMarks: call(setAuthorMarks), getAuthorMarks: call(getAuthorMarks), updateAuthorMarks: call(updateAuthorMarks), - checkAuthors: call(checkAuthors), + checkMarks: call(checkMarks), setMarks: call(setMarks), localChange: call(localChange), ready: call(ready), + setButton: call(setButton) }; }; diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js index 58f10e738..07193a679 100644 --- a/www/common/inner/properties.js +++ b/www/common/inner/properties.js @@ -50,6 +50,51 @@ define([ // File and history size... var owned = Modal.isOwned(Env, data); + var metadataMgr = common.getMetadataMgr(); + var priv = metadataMgr.getPrivateData(); + Messages.cba_properties = "Author colors (experimental)"; // XXX + Messages.cba_enable = "Enable author colors in this pad"; // XXX + Messages.cba_disable = "Clear all colors and disable"; // XXX + if (owned && priv.app === 'code') { + (function () { + var md = metadataMgr.getMetadata(); + var div = h('div'); + var $div = $(div); + var setButton = function (state) { + var button = h('button.btn'); + var $button = $(button); + $div.html('').append($button); + if (state) { + // Add "enable" button + $button.addClass('btn-secondary').text(Messages.cba_enable); + UI.confirmButton(button, { + classes: 'btn-primary' + }, function () { + $button.remove(); + var md = Util.clone(metadataMgr.getMetadata()); + md.enableColors = true; + metadataMgr.updateMetadata(md); + setButton(false); + }); + return; + } + // Add "disable" button + $button.addClass('btn-danger-alt').text(Messages.cba_disable); + UI.confirmButton(button, { + classes: 'btn-danger' + }, function () { + $button.remove(); + var md = Util.clone(metadataMgr.getMetadata()); + md.enableColors = false; + metadataMgr.updateMetadata(md); + setButton(true); + }); + }; + setButton(!md.enableColors); + $d.append(h('div.cp-app-prop', [Messages.cba_properties, h('br'), div])); + })(); + } + // check the size of this file, including additional channels var bytes = 0; var historyBytes; From 94c9e47d325130fbe9bf99034e4035d8d03e07dc Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 15 Apr 2020 15:36:37 +0200 Subject: [PATCH 15/52] Remember the state of cba per user in their owned pads --- www/code/inner.js | 7 +++++++ www/common/inner/properties.js | 2 ++ 2 files changed, 9 insertions(+) diff --git a/www/code/inner.js b/www/code/inner.js index fbb170e53..73f426ec5 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -395,6 +395,13 @@ define([ //console.log("%s => %s", CodeMirror.highlightMode, CodeMirror.$language.val()); } + if (newPad && Util.find(privateData, ['settings', 'code', 'enableColors'])) { + var metadataMgr = common.getMetadataMgr(); + var md = Util.clone(metadataMgr.getMetadata()); + md.enableColors = true; + metadataMgr.updateMetadata(md); + } + markers.ready(); var fmConfig = { diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js index 07193a679..e787908f3 100644 --- a/www/common/inner/properties.js +++ b/www/common/inner/properties.js @@ -73,6 +73,7 @@ define([ $button.remove(); var md = Util.clone(metadataMgr.getMetadata()); md.enableColors = true; + common.setAttribute(['code', 'enableColors'], true); metadataMgr.updateMetadata(md); setButton(false); }); @@ -86,6 +87,7 @@ define([ $button.remove(); var md = Util.clone(metadataMgr.getMetadata()); md.enableColors = false; + common.setAttribute(['code', 'enableColors'], false); metadataMgr.updateMetadata(md); setButton(true); }); From fd2ee0fced1109fac6f98bd78f0bd077068686cb Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 15 Apr 2020 16:23:36 +0200 Subject: [PATCH 16/52] Add comment --- www/code/markers.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index 459a6a856..4ae555b31 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -37,6 +37,12 @@ define([ if (a[2] > b[2]) { return 1; } return 0; }; + + /* Formats: + [uid, startLine, startCh, endLine, endCh] (multi line) + [uid, startLine, startCh, endCh] (single line) + [uid, startLine, startCh] (single character) + */ var parseMark = Markers.parseMark = function (array) { if (!Array.isArray(array)) { return {}; } var multiline = typeof(array[4]) !== "undefined"; @@ -315,10 +321,14 @@ define([ } // Remove marks that are placed under this one - Env.editor.findMarks(from, to).forEach(function (mark) { - if (mark.attributes['data-type'] !== 'authormark') { return; } - mark.clear(); - }); + try { + Env.editor.findMarks(from, to).forEach(function (mark) { + if (mark.attributes['data-type'] !== 'authormark') { return; } + mark.clear(); + }); + } catch (e) { + console.error(e); + } addMark(Env, from, to, uid); }); From 7890342ae18b49f8a786f3fa2e5c7c97f0563722 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 15 Apr 2020 16:49:38 +0200 Subject: [PATCH 17/52] Add cba hint --- customize.dist/src/less2/include/modals-ui-elements.less | 5 +++++ www/common/inner/properties.js | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 0cc580ec0..c407c8523 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -26,6 +26,11 @@ // Properties modal .cp-app-prop { margin-bottom: 10px; + .cp-app-prop-hint { + color: @cryptpad_text_col; + font-size: 0.8em; + margin-bottom: 5px; + } .cp-app-prop-size-container { height: 20px; background-color: @colortheme_logo-2; diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js index e787908f3..5a17a2c76 100644 --- a/www/common/inner/properties.js +++ b/www/common/inner/properties.js @@ -53,12 +53,14 @@ define([ var metadataMgr = common.getMetadataMgr(); var priv = metadataMgr.getPrivateData(); Messages.cba_properties = "Author colors (experimental)"; // XXX + Messages.cba_hint = "This setting will be remembered for your next pad."; // XXX Messages.cba_enable = "Enable author colors in this pad"; // XXX Messages.cba_disable = "Clear all colors and disable"; // XXX if (owned && priv.app === 'code') { (function () { var md = metadataMgr.getMetadata(); var div = h('div'); + var hint = h('div.cp-app-prop-hint', Messages.cba_hint); var $div = $(div); var setButton = function (state) { var button = h('button.btn'); @@ -93,7 +95,7 @@ define([ }); }; setButton(!md.enableColors); - $d.append(h('div.cp-app-prop', [Messages.cba_properties, h('br'), div])); + $d.append(h('div.cp-app-prop', [Messages.cba_properties, hint, div])); })(); } From 9c5f0c0d6f36a78aa3366902f0af2076ac33e731 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 16 Apr 2020 10:36:51 +0200 Subject: [PATCH 18/52] Use rgba instead of #RRGGBBAA for the author markers --- www/code/markers.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index 4ae555b31..4fedd6dfc 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -6,7 +6,7 @@ define([ ], function (Util, SFCodeMirror, Messages, ChainPad) { var Markers = {}; - var MARK_OPACITY = 90; + var MARK_OPACITY = 0.5; Messages.cba_writtenBy = 'Written by {0}'; // XXX @@ -15,10 +15,12 @@ define([ var author = Env.authormarks.authors[uid] || {}; uid = Number(uid); var name = Util.fixHTML(author.name || Messages.anonymous); + var col = Util.hexToRGB(author.color); + var rgba = 'rgba('+col[0]+','+col[1]+','+col[2]+','+Env.opacity+');'; return Env.editor.markText(from, to, { inclusiveLeft: uid === Env.myAuthorId, inclusiveRight: uid === Env.myAuthorId, - css: "background-color: " + author.color + Env.opacity, + css: "background-color: " + rgba, attributes: { title: Env.opacity ? Messages._getKey('cba_writtenBy', [name]) : undefined, 'data-type': 'authormark', From 026bf6a425d4bc70582f6de49f49196b692cd518 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 16 Apr 2020 11:20:18 +0200 Subject: [PATCH 19/52] Fix obvious issues in cba --- www/code/markers.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index 4fedd6dfc..862f3962b 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -135,6 +135,7 @@ define([ // in the comments, "I" am "first" var fixMarks = function (first, last, content, toKeepEnd) { var toKeep = []; +console.log(first, last, JSON.stringify(toKeepEnd)); // Get their start position compared to the authDoc var lastAuthOffset = last.offset + last.total; @@ -175,6 +176,10 @@ define([ toJoin = parseMark(toJoinMark); } +console.log('to keep, to join'); +console.warn(JSON.stringify(toKeep)); +console.warn(JSON.stringify(toJoin)); + // Add the new markers to the result Array.prototype.unshift.apply(toKeepEnd, toKeep); @@ -199,12 +204,13 @@ define([ } }); - if (toKeep.length && toJoin) { + if (toKeep.length && toJoin && toJoin.endLine && toJoin.startLine) { // Make sure the marks are joined correctly: // fix the start position of the marks to keep toKeepEnd[0][1] = toJoin.endLine; toKeepEnd[0][2] = toJoin.endCh; } + console.warn(JSON.stringify(toKeepEnd)); }; var checkMarks = function (Env, userDoc) { @@ -212,10 +218,11 @@ define([ var chainpad = Env.framework._.cpNfInner.chainpad; var editor = Env.editor; var CodeMirror = Env.CodeMirror; - var oldMarks = Env.oldMarks; setAuthorMarks(Env, userDoc.authormarks); + var oldMarks = Env.oldMarks; + if (!Env.enabled) { return; } var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); @@ -229,7 +236,13 @@ define([ var commonParent = chainpad.getAuthBlock().getParent().getContent().doc; var content = JSON.parse(commonParent || '{}').content || ''; + console.error("BEGIN"); + console.log(JSON.stringify(oldMarks.marks)); + console.log(JSON.stringify(authDoc.authormarks.marks)); + + var theirOps = ChainPad.Diff.diff(content, authDoc.content); + console.warn(theirOps, chainpad.getAuthBlock().getPatch().operations); var myOps = ChainPad.Diff.diff(content, localDoc); if (!myOps.length || !theirOps.length) { return; } @@ -263,7 +276,8 @@ define([ theirOps.forEach(parseOp(false)); var sorted = Object.keys(ops).map(Number); - sorted.sort().reverse(); + sorted.sort(function (a, b) { return a-b; }).reverse(); +console.warn(ops, sorted); // We start from the end so that we don't have to fix the offsets everytime var prev; @@ -289,6 +303,10 @@ define([ // Prepend the markers placed before this operation var first = ops[sorted[sorted.length - 1]]; if (first) { Array.prototype.unshift.apply(toKeepEnd, first.marks); } +console.error(JSON.stringify(first.marks)); +console.error(JSON.stringify(toKeepEnd)); + +console.error("END"); // Commit our new markers Env.authormarks.marks = toKeepEnd; @@ -329,6 +347,8 @@ define([ mark.clear(); }); } catch (e) { + console.warn(mark, JSON.stringify(authormarks.marks)); + console.error(from, to); console.error(e); } @@ -515,7 +535,12 @@ define([ if (Env.ready) { Env.framework.localChange(); } } - if (!Env.enabled) { return; } + // If the markers are disabled or if I haven't pushed content since the last reset, + // don't update my data + if (!Env.enabled || !Env.myAuthorId || !Env.authormarks.authors[Env.myAuthorId]) { + return; + } + // Update my data var changed = setMyData(Env); if (changed) { From b74a4b6bb48295ed5b6499f175c5925f1518f44b Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 16 Apr 2020 16:03:17 +0200 Subject: [PATCH 20/52] Fix more cba issues and add debugging data --- www/code/markers.js | 22 +++++++++++++++++++-- www/common/sframe-chainpad-netflux-inner.js | 3 +++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index 862f3962b..c8f89069c 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -182,6 +182,7 @@ console.warn(JSON.stringify(toJoin)); // Add the new markers to the result Array.prototype.unshift.apply(toKeepEnd, toKeep); + console.warn(JSON.stringify(toKeepEnd)); // Fix their offset: compute added lines and added characters on the last line // using the chainpad operation data (toInsert and toRemove) @@ -190,17 +191,20 @@ console.warn(JSON.stringify(toJoin)); var added = first.toInsert.split('\n'); var addLine = added.length - removed.length; var addCh = added[added.length - 1].length - removed[removed.length - 1].length; + console.log(removed, added, addLine, addCh); if (addLine > 0) { addCh -= pos.ch; } - toKeepEnd.forEach(function (array) { + toKeepEnd.forEach(function (array, i) { // Push to correct lines array[1] += addLine; if (typeof(array[4]) !== "undefined") { array[3] += addLine; } // If they have markers on my end line, push their "ch" - if (array[1] === toJoin[1]) { + // If i===0, this marker will be joined later and it will also start on my end line + if (array[1] === toJoin.endLine || i === 0) { array[2] += addCh; // If they have no end line, it means end line === start line, // so we also push their end offset if (!array[4] && array[3]) { array[3] += addCh; } + else if (array[4] && array[3] === toJoin.endLine) { array[4] += addCh; } } }); @@ -241,9 +245,18 @@ console.warn(JSON.stringify(toJoin)); console.log(JSON.stringify(authDoc.authormarks.marks)); +var authpatch = chainpad.getAuthBlock(); +var test = chainpad._.messages[authpatch.hashOf]; // XXX use new chainpad api +if (test.mut.isFromMe) { + console.error('stopped'); + return; +} + + console.log(content); var theirOps = ChainPad.Diff.diff(content, authDoc.content); console.warn(theirOps, chainpad.getAuthBlock().getPatch().operations); var myOps = ChainPad.Diff.diff(content, localDoc); + console.warn(myOps); if (!myOps.length || !theirOps.length) { return; } @@ -375,6 +388,8 @@ console.error("END"); if (!Env.enabled) { return void cb(); } +console.warn(change); + if (change.origin === "setValue") { // If the content is changed from a remote patch, we call localChange // in "onContentUpdate" directly @@ -405,6 +420,7 @@ console.error("END"); // another mark (cursor selection...) at this position so we use ".some" var toSplit, abort; + Env.editor.findMarks(change.from, to_add).some(function (mark) { if (!mark.attributes) { return; } if (mark.attributes['data-type'] !== 'authormark') { return; } @@ -420,6 +436,8 @@ console.error("END"); return true; }); +console.warn(Env.editor.findMarks(change.from, to_add)); +console.log(change.from, to_add, change.text, abort, toSplit); if (abort) { return void cb(); } // Add my data to the doc if it's missing diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index b6f4c6aaf..0937e1289 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -65,10 +65,12 @@ define([ sframeChan.query('Q_RT_MESSAGE', message, function (_err, obj) { var err = _err || (obj && obj.error); if (!err) { evPatchSent.fire(); } + console.error('cb', JSON.stringify(message)); cb(err); }, { timeout: -1 }); }); _chainpad.onPatch(function () { + console.log('patch'); onRemote({ realtime: chainpad }); }); return _chainpad; @@ -137,6 +139,7 @@ define([ if (isReady) { onLocal(true); // should be onBeforeMessage } + console.error('received', JSON.stringify(content)); chainpad.message(content); if (isHistory && updateLoadingProgress) { updateLoadingProgress({ From 38acd01b35401df7d5b25f562c5e387baf504789 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 16 Apr 2020 16:24:09 +0200 Subject: [PATCH 21/52] Fix issues with falsy values --- www/code/markers.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index c8f89069c..46c8a4a7f 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -203,8 +203,11 @@ console.warn(JSON.stringify(toJoin)); array[2] += addCh; // If they have no end line, it means end line === start line, // so we also push their end offset - if (!array[4] && array[3]) { array[3] += addCh; } - else if (array[4] && array[3] === toJoin.endLine) { array[4] += addCh; } + if (typeof(array[4]) === "undefined" && typeof(array[3]) !== "undefined") { + array[3] += addCh; + } else if (typeof(array[4]) !== "undefined" && array[3] === toJoin.endLine) { + array[4] += addCh; + } } }); From 1d3f0ded810456b779c37a66ce4024fa64599ac7 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 17 Apr 2020 10:31:37 +0200 Subject: [PATCH 22/52] Fix more markers errors --- www/code/markers.js | 91 +++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index 46c8a4a7f..c8d384312 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -82,6 +82,7 @@ define([ var attributes = mark.attributes || {}; if (!pos || attributes['data-type'] !== 'authormark') { return; } + var uid = Number(attributes['data-uid']) || 0; all.forEach(function (obj) { @@ -126,6 +127,7 @@ define([ i++; }); _marks.sort(sortMarks); + console.error(JSON.stringify(_marks.filter(Boolean))); Env.authormarks.marks = _marks.filter(Boolean); }; @@ -135,39 +137,41 @@ define([ // in the comments, "I" am "first" var fixMarks = function (first, last, content, toKeepEnd) { var toKeep = []; + var toJoin = {}; console.log(first, last, JSON.stringify(toKeepEnd)); - // Get their start position compared to the authDoc - var lastAuthOffset = last.offset + last.total; - var lastAuthPos = SFCodeMirror.posToCursor(lastAuthOffset, last.doc); - // Get their start position compared to the localDoc - var lastLocalOffset = last.offset + first.total; - var lastLocalPos = SFCodeMirror.posToCursor(lastLocalOffset, first.doc); - - // Keep their changes in the marks (after their offset) - last.marks.some(function (array, i) { - var p = parseMark(array); - // End of the mark before offset? ignore - if (p.endLine < lastAuthPos.line) { return; } - // Take everything from the first mark ending after the pos - if (p.endLine > lastAuthPos.line || p.endCh >= lastAuthPos.ch) { - toKeep = last.marks.slice(i); - last.marks.splice(i); - return true; - } - }); - // Keep my marks (based on currentDoc) before their changes - var toJoin = {}; - first.marks.some(function (array, i) { - var p = parseMark(array); - // End of the mark before offset? ignore - if (p.endLine < lastLocalPos.line) { return; } - // Take everything from the first mark ending after the pos - if (p.endLine > lastLocalPos.line || p.endCh >= lastLocalPos.ch) { - first.marks.splice(i); - return true; - } - }); + if (first.me !== last.me) { + // Get their start position compared to the authDoc + var lastAuthOffset = last.offset + last.total; + var lastAuthPos = SFCodeMirror.posToCursor(lastAuthOffset, last.doc); + // Get their start position compared to the localDoc + var lastLocalOffset = last.offset + first.total; + var lastLocalPos = SFCodeMirror.posToCursor(lastLocalOffset, first.doc); + + // Keep their changes in the marks (after their offset) + last.marks.some(function (array, i) { + var p = parseMark(array); + // End of the mark before offset? ignore + if (p.endLine < lastAuthPos.line) { return; } + // Take everything from the first mark ending after the pos + if (p.endLine > lastAuthPos.line || p.endCh >= lastAuthPos.ch) { + toKeep = last.marks.slice(i); + last.marks.splice(i); + return true; + } + }); + // Keep my marks (based on currentDoc) before their changes + first.marks.some(function (array, i) { + var p = parseMark(array); + // End of the mark before offset? ignore + if (p.endLine < lastLocalPos.line) { return; } + // Take everything from the first mark ending after the pos + if (p.endLine > lastLocalPos.line || p.endCh >= lastLocalPos.ch) { + first.marks.splice(i); + return true; + } + }); + } // If we still have markers in "first", store the last one so that we can "join" // everything at the end @@ -193,6 +197,7 @@ console.warn(JSON.stringify(toJoin)); var addCh = added[added.length - 1].length - removed[removed.length - 1].length; console.log(removed, added, addLine, addCh); if (addLine > 0) { addCh -= pos.ch; } + if (addLine < 0) { addCh += pos.ch; } toKeepEnd.forEach(function (array, i) { // Push to correct lines array[1] += addLine; @@ -211,7 +216,8 @@ console.warn(JSON.stringify(toJoin)); } }); - if (toKeep.length && toJoin && toJoin.endLine && toJoin.startLine) { + if (toKeep.length && toJoin && typeof(toJoin.endLine) !== "undefined" + && typeof(toJoin.endCh) !== "undefined") { // Make sure the marks are joined correctly: // fix the start position of the marks to keep toKeepEnd[0][1] = toJoin.endLine; @@ -226,6 +232,18 @@ console.warn(JSON.stringify(toJoin)); var editor = Env.editor; var CodeMirror = Env.CodeMirror; + var authPatch = chainpad.getAuthBlock(); +var test = chainpad._.messages[authpatch.hashOf]; // XXX use new chainpad api + if (authPatch.isFromMe) { + console.error('stopped'); + console.error(JSON.stringify(Env.authormarks.marks)); + return; + } +if (test.mut.isFromMe) { // XXX + console.error('ERROR'); + window.alert('error authPatch'); +} + setAuthorMarks(Env, userDoc.authormarks); var oldMarks = Env.oldMarks; @@ -248,13 +266,6 @@ console.warn(JSON.stringify(toJoin)); console.log(JSON.stringify(authDoc.authormarks.marks)); -var authpatch = chainpad.getAuthBlock(); -var test = chainpad._.messages[authpatch.hashOf]; // XXX use new chainpad api -if (test.mut.isFromMe) { - console.error('stopped'); - return; -} - console.log(content); var theirOps = ChainPad.Diff.diff(content, authDoc.content); console.warn(theirOps, chainpad.getAuthBlock().getPatch().operations); @@ -302,7 +313,7 @@ console.warn(ops, sorted); var op = ops[offset]; // Not the same author? fix! - if (prev && prev.me !== op.me) { + if (prev) { // Provide the new "totals" prev.total = prev.me ? myTotal : theirTotal; op.total = op.me ? myTotal : theirTotal; From f03713e60a40210169d9d6c202091fe3b4aca997 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 17 Apr 2020 15:16:49 +0200 Subject: [PATCH 23/52] Fix small issues, clean debugging logs and add known issues --- www/code/inner.js | 1 + www/code/markers.js | 207 +++++++++++++++----- www/common/sframe-chainpad-netflux-inner.js | 3 - 3 files changed, 160 insertions(+), 51 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index 73f426ec5..408e78c45 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -307,6 +307,7 @@ define([ common: common, framework: framework, CodeMirror: CodeMirror, + devMode: privateData.devMode, editor: editor }); diff --git a/www/code/markers.js b/www/code/markers.js index c8d384312..9ae302b97 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -6,6 +6,27 @@ define([ ], function (Util, SFCodeMirror, Messages, ChainPad) { var Markers = {}; + /* TODO Known Issues + * 1. ChainPad diff is not completely accurate: we're not aware of the other user's cursor + position so if they insert an "a" in the middle of "aaaaa", the diff will think that + the "a" was inserted at the end of this sequence. This is not an issue for the content + but it will cause issues for the colors + 2. ChainPad doesn't always provide the good result in case of conflict (?) + e.g. Alice is inserting "pew" at offset 10, Bob is removing 1 character at offset 10 + The expected result is to have "pew" and the following character deleted + In some cases, the result is "ew" inserted and the following character not deleted + */ + + var debug = function (Env, level, obj, logObject) { + if (!Env.devMode) { return function () {}; } + var f = console.log; + if (typeof(console[level]) === "function") { + f = console[level]; + }; + if (logObject) { return void f(obj); } + f(JSON.stringify(obj)); + }; + var MARK_OPACITY = 0.5; Messages.cba_writtenBy = 'Written by {0}'; // XXX @@ -50,6 +71,7 @@ define([ var multiline = typeof(array[4]) !== "undefined"; var singleChar = typeof(array[3]) === "undefined"; return { + uid: array[0], startLine: array[1], startCh: array[2], endLine: multiline ? array[3] : array[1], @@ -127,10 +149,64 @@ define([ i++; }); _marks.sort(sortMarks); - console.error(JSON.stringify(_marks.filter(Boolean))); + debug(Env, 'warn', _marks); Env.authormarks.marks = _marks.filter(Boolean); }; + // Fix all marks located after the given operation in the provided document + var fixMarksFromOp = function (Env, op, marks, doc) { + var pos = SFCodeMirror.posToCursor(op.offset, doc); // pos of start offset + var rPos = SFCodeMirror.posToCursor(op.offset + op.toRemove, doc); // end of removed content + var removed = doc.slice(op.offset, op.offset + op.toRemove).split('\n'); // removed content + var added = op.toInsert.split('\n'); // added content + var posEndLine = pos.line + added.length - 1; // end line after op + var posEndCh = added[added.length - 1].length; // end ch after op + var addLine = added.length - removed.length; + var addCh = added[added.length - 1].length - removed[removed.length - 1].length; + if (addLine > 0) { addCh -= pos.ch; } + else if (addLine < 0) { addCh += pos.ch; } + else { posEndCh += pos.ch; } + marks.forEach(function (mark, i) { + var p = parseMark(mark); + // Don't update marks located before the operation + if (p.endLine < pos.line || (p.endLine === pos.line && p.endCh < pos.ch)) { return; } + // Remove markers that have been deleted by my changes + if ((p.startLine > pos.line || (p.startLine === pos.line && p.startCh >= pos.ch)) && + (p.endLine < rPos.line || (p.endLine === rPos.line && p.endCh <= rPos.ch))) { + mark[i] = undefined; + return; + } + // Update markers that have been cropped right + if (p.endLine < rPos.line || (p.endLine === rPos.line && p.endCh <= rPos.ch)) { + mark[3] = pos.line; + mark[4] = pos.ch; + return; + } + // Update markers that have been cropped left. This markers will be affected by + // my toInsert so don't abort + if (p.startLine < rPos.line || (p.startLine === pos.line && p.startCh < pos.ch)) { + mark[1] = rPos.line; + mark[2] = rPos.ch; + } + // Apply my toInsert the to remaining marks + mark[1] += addLine; + if (typeof(mark[4]) !== "undefined") { mark[3] += addLine; } + + if (mark[1] === posEndLine) { + mark[2] += addCh; + if (typeof(mark[4]) === "undefined" && typeof(mark[3]) !== "undefined") { + mark[3] += addCh; + } else if (typeof(mark[4]) !== "undefined" && mark[3] === posEndLine) { + mark[4] += addCh; + } + } + }); + if (op.toInsert.length) { + marks.push([Env.myAuthorId, pos.line, pos.ch, posEndLine, posEndCh]); + } + marks.sort(sortMarks); + }; + // Remove marks added by OT and fix the incorrect ones // first: data about the change with the lowest offset // last: data about the change with the latest offset @@ -138,7 +214,10 @@ define([ var fixMarks = function (first, last, content, toKeepEnd) { var toKeep = []; var toJoin = {}; -console.log(first, last, JSON.stringify(toKeepEnd)); + + debug(Env, 'error', "Fix marks"); + debug(Env, 'warn', first); + debug(Env, 'warn', last); if (first.me !== last.me) { // Get their start position compared to the authDoc @@ -180,22 +259,22 @@ console.log(first, last, JSON.stringify(toKeepEnd)); toJoin = parseMark(toJoinMark); } -console.log('to keep, to join'); -console.warn(JSON.stringify(toKeep)); -console.warn(JSON.stringify(toJoin)); // Add the new markers to the result Array.prototype.unshift.apply(toKeepEnd, toKeep); - console.warn(JSON.stringify(toKeepEnd)); + + debug(Env, 'warn', toJoin); + debug(Env, 'warn', toKeep); + debug(Env, 'warn', toKeepEnd); // Fix their offset: compute added lines and added characters on the last line // using the chainpad operation data (toInsert and toRemove) var pos = SFCodeMirror.posToCursor(first.offset, content); var removed = content.slice(first.offset, first.offset + first.toRemove).split('\n'); var added = first.toInsert.split('\n'); + var posEndLine = pos.line + added.length - 1; // end line after op var addLine = added.length - removed.length; var addCh = added[added.length - 1].length - removed[removed.length - 1].length; - console.log(removed, added, addLine, addCh); if (addLine > 0) { addCh -= pos.ch; } if (addLine < 0) { addCh += pos.ch; } toKeepEnd.forEach(function (array, i) { @@ -203,14 +282,13 @@ console.warn(JSON.stringify(toJoin)); array[1] += addLine; if (typeof(array[4]) !== "undefined") { array[3] += addLine; } // If they have markers on my end line, push their "ch" - // If i===0, this marker will be joined later and it will also start on my end line - if (array[1] === toJoin.endLine || i === 0) { + if (array[1] === posEndLine) { array[2] += addCh; // If they have no end line, it means end line === start line, // so we also push their end offset if (typeof(array[4]) === "undefined" && typeof(array[3]) !== "undefined") { array[3] += addCh; - } else if (typeof(array[4]) !== "undefined" && array[3] === toJoin.endLine) { + } else if (typeof(array[4]) !== "undefined" && array[3] === posEndLine) { array[4] += addCh; } } @@ -220,10 +298,17 @@ console.warn(JSON.stringify(toJoin)); && typeof(toJoin.endCh) !== "undefined") { // Make sure the marks are joined correctly: // fix the start position of the marks to keep + // Note: we must preserve the same end for this mark if it was single line! + if (typeof(toKeepEnd[0][4]) === "undefined") { // Single line + toKeepEnd[0][4] = toKeepEnd[0][3] || (toKeepEnd[0][2]+1); // preserve end ch + toKeepEnd[0][3] = toKeepEnd[0][1]; // preserve end line + } toKeepEnd[0][1] = toJoin.endLine; toKeepEnd[0][2] = toJoin.endCh; } - console.warn(JSON.stringify(toKeepEnd)); + + debug(Env, 'log', 'Fixed'); + debug(Env, 'warn', toKeepEnd); }; var checkMarks = function (Env, userDoc) { @@ -232,51 +317,62 @@ console.warn(JSON.stringify(toJoin)); var editor = Env.editor; var CodeMirror = Env.CodeMirror; + setAuthorMarks(Env, userDoc.authormarks); + + if (!Env.enabled) { return; } + + debug(Env, 'error', 'Check marks'); + + var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); + if (!authDoc.content || !userDoc.content) { return; } + var authPatch = chainpad.getAuthBlock(); -var test = chainpad._.messages[authpatch.hashOf]; // XXX use new chainpad api if (authPatch.isFromMe) { - console.error('stopped'); - console.error(JSON.stringify(Env.authormarks.marks)); + debug(Env, 'log', 'Switch branch, from me'); + debug(Env, 'log', authDoc.content); + debug(Env, 'log', authDoc.authormarks.marks); + debug(Env, 'log', userDoc.content); + // We're switching to a different branch that was created by us. + // We can't trust localDoc anymore because it contains data from the other branch + // It means the only changes that we need to consider are ours. + // Diff between userDoc and authDoc to see what we changed + var myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content).reverse(); + var authormarks = Util.clone(authDoc.authormarks); + myOps.forEach(function (op) { + fixMarksFromOp(Env, op, authormarks.marks, authDoc.content); + }); + debug(Env, 'log', 'Fixed marks'); + debug(Env, 'warn', authormarks.marks); + setAuthorMarks(Env, authormarks); return; } -if (test.mut.isFromMe) { // XXX - console.error('ERROR'); - window.alert('error authPatch'); -} - setAuthorMarks(Env, userDoc.authormarks); var oldMarks = Env.oldMarks; - if (!Env.enabled) { return; } - var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); - if (!authDoc.content || !userDoc.content) { return; } if (authDoc.content === userDoc.content) { return; } // No uncommitted work if (!userDoc.authormarks || !Array.isArray(userDoc.authormarks.marks)) { return; } + debug(Env, 'warn', 'Begin...'); + var localDoc = CodeMirror.canonicalize(editor.getValue()); var commonParent = chainpad.getAuthBlock().getParent().getContent().doc; var content = JSON.parse(commonParent || '{}').content || ''; - console.error("BEGIN"); - console.log(JSON.stringify(oldMarks.marks)); - console.log(JSON.stringify(authDoc.authormarks.marks)); - - - console.log(content); var theirOps = ChainPad.Diff.diff(content, authDoc.content); - console.warn(theirOps, chainpad.getAuthBlock().getPatch().operations); var myOps = ChainPad.Diff.diff(content, localDoc); - console.warn(myOps); + + debug(Env, 'log', theirOps); + debug(Env, 'log', myOps); if (!myOps.length || !theirOps.length) { return; } // If I have uncommited content when receiving a remote patch, all the operations // placed after someone else's changes will create marker issues. We have to fix it - var ops = {}; + var sorted = []; var myTotal = 0; var theirTotal = 0; @@ -284,7 +380,7 @@ if (test.mut.isFromMe) { // XXX return function (op) { var size = (op.toInsert.length - op.toRemove); - ops[op.offset] = { + sorted.push({ me: me, offset: op.offset, toInsert: op.toInsert, @@ -293,7 +389,7 @@ if (test.mut.isFromMe) { // XXX marks: (me ? (oldMarks && oldMarks.marks) : (authDoc.authormarks && authDoc.authormarks.marks)) || [], doc: me ? localDoc : authDoc.content - }; + }); if (me) { myTotal += size; } else { theirTotal += size; } @@ -302,15 +398,21 @@ if (test.mut.isFromMe) { // XXX myOps.forEach(parseOp(true)); theirOps.forEach(parseOp(false)); - var sorted = Object.keys(ops).map(Number); - sorted.sort(function (a, b) { return a-b; }).reverse(); -console.warn(ops, sorted); + // Sort the operation in reverse order of offset + // If an operation from them has the same offset than an operation from me, put mine first + sorted.sort(function (a, b) { + if (a.offset === b.offset) { + return a.me ? -1 : 1; + } + return b.offset - a.offset; + }); + + debug(Env, 'log', sorted); // We start from the end so that we don't have to fix the offsets everytime var prev; var toKeepEnd = []; - sorted.forEach(function (offset) { - var op = ops[offset]; + sorted.forEach(function (op) { // Not the same author? fix! if (prev) { @@ -326,19 +428,21 @@ console.warn(ops, sorted); prev = op; }); + debug(Env, 'log', toKeepEnd); + // We now have all the markers located after the first operation (ordered by offset). // Prepend the markers placed before this operation - var first = ops[sorted[sorted.length - 1]]; + var first = sorted[sorted.length - 1]; if (first) { Array.prototype.unshift.apply(toKeepEnd, first.marks); } -console.error(JSON.stringify(first.marks)); -console.error(JSON.stringify(toKeepEnd)); - -console.error("END"); // Commit our new markers Env.authormarks.marks = toKeepEnd; + + debug(Env, 'warn', toKeepEnd); + debug(Env, 'warn', '...End'); }; + // Reset marks displayed in CodeMirror to the marks stored in Env var setMarks = function (Env) { // on remote update: remove all marks, add new marks if colors are enabled Env.editor.getAllMarks().forEach(function (marker) { @@ -348,6 +452,10 @@ console.error("END"); }); if (!Env.enabled) { return; } + + debug(Env, 'error', 'setMarks'); + debug(Env, 'log', Env.authormarks.marks); + var authormarks = Env.authormarks; authormarks.marks.forEach(function (mark) { var uid = mark[0]; @@ -402,7 +510,8 @@ console.error("END"); if (!Env.enabled) { return void cb(); } -console.warn(change); + debug(Env, 'error', 'Local change'); + debug(Env, 'log', change, true); if (change.origin === "setValue") { // If the content is changed from a remote patch, we call localChange @@ -450,8 +559,6 @@ console.warn(change); return true; }); -console.warn(Env.editor.findMarks(change.from, to_add)); -console.log(change.from, to_add, change.text, abort, toSplit); if (abort) { return void cb(); } // Add my data to the doc if it's missing @@ -583,8 +690,12 @@ console.log(change.from, to_add, change.text, abort, toSplit); var call = function (f) { return function () { - [].unshift.call(arguments, Env); - return f.apply(null, arguments); + try { + [].unshift.call(arguments, Env); + return f.apply(null, arguments); + } catch (e) { + console.error(e); + } }; }; diff --git a/www/common/sframe-chainpad-netflux-inner.js b/www/common/sframe-chainpad-netflux-inner.js index 0937e1289..b6f4c6aaf 100644 --- a/www/common/sframe-chainpad-netflux-inner.js +++ b/www/common/sframe-chainpad-netflux-inner.js @@ -65,12 +65,10 @@ define([ sframeChan.query('Q_RT_MESSAGE', message, function (_err, obj) { var err = _err || (obj && obj.error); if (!err) { evPatchSent.fire(); } - console.error('cb', JSON.stringify(message)); cb(err); }, { timeout: -1 }); }); _chainpad.onPatch(function () { - console.log('patch'); onRemote({ realtime: chainpad }); }); return _chainpad; @@ -139,7 +137,6 @@ define([ if (isReady) { onLocal(true); // should be onBeforeMessage } - console.error('received', JSON.stringify(content)); chainpad.message(content); if (isHistory && updateLoadingProgress) { updateLoadingProgress({ From 283f739be5980dd712c92bde27f02d848cc7a1bc Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 17 Apr 2020 15:30:27 +0200 Subject: [PATCH 24/52] lint compliance --- www/code/markers.js | 12 ++++++------ www/debug/inner.js | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index 9ae302b97..9ce7de56a 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -22,7 +22,7 @@ define([ var f = console.log; if (typeof(console[level]) === "function") { f = console[level]; - }; + } if (logObject) { return void f(obj); } f(JSON.stringify(obj)); }; @@ -211,7 +211,7 @@ define([ // first: data about the change with the lowest offset // last: data about the change with the latest offset // in the comments, "I" am "first" - var fixMarks = function (first, last, content, toKeepEnd) { + var fixMarks = function (Env, first, last, content, toKeepEnd) { var toKeep = []; var toJoin = {}; @@ -277,7 +277,7 @@ define([ var addCh = added[added.length - 1].length - removed[removed.length - 1].length; if (addLine > 0) { addCh -= pos.ch; } if (addLine < 0) { addCh += pos.ch; } - toKeepEnd.forEach(function (array, i) { + toKeepEnd.forEach(function (array) { // Push to correct lines array[1] += addLine; if (typeof(array[4]) !== "undefined") { array[3] += addLine; } @@ -336,9 +336,9 @@ define([ // We can't trust localDoc anymore because it contains data from the other branch // It means the only changes that we need to consider are ours. // Diff between userDoc and authDoc to see what we changed - var myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content).reverse(); + var _myOps = ChainPad.Diff.diff(authDoc.content, userDoc.content).reverse(); var authormarks = Util.clone(authDoc.authormarks); - myOps.forEach(function (op) { + _myOps.forEach(function (op) { fixMarksFromOp(Env, op, authormarks.marks, authDoc.content); }); debug(Env, 'log', 'Fixed marks'); @@ -420,7 +420,7 @@ define([ prev.total = prev.me ? myTotal : theirTotal; op.total = op.me ? myTotal : theirTotal; // Fix the markers - fixMarks(op, prev, content, toKeepEnd); + fixMarks(Env, op, prev, content, toKeepEnd); } if (op.me) { myTotal -= op.size; } diff --git a/www/debug/inner.js b/www/debug/inner.js index 83c7c29f0..f7857dda5 100644 --- a/www/debug/inner.js +++ b/www/debug/inner.js @@ -416,6 +416,7 @@ define([ sframeChan.query('Q_GET_FULL_HISTORY', { debug: true, }, function (err, data) { + var start = 0; var replay, input, left, right; var content = h('div.cp-app-debug-progress.cp-loading-progress', [ h('p', [ @@ -438,9 +439,7 @@ define([ var chainpad = makeChainpad(); console.warn(chainpad); - var start = 0; var i = 0; - var messages = data.slice(); var play = function (_i) { if (_i < (start+1)) { _i = start + 1; } if (_i > data.length - 1) { _i = data.length - 1; } From d27dc768f31e97bcf1c88f56127a2b54a857d542 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 17 Apr 2020 15:56:17 +0200 Subject: [PATCH 25/52] Fix missing button to enable cba --- www/common/sframe-common-outer.js | 1 + www/secureiframe/main.js | 1 + 2 files changed, 2 insertions(+) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 0971300b6..0e87439b5 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1007,6 +1007,7 @@ define([ SecureModal.$iframe.hide(); }; config.data = { + app: parsed.type, hashes: hashes, password: password, isTemplate: isTemplate diff --git a/www/secureiframe/main.js b/www/secureiframe/main.js index ced68bbc6..330356135 100644 --- a/www/secureiframe/main.js +++ b/www/secureiframe/main.js @@ -88,6 +88,7 @@ define([ }).nThen(function (/*waitFor*/) { metaObj.doc = {}; var additionalPriv = { + app: config.data.app, fileHost: ApiConfig.fileHost, loggedIn: Utils.LocalStore.isLoggedIn(), origin: window.location.origin, From 9ba2d11cd5148caea99747b977827b9400a7058d Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 17 Apr 2020 16:50:41 +0200 Subject: [PATCH 26/52] Guard against a type error --- www/code/markers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/code/markers.js b/www/code/markers.js index 9ce7de56a..5fd6fe068 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -478,7 +478,7 @@ define([ // Remove marks that are placed under this one try { Env.editor.findMarks(from, to).forEach(function (mark) { - if (mark.attributes['data-type'] !== 'authormark') { return; } + if (!mark || !mark.attributes || mark.attributes['data-type'] !== 'authormark') { return; } mark.clear(); }); } catch (e) { From 1e9b9913c556b69c1c21e9f01d06062f408712fe Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 17 Apr 2020 18:18:16 +0200 Subject: [PATCH 27/52] Better debug function and fix marker error --- www/code/markers.js | 92 ++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index 5fd6fe068..74541cf4a 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -17,15 +17,7 @@ define([ In some cases, the result is "ew" inserted and the following character not deleted */ - var debug = function (Env, level, obj, logObject) { - if (!Env.devMode) { return function () {}; } - var f = console.log; - if (typeof(console[level]) === "function") { - f = console[level]; - } - if (logObject) { return void f(obj); } - f(JSON.stringify(obj)); - }; + var debug = function () {}; var MARK_OPACITY = 0.5; @@ -149,7 +141,7 @@ define([ i++; }); _marks.sort(sortMarks); - debug(Env, 'warn', _marks); + debug('warn', _marks); Env.authormarks.marks = _marks.filter(Boolean); }; @@ -166,14 +158,18 @@ define([ if (addLine > 0) { addCh -= pos.ch; } else if (addLine < 0) { addCh += pos.ch; } else { posEndCh += pos.ch; } + + var splitted; + marks.forEach(function (mark, i) { + if (!mark) { return; } var p = parseMark(mark); // Don't update marks located before the operation if (p.endLine < pos.line || (p.endLine === pos.line && p.endCh < pos.ch)) { return; } // Remove markers that have been deleted by my changes if ((p.startLine > pos.line || (p.startLine === pos.line && p.startCh >= pos.ch)) && (p.endLine < rPos.line || (p.endLine === rPos.line && p.endCh <= rPos.ch))) { - mark[i] = undefined; + marks[i] = undefined; return; } // Update markers that have been cropped right @@ -184,7 +180,12 @@ define([ } // Update markers that have been cropped left. This markers will be affected by // my toInsert so don't abort - if (p.startLine < rPos.line || (p.startLine === pos.line && p.startCh < pos.ch)) { + if (p.startLine < rPos.line || (p.startLine === rPos.line && p.startCh < rPos.ch)) { + // If our change will split an existing mark, put the existing mark after the change + // and create a new mark before + if (p.startLine < pos.line || (p.startLine === pos.line && p.startCh < pos.ch)) { + splitted = [mark[0], mark[1], mark[2], pos.line, pos.ch]; + } mark[1] = rPos.line; mark[2] = rPos.ch; } @@ -204,6 +205,9 @@ define([ if (op.toInsert.length) { marks.push([Env.myAuthorId, pos.line, pos.ch, posEndLine, posEndCh]); } + if (splitted) { + marks.push(splitted); + } marks.sort(sortMarks); }; @@ -215,9 +219,9 @@ define([ var toKeep = []; var toJoin = {}; - debug(Env, 'error', "Fix marks"); - debug(Env, 'warn', first); - debug(Env, 'warn', last); + debug('error', "Fix marks"); + debug('warn', first); + debug('warn', last); if (first.me !== last.me) { // Get their start position compared to the authDoc @@ -263,9 +267,9 @@ define([ // Add the new markers to the result Array.prototype.unshift.apply(toKeepEnd, toKeep); - debug(Env, 'warn', toJoin); - debug(Env, 'warn', toKeep); - debug(Env, 'warn', toKeepEnd); + debug('warn', toJoin); + debug('warn', toKeep); + debug('warn', toKeepEnd); // Fix their offset: compute added lines and added characters on the last line // using the chainpad operation data (toInsert and toRemove) @@ -307,8 +311,8 @@ define([ toKeepEnd[0][2] = toJoin.endCh; } - debug(Env, 'log', 'Fixed'); - debug(Env, 'warn', toKeepEnd); + debug('log', 'Fixed'); + debug('warn', toKeepEnd); }; var checkMarks = function (Env, userDoc) { @@ -321,17 +325,17 @@ define([ if (!Env.enabled) { return; } - debug(Env, 'error', 'Check marks'); + debug('error', 'Check marks'); var authDoc = JSON.parse(chainpad.getAuthDoc() || '{}'); if (!authDoc.content || !userDoc.content) { return; } var authPatch = chainpad.getAuthBlock(); if (authPatch.isFromMe) { - debug(Env, 'log', 'Switch branch, from me'); - debug(Env, 'log', authDoc.content); - debug(Env, 'log', authDoc.authormarks.marks); - debug(Env, 'log', userDoc.content); + debug('log', 'Switch branch, from me'); + debug('log', authDoc.content); + debug('log', authDoc.authormarks.marks); + debug('log', userDoc.content); // We're switching to a different branch that was created by us. // We can't trust localDoc anymore because it contains data from the other branch // It means the only changes that we need to consider are ours. @@ -341,8 +345,9 @@ define([ _myOps.forEach(function (op) { fixMarksFromOp(Env, op, authormarks.marks, authDoc.content); }); - debug(Env, 'log', 'Fixed marks'); - debug(Env, 'warn', authormarks.marks); + authormarks.marks = authormarks.marks.filter(Boolean); + debug('log', 'Fixed marks'); + debug('warn', authormarks.marks); setAuthorMarks(Env, authormarks); return; } @@ -355,7 +360,7 @@ define([ if (!userDoc.authormarks || !Array.isArray(userDoc.authormarks.marks)) { return; } - debug(Env, 'warn', 'Begin...'); + debug('warn', 'Begin...'); var localDoc = CodeMirror.canonicalize(editor.getValue()); @@ -365,8 +370,8 @@ define([ var theirOps = ChainPad.Diff.diff(content, authDoc.content); var myOps = ChainPad.Diff.diff(content, localDoc); - debug(Env, 'log', theirOps); - debug(Env, 'log', myOps); + debug('log', theirOps); + debug('log', myOps); if (!myOps.length || !theirOps.length) { return; } @@ -407,7 +412,7 @@ define([ return b.offset - a.offset; }); - debug(Env, 'log', sorted); + debug('log', sorted); // We start from the end so that we don't have to fix the offsets everytime var prev; @@ -428,7 +433,7 @@ define([ prev = op; }); - debug(Env, 'log', toKeepEnd); + debug('log', toKeepEnd); // We now have all the markers located after the first operation (ordered by offset). // Prepend the markers placed before this operation @@ -438,8 +443,8 @@ define([ // Commit our new markers Env.authormarks.marks = toKeepEnd; - debug(Env, 'warn', toKeepEnd); - debug(Env, 'warn', '...End'); + debug('warn', toKeepEnd); + debug('warn', '...End'); }; // Reset marks displayed in CodeMirror to the marks stored in Env @@ -453,8 +458,8 @@ define([ if (!Env.enabled) { return; } - debug(Env, 'error', 'setMarks'); - debug(Env, 'log', Env.authormarks.marks); + debug('error', 'setMarks'); + debug('log', Env.authormarks.marks); var authormarks = Env.authormarks; authormarks.marks.forEach(function (mark) { @@ -510,8 +515,8 @@ define([ if (!Env.enabled) { return void cb(); } - debug(Env, 'error', 'Local change'); - debug(Env, 'log', change, true); + debug('error', 'Local change'); + debug('log', change, true); if (change.origin === "setValue") { // If the content is changed from a remote patch, we call localChange @@ -653,6 +658,17 @@ define([ Env.enabled = false; Env.myAuthorId = 0; + if (Env.devMode) { + debug = function (level, obj, logObject) { + var f = console.log; + if (typeof(console[level]) === "function") { + f = console[level]; + } + if (logObject) { return void f(obj); } + f(JSON.stringify(obj)); + }; + } + var metadataMgr = Env.common.getMetadataMgr(); metadataMgr.onChange(function () { var md = metadataMgr.getMetadata(); From 14b44111109d18d2417b3f3058df0c9e8c3b7d94 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sat, 18 Apr 2020 18:41:04 +0200 Subject: [PATCH 28/52] Translated using Weblate (German) Currently translated at 100.0% (1254 of 1254 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/ --- www/common/translations/messages.de.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index b100a141c..d3f48eab7 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -1343,10 +1343,12 @@ "canvas_select": "Auswahl", "canvas_brush": "Pinsel", "profile_copyKey": "Öffentlichen Schlüssel kopieren", - "cba_hide": "", - "cba_enable": "", - "cba_properties": "", - "cba_disable": "", - "cba_show": "", - "cba_writtenBy": "" + "cba_hide": "Autorenfarben verbergen", + "cba_enable": "Aktivieren", + "cba_properties": "Autorenfarben (experimentell)", + "cba_disable": "Löschen und Deaktivieren", + "cba_show": "Autorenfarben anzeigen", + "cba_writtenBy": "Geschrieben von: {0}", + "cba_hint": "Diese Einstellung wird zukünftig verwendet, wenn du ein neues Pad erstellst.", + "oo_login": "Bitte logge dich ein oder erstelle ein Account, um die Performance von Tabellen zu verbessern." } From 9f5f4a4d5287b3240f3f905d6e3db65b016d5805 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 20 Apr 2020 15:38:27 +0200 Subject: [PATCH 29/52] Fix possible duplicate author id --- www/code/markers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/code/markers.js b/www/code/markers.js index 74541cf4a..125fe82ed 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -623,7 +623,7 @@ define([ return n; }; var getAuthorId = function (Env) { - var existing = Object.keys(Env.authormarks.authors || {}); + var existing = Object.keys(Env.authormarks.authors || {}).map(Number); if (!Env.common.isLoggedIn()) { return authorUid(existing); } var userData = Env.common.getMetadataMgr().getUserData(); From 0ca779dbd1aff261f1ae10812b46cdc97f32cca1 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 20 Apr 2020 16:20:46 +0200 Subject: [PATCH 30/52] Fix 'enable cba' button in properties --- www/common/common-ui-elements.js | 12 +++++++++++- www/common/inner/properties.js | 17 +++++++++++------ www/common/sframe-common-outer.js | 4 ++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index a5c1396c2..82e1f113d 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1621,7 +1621,17 @@ define([ if (!data) { return void UI.alert(Messages.autostore_notAvailable); } - sframeChan.event('EV_PROPERTIES_OPEN'); + sframeChan.query('Q_PROPERTIES_OPEN', null, function (err, data) { + if (!data || !data.cmd) { return; } + if (data.cmd === "UPDATE_METADATA") { + if (!data.key) { return; } + var metadataMgr = common.getMetadataMgr(); + var md = Util.clone(metadataMgr.getMetadata()); + md[data.key] = data.value; + if (!data.value) { delete md[data.key]; } + metadataMgr.updateMetadata(md); + } + }); }); }); break; diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js index 5a17a2c76..3dd3d972a 100644 --- a/www/common/inner/properties.js +++ b/www/common/inner/properties.js @@ -58,6 +58,7 @@ define([ Messages.cba_disable = "Clear all colors and disable"; // XXX if (owned && priv.app === 'code') { (function () { + var sframeChan = common.getSframeChannel(); var md = metadataMgr.getMetadata(); var div = h('div'); var hint = h('div.cp-app-prop-hint', Messages.cba_hint); @@ -73,10 +74,12 @@ define([ classes: 'btn-primary' }, function () { $button.remove(); - var md = Util.clone(metadataMgr.getMetadata()); - md.enableColors = true; + sframeChan.event("EV_SECURE_ACTION", { + cmd: 'UPDATE_METADATA', + key: 'enableColors', + value: true + }); common.setAttribute(['code', 'enableColors'], true); - metadataMgr.updateMetadata(md); setButton(false); }); return; @@ -87,10 +90,12 @@ define([ classes: 'btn-danger' }, function () { $button.remove(); - var md = Util.clone(metadataMgr.getMetadata()); - md.enableColors = false; + sframeChan.event("EV_SECURE_ACTION", { + cmd: 'UPDATE_METADATA', + key: 'enableColors', + value: false + }); common.setAttribute(['code', 'enableColors'], false); - metadataMgr.updateMetadata(md); setButton(true); }); }; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 0e87439b5..4fa73e109 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1036,8 +1036,8 @@ define([ initSecureModal('filepicker', data || {}, cb); }); - sframeChan.on('EV_PROPERTIES_OPEN', function (data) { - initSecureModal('properties', data || {}, null); + sframeChan.on('Q_PROPERTIES_OPEN', function (data, cb) { + initSecureModal('properties', data || {}, cb); }); sframeChan.on('EV_ACCESS_OPEN', function (data) { From 49eacf752b195c2dcd6516c62ebd3774273f9e3c Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 20 Apr 2020 16:37:40 +0200 Subject: [PATCH 31/52] Fix markers errors --- www/code/markers.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index 125fe82ed..ed8453450 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -219,6 +219,8 @@ define([ var toKeep = []; var toJoin = {}; + var firstMarks = first.marks.slice(); + debug('error', "Fix marks"); debug('warn', first); debug('warn', last); @@ -258,9 +260,15 @@ define([ // If we still have markers in "first", store the last one so that we can "join" // everything at the end + // NOTE: we only join if the marks were joined initially! if (first.marks.length) { - var toJoinMark = first.marks[first.marks.length - 1].slice(); + var idx = first.marks.length - 1; + var toJoinMark = first.marks[index].slice(); toJoin = parseMark(toJoinMark); + var next = parseMark(firstMarks[idx + 1]); // always an object + if (toJoin.endLine !== next.startLine || toJoin.endCh !== next.startCh) { + toJoin.overlapOnly = true; + } } @@ -302,13 +310,17 @@ define([ && typeof(toJoin.endCh) !== "undefined") { // Make sure the marks are joined correctly: // fix the start position of the marks to keep - // Note: we must preserve the same end for this mark if it was single line! - if (typeof(toKeepEnd[0][4]) === "undefined") { // Single line - toKeepEnd[0][4] = toKeepEnd[0][3] || (toKeepEnd[0][2]+1); // preserve end ch - toKeepEnd[0][3] = toKeepEnd[0][1]; // preserve end line + var overlap = toKeepEnd[0][1] < toJoin.endLine || + (toKeepEnd[0][1] === toJoin.endLine && toKeepEnd[0][2] < toJoin.endCh); + if (!toJoin.overlapOnly || overlap) { + // Note: we must preserve the same end for this mark if it was single line! + if (typeof(toKeepEnd[0][4]) === "undefined") { // Single line + toKeepEnd[0][4] = toKeepEnd[0][3] || (toKeepEnd[0][2]+1); // preserve end ch + toKeepEnd[0][3] = toKeepEnd[0][1]; // preserve end line + } + toKeepEnd[0][1] = toJoin.endLine; + toKeepEnd[0][2] = toJoin.endCh; } - toKeepEnd[0][1] = toJoin.endLine; - toKeepEnd[0][2] = toJoin.endCh; } debug('log', 'Fixed'); From c6cb9876a7f63db608a1fab3d96baac6263a8200 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 20 Apr 2020 16:47:04 +0200 Subject: [PATCH 32/52] Fix secure iframe conflict with cba button --- www/common/common-ui-elements.js | 6 ++++-- www/common/inner/properties.js | 2 +- www/common/sframe-common-outer.js | 6 +++--- www/secureiframe/inner.js | 3 ++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 82e1f113d..049e26366 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1621,11 +1621,13 @@ define([ if (!data) { return void UI.alert(Messages.autostore_notAvailable); } - sframeChan.query('Q_PROPERTIES_OPEN', null, function (err, data) { + var metadataMgr = common.getMetadataMgr(); + sframeChan.query('Q_PROPERTIES_OPEN', { + metadata: metadataMgr.getMetadata() + }, function (err, data) { if (!data || !data.cmd) { return; } if (data.cmd === "UPDATE_METADATA") { if (!data.key) { return; } - var metadataMgr = common.getMetadataMgr(); var md = Util.clone(metadataMgr.getMetadata()); md[data.key] = data.value; if (!data.value) { delete md[data.key]; } diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js index 3dd3d972a..c82f51e49 100644 --- a/www/common/inner/properties.js +++ b/www/common/inner/properties.js @@ -59,7 +59,7 @@ define([ if (owned && priv.app === 'code') { (function () { var sframeChan = common.getSframeChannel(); - var md = metadataMgr.getMetadata(); + var md = (opts.data && opts.data.metadata) || {}; var div = h('div'); var hint = h('div.cp-app-prop-hint', Messages.cba_hint); var $div = $(div); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 4fa73e109..8cddefb9f 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1020,12 +1020,12 @@ define([ }; SecureModal.$iframe = $('