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