diff --git a/src/1p-filters.html b/src/1p-filters.html index ecd186277..26fce7f6e 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -49,6 +49,7 @@ + @@ -59,6 +60,7 @@ + diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index e02de5df2..86a5b707d 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -108,17 +108,72 @@ const userFiltersChanged = function(changed) { /******************************************************************************/ -const renderUserFilters = async function() { +// https://github.com/gorhill/uBlock/issues/3704 +// Merge changes to user filters occurring in the background with changes +// made in the editor. The code assumes that no deletion occurred in the +// background. + +const threeWayMerge = function(newContent) { + const prvContent = cachedUserFilters.trim().split(/\n/); + const differ = new self.diff_match_patch(); + const newChanges = differ.diff( + prvContent, + newContent.trim().split(/\n/) + ); + const usrChanges = differ.diff( + prvContent, + getEditorText().trim().split(/\n/) + ); + const out = []; + let i = 0, j = 0, k = 0; + while ( i < prvContent.length ) { + for ( ; j < newChanges.length; j++ ) { + const change = newChanges[j]; + if ( change[0] !== 1 ) { break; } + out.push(change[1]); + } + for ( ; k < usrChanges.length; k++ ) { + const change = usrChanges[k]; + if ( change[0] !== 1 ) { break; } + out.push(change[1]); + } + if ( k === usrChanges.length || usrChanges[k][0] !== -1 ) { + out.push(prvContent[i]); + } + i += 1; j += 1; k += 1; + } + for ( ; j < newChanges.length; j++ ) { + const change = newChanges[j]; + if ( change[0] !== 1 ) { continue; } + out.push(change[1]); + } + for ( ; k < usrChanges.length; k++ ) { + const change = usrChanges[k]; + if ( change[0] !== 1 ) { continue; } + out.push(change[1]); + } + return out.join('\n'); +}; + +/******************************************************************************/ + +const renderUserFilters = async function(merge = false) { const details = await vAPI.messaging.send('dashboard', { what: 'readUserFilters', }); if ( details instanceof Object === false || details.error ) { return; } - let content = details.content.trim(); - cachedUserFilters = content; - setEditorText(content); + const newContent = details.content.trim(); - userFiltersChanged(false); + if ( merge && self.hasUnsavedData() ) { + setEditorText(threeWayMerge(newContent)); + userFiltersChanged(true); + } else { + setEditorText(newContent); + userFiltersChanged(false); + } + + cachedUserFilters = newContent; }; /******************************************************************************/ @@ -240,21 +295,26 @@ uDom('#exportUserFiltersToFile').on('click', exportUserFiltersToFile); uDom('#userFiltersApply').on('click', ( ) => { applyChanges(); }); uDom('#userFiltersRevert').on('click', revertChanges); -// https://github.com/gorhill/uBlock/issues/3706 -// Save/restore cursor position -// -// CodeMirror reference: https://codemirror.net/doc/manual.html#api_selection -{ - let curline = 0; - let timer; +(async ( ) => { + await renderUserFilters(); - renderUserFilters().then(( ) => { - cmEditor.clearHistory(); - return vAPI.localStorage.getItemAsync('myFiltersCursorPosition'); - }).then(line => { + cmEditor.clearHistory(); + + // https://github.com/gorhill/uBlock/issues/3706 + // Save/restore cursor position + { + const line = + await vAPI.localStorage.getItemAsync('myFiltersCursorPosition'); if ( typeof line === 'number' ) { cmEditor.setCursor(line, 0); } + } + + // https://github.com/gorhill/uBlock/issues/3706 + // Save/restore cursor position + { + let curline = 0; + let timer; cmEditor.on('cursorActivity', ( ) => { if ( timer !== undefined ) { return; } if ( cmEditor.getCursor().line === curline ) { return; } @@ -264,8 +324,29 @@ uDom('#userFiltersRevert').on('click', revertChanges); vAPI.localStorage.setItem('myFiltersCursorPosition', curline); }, 701); }); + } + + // https://github.com/gorhill/uBlock/issues/3704 + // Merge changes to user filters occurring in the background + vAPI.broadcastListener.add(msg => { + switch ( msg.what ) { + case 'userFiltersUpdated': { + cmEditor.startOperation(); + const scroll = cmEditor.getScrollInfo(); + const selections = cmEditor.listSelections(); + renderUserFilters(true).then(( ) => { + cmEditor.clearHistory(); + cmEditor.setSelection(selections[0].anchor, selections[0].head); + cmEditor.scrollTo(scroll.left, scroll.top); + cmEditor.endOperation(); + }); + break; + } + default: + break; + } }); -} +})(); cmEditor.on('changes', userFiltersChanged); CodeMirror.commands.save = applyChanges; diff --git a/src/js/storage.js b/src/js/storage.js index 5f12022ea..e0fb1668c 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -331,14 +331,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { this.netWhitelistModifyTime = Date.now(); }; -/******************************************************************************* - - TODO(seamless migration): - The code related to 'remoteBlacklist' can be removed when I am confident - all users have moved to a version of uBO which no longer depends on - the property 'remoteBlacklists, i.e. v1.11 and beyond. - -**/ +/******************************************************************************/ µBlock.loadSelectedFilterLists = async function() { const bin = await vAPI.storage.get('selectedFilterLists'); @@ -559,6 +552,8 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { if ( options.killCache ) { browser.webRequest.handlerBehaviorChanged(); } + + vAPI.messaging.broadcast({ what: 'userFiltersUpdated' }); }; µBlock.createUserFilters = function(details) { @@ -1281,7 +1276,6 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { }; const destroy = function() { - µb.cacheStorage.remove('selfie'); // TODO: obsolete, remove eventually. µb.assets.remove(/^selfie\//); µb.selfieIsInvalid = true; createTimer = vAPI.setTimeout(( ) => { diff --git a/src/lib/diff/swatinem_diff.js b/src/lib/diff/swatinem_diff.js index 481f2cfbc..d986b2f64 100644 --- a/src/lib/diff/swatinem_diff.js +++ b/src/lib/diff/swatinem_diff.js @@ -28,6 +28,7 @@ diff_match_patch, as expected by CodeMirror. 2018-12-20 gorhill: + =================== There was an issue causing the wrong diff data to be issued, for instance when diff-ing these two URLs on a character granularity basis (failure point is marked): @@ -60,8 +61,10 @@ So I will assume this was the issue. - TODO: - - Apply other changes which were applied to the original code + 2021-07-17 gorhill: + =================== + Added pure diff() method which natively deals with arrays and other minor + changes related to ES6. **/ @@ -77,15 +80,20 @@ context.diff_match_patch.prototype.diff_main = function(a, b) { if ( a === b ) { return [ [ 0, a ] ]; } - var aa = a.match(/\n|[^\n]+\n?/g) || []; - var bb = b.match(/\n|[^\n]+\n?/g) || []; - var d = new Diff(aa, bb, eqlDefault); + const aa = a.match(/\n|[^\n]+\n?/g) || []; + const bb = b.match(/\n|[^\n]+\n?/g) || []; + const d = new Diff(aa, bb); return d.editscript(); }; - function eqlDefault(a, b) { return a === b; } + context.diff_match_patch.prototype.diff = function(a, b) { + const d = new Diff(a, b); + return d.editscript(); + }; - function Diff(a, b, eql) { + const eqlDefault = (a, b) => a === b; + + function Diff(a, b, eql = eqlDefault) { this.a = a; this.b = b; this.eql = eql; @@ -101,10 +109,10 @@ } Diff.prototype.editscript = function Diff_editscript() { - var moda = this.moda, modb = this.modb; + const moda = this.moda, modb = this.modb; var astart = 0, aend = moda.length; var bstart = 0, bend = modb.length; - var result = []; + const result = []; while (astart < aend || bstart < bend) { if (astart < aend && bstart < bend) { if (!moda[astart] && !modb[bstart]) { @@ -131,7 +139,7 @@ }; Diff.prototype.lcs = function Diff_lcs(astart, aend, bstart, bend) { - var a = this.a, b = this.b, eql = this.eql; + const a = this.a, b = this.b, eql = this.eql; // separate common head while (astart < aend && bstart < bend && eql(a[astart], b[bstart])) { astart++; bstart++; @@ -154,7 +162,7 @@ astart++; } } else { - var snake = this.snake(astart, aend, bstart, bend); + const snake = this.snake(astart, aend, bstart, bend); this.lcs(astart, snake.x, bstart, snake.y); this.lcs(snake.x, aend, snake.y, bend); @@ -162,27 +170,27 @@ }; Diff.prototype.snake = function Diff_snake(astart, aend, bstart, bend) { - var a = this.a, b = this.b, eql = this.eql; + const a = this.a, b = this.b, eql = this.eql; - var N = aend - astart, - M = bend - bstart; + const N = aend - astart; + const M = bend - bstart; - var kdown = astart - bstart; - var kup = aend - bend; + const kdown = astart - bstart; + const kup = aend - bend; - var delta = N - M; - var deltaOdd = delta & 1; + const delta = N - M; + const deltaOdd = delta & 1; - var down = this.down; + const down = this.down; down[kdown + 1] = astart; - var up = this.up; + const up = this.up; up[kup - 1] = aend; - var Dmax = (N + M + 1) / 2; - for (var D = 0; D <= Dmax; D++) { - var k, x, y; + const Dmax = (N + M + 1) / 2; + for (let D = 0; D <= Dmax; D++) { // forward path - for (k = kdown - D; k <= kdown + D; k += 2) { + for (let k = kdown - D; k <= kdown + D; k += 2) { + let x; if (k === kdown - D) { x = down[k + 1]; // down } else { @@ -191,7 +199,7 @@ x = down[k + 1]; // down } } - y = x - k; + let y = x - k; while (x < aend && y < bend && eql(a[x], b[y])) { x++; y++; // diagonal @@ -210,7 +218,8 @@ } // reverse path - for (k = kup - D; k <= kup + D; k += 2) { + for (let k = kup - D; k <= kup + D; k += 2) { + let x; if (k === kup + D) { x = up[k - 1]; // up } else { @@ -219,7 +228,7 @@ x = up[k - 1]; // up } } - y = x - k; + let y = x - k; while (x > astart && y > bstart && eql(a[x - 1], b[y - 1])) { x--; y--; // diagonal