diff --git a/src/css/codemirror.css b/src/css/codemirror.css
index c048286e9..f784f6069 100644
--- a/src/css/codemirror.css
+++ b/src/css/codemirror.css
@@ -136,6 +136,9 @@ div.CodeMirror span.CodeMirror-matchingbracket {
.CodeMirror-merge-gap {
vertical-align: top;
}
+.CodeMirror-merge-spacer {
+ background-color: var(--bg-code);
+ }
.CodeMirror-hints {
z-index: 10000;
diff --git a/src/css/dyna-rules.css b/src/css/dyna-rules.css
index ca0735562..3c06cef23 100644
--- a/src/css/dyna-rules.css
+++ b/src/css/dyna-rules.css
@@ -35,13 +35,6 @@ body {
#diff .ruleActions .fieldset-header {
margin: 0.5em 0;
}
-#ruleFilter {
- direction: ltr;
- text-align: center;
- }
-#ruleFilter .fa {
- color: var(--fg-0-60);
- }
#revertButton,
#commitButton,
body.editing #diff #exportButton,
@@ -65,6 +58,22 @@ body:not(.editing) #diff.dirty #commitButton:hover {
background-color: var(--button-surface-hover);
}
+#ruleFilter {
+ direction: ltr;
+ display: flex;
+ justify-content: center;
+ }
+#ruleFilter .fa {
+ color: var(--fg-0-60);
+ }
+#ruleFilter #diffCollapse {
+ padding: 0 0.5em;
+ font-size: 150%;
+ }
+#ruleFilter #diffCollapse.active {
+ transform: scale(1, -1);
+ }
+
.codeMirrorContainer {
flex-grow: 1;
}
diff --git a/src/dyna-rules.html b/src/dyna-rules.html
index a6c063c90..dfd2729a1 100644
--- a/src/dyna-rules.html
+++ b/src/dyna-rules.html
@@ -35,7 +35,7 @@
- filter Sort:
+ filter Sort: double-angle-up
diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js
index 7b032073f..f51bce22c 100644
--- a/src/js/dyna-rules.js
+++ b/src/js/dyna-rules.js
@@ -48,15 +48,22 @@ mergeView.leftOriginal().setOption('readOnly', 'nocursor');
uBlockDashboard.patchCodeMirrorEditor(mergeView.editor());
-const unfilteredRules = {
- orig: { doc: mergeView.leftOriginal(), rules: [] },
- edit: { doc: mergeView.editor(), rules: [] }
+const thePanes = {
+ orig: {
+ doc: mergeView.leftOriginal(),
+ original: [],
+ modified: [],
+ },
+ edit: {
+ doc: mergeView.editor(),
+ original: [],
+ modified: [],
+ },
};
let cleanEditToken = 0;
let cleanEditText = '';
-
-let differ;
+let isCollapsed = false;
/******************************************************************************/
@@ -97,6 +104,16 @@ let differ;
/******************************************************************************/
+const getDiffer = (( ) => {
+ let differ;
+ return ( ) => {
+ if ( differ === undefined ) { differ = new diff_match_patch(); }
+ return differ;
+ };
+})();
+
+/******************************************************************************/
+
// Borrowed from...
// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22
// ... and modified as needed.
@@ -135,13 +152,14 @@ const updateOverlay = (( ) => {
// - Minimum amount of text updated
const rulesToDoc = function(clearHistory) {
- const orig = unfilteredRules.orig.doc;
- const edit = unfilteredRules.edit.doc;
+ const orig = thePanes.orig.doc;
+ const edit = thePanes.edit.doc;
orig.startOperation();
edit.startOperation();
- for ( const key in unfilteredRules ) {
- if ( unfilteredRules.hasOwnProperty(key) === false ) { continue; }
- const doc = unfilteredRules[key].doc;
+
+ for ( const key in thePanes ) {
+ if ( thePanes.hasOwnProperty(key) === false ) { continue; }
+ const doc = thePanes[key].doc;
const rules = filterRules(key);
if (
clearHistory ||
@@ -151,7 +169,6 @@ const rulesToDoc = function(clearHistory) {
doc.setValue(rules.length !== 0 ? rules.join('\n') + '\n' : '');
continue;
}
- if ( differ === undefined ) { differ = new diff_match_patch(); }
// https://github.com/uBlockOrigin/uBlock-issues/issues/593
// Ensure the text content always ends with an empty line to avoid
// spurious diff entries.
@@ -161,7 +178,7 @@ const rulesToDoc = function(clearHistory) {
let beforeText = doc.getValue();
let afterText = rules.join('\n').trim();
if ( afterText !== '' ) { afterText += '\n'; }
- const diffs = differ.diff_main(beforeText, afterText);
+ const diffs = getDiffer().diff_main(beforeText, afterText);
let i = diffs.length;
let iedit = beforeText.length;
while ( i-- ) {
@@ -181,112 +198,61 @@ const rulesToDoc = function(clearHistory) {
doc.replaceRange('', beg, end);
}
}
+
+ // Mark ellipses as read-only
+ const marks = edit.getAllMarks();
+ for ( const mark of marks ) {
+ if ( mark.uboEllipsis !== true ) { continue; }
+ mark.clear();
+ }
+ if ( isCollapsed ) {
+ for ( let iline = 0, n = edit.lineCount(); iline < n; iline++ ) {
+ if ( edit.getLine(iline) !== '...' ) { continue; }
+ const mark = edit.markText(
+ { line: iline, ch: 0 },
+ { line: iline + 1, ch: 0 },
+ { readOnly: true }
+ );
+ mark.uboEllipsis = true;
+ }
+ }
+
orig.endOperation();
edit.endOperation();
cleanEditText = mergeView.editor().getValue().trim();
cleanEditToken = mergeView.editor().changeGeneration();
- if ( clearHistory ) {
- mergeView.editor().clearHistory();
- }
+
+ if ( clearHistory !== true ) { return; }
+
+ mergeView.editor().clearHistory();
+ const chunks = mergeView.leftChunks();
+ if ( chunks.length === 0 ) { return; }
+ const ldoc = thePanes.orig.doc;
+ const { clientHeight } = ldoc.getScrollInfo();
+ const line = Math.min(chunks[0].editFrom, chunks[0].origFrom);
+ ldoc.setCursor(line, 0);
+ ldoc.scrollIntoView(
+ { line, ch: 0 },
+ (clientHeight - ldoc.defaultTextHeight()) / 2
+ );
};
/******************************************************************************/
const filterRules = function(key) {
const filter = uDom.nodeFromSelector('#ruleFilter input').value;
- let rules = unfilteredRules[key].rules;
- if ( filter !== '' ) {
- rules = rules.slice();
- let i = rules.length;
- while ( i-- ) {
- if ( rules[i].indexOf(filter) === -1 ) {
- rules.splice(i, 1);
- }
- }
+ const rules = thePanes[key].modified;
+ if ( filter === '' ) { return rules; }
+ const out = [];
+ for ( const rule of rules ) {
+ if ( rule.indexOf(filter) === -1 ) { continue; }
+ out.push(rule);
}
- return rules;
+ return out;
};
/******************************************************************************/
-const renderRules = (( ) => {
- let firstVisit = true;
- let sortType = 1;
-
- const reSwRule = /^([^/]+): ([^/ ]+) ([^ ]+)/;
- const reRule = /^([^ ]+) ([^/ ]+) ([^ ]+ [^ ]+)/;
- const reUrlRule = /^([^ ]+) ([^ ]+) ([^ ]+ [^ ]+)/;
-
- const reverseHn = function(hn) {
- return hn.split('.').reverse().join('.');
- };
-
- const slotFromRule = function(rule) {
- let type, srcHn, desHn, extra = '';
- let match = reSwRule.exec(rule);
- if ( match !== null ) {
- type = ' ' + match[1];
- srcHn = reverseHn(match[2]);
- desHn = srcHn;
- } else if ( (match = reRule.exec(rule)) !== null ) {
- type = '\x10FFFE';
- srcHn = reverseHn(match[1]);
- desHn = reverseHn(match[2]);
- } else if ( (match = reUrlRule.exec(rule)) !== null ) {
- type = '\x10FFFF';
- srcHn = reverseHn(match[1]);
- desHn = reverseHn(vAPI.hostnameFromURI(match[2]));
- extra = rule;
- }
- if ( sortType === 0 ) {
- return { rule, token: `${type} ${srcHn} ${desHn} ${extra}` };
- }
- if ( sortType === 1 ) {
- return { rule, token: `${srcHn} ${type} ${desHn} ${extra}` };
- }
- return { rule, token: `${desHn} ${type} ${srcHn} ${extra}` };
- };
-
- const sort = rules => {
- const slots = [];
- for ( let i = 0; i < rules.length; i++ ) {
- slots.push(slotFromRule(rules[i], 1));
- }
- slots.sort((a, b) => a.token.localeCompare(b.token));
- for ( let i = 0; i < rules.length; i++ ) {
- rules[i] = slots[i].rule;
- }
- };
-
- return function(clearHistory = false) {
- const select = document.querySelector('#ruleFilter select');
- sortType = parseInt(select.value, 10);
- if ( isNaN(sortType) ) { sortType = 1; }
- unfilteredRules.orig.doc.getMode().sortType = sortType;
- unfilteredRules.edit.doc.getMode().sortType = sortType;
- sort(unfilteredRules.orig.rules);
- sort(unfilteredRules.edit.rules);
- rulesToDoc(firstVisit || clearHistory);
- if ( firstVisit || clearHistory ) {
- firstVisit = false;
- const chunks = mergeView.leftChunks();
- if ( chunks.length !== 0 ) {
- const ldoc = unfilteredRules.orig.doc;
- const { clientHeight } = ldoc.getScrollInfo();
- const line = Math.min(chunks[0].editFrom, chunks[0].origFrom);
- ldoc.setCursor(line, 0);
- ldoc.scrollIntoView(
- { line, ch: 0 },
- (clientHeight - ldoc.defaultTextHeight()) / 2
- );
- }
- }
- onTextChanged(true);
- };
-})();
-
-/******************************************************************************/
-
const applyDiff = async function(permanent, toAdd, toRemove) {
const details = await vAPI.messaging.send('dashboard', {
what: 'modifyRuleset',
@@ -294,9 +260,9 @@ const applyDiff = async function(permanent, toAdd, toRemove) {
toAdd: toAdd,
toRemove: toRemove,
});
- unfilteredRules.orig.rules = details.permanentRules;
- unfilteredRules.edit.rules = details.sessionRules;
- renderRules();
+ thePanes.orig.original = details.permanentRules;
+ thePanes.edit.original = details.sessionRules;
+ onPresentationChanged();
};
/******************************************************************************/
@@ -414,9 +380,109 @@ const onFilterChanged = (( ) => {
/******************************************************************************/
-const onSortChanged = function() {
- renderRules(true);
-};
+const onPresentationChanged = (( ) => {
+ let sortType = 1;
+
+ const reSwRule = /^([^/]+): ([^/ ]+) ([^ ]+)/;
+ const reRule = /^([^ ]+) ([^/ ]+) ([^ ]+ [^ ]+)/;
+ const reUrlRule = /^([^ ]+) ([^ ]+) ([^ ]+ [^ ]+)/;
+
+ const reverseHn = function(hn) {
+ return hn.split('.').reverse().join('.');
+ };
+
+ const slotFromRule = rule => {
+ let type, srcHn, desHn, extra = '';
+ let match = reSwRule.exec(rule);
+ if ( match !== null ) {
+ type = ' ' + match[1];
+ srcHn = reverseHn(match[2]);
+ desHn = srcHn;
+ } else if ( (match = reRule.exec(rule)) !== null ) {
+ type = '\x10FFFE';
+ srcHn = reverseHn(match[1]);
+ desHn = reverseHn(match[2]);
+ } else if ( (match = reUrlRule.exec(rule)) !== null ) {
+ type = '\x10FFFF';
+ srcHn = reverseHn(match[1]);
+ desHn = reverseHn(vAPI.hostnameFromURI(match[2]));
+ extra = rule;
+ }
+ if ( sortType === 0 ) {
+ return { rule, token: `${type} ${srcHn} ${desHn} ${extra}` };
+ }
+ if ( sortType === 1 ) {
+ return { rule, token: `${srcHn} ${type} ${desHn} ${extra}` };
+ }
+ return { rule, token: `${desHn} ${type} ${srcHn} ${extra}` };
+ };
+
+ const sort = rules => {
+ const slots = [];
+ for ( let i = 0; i < rules.length; i++ ) {
+ slots.push(slotFromRule(rules[i], 1));
+ }
+ slots.sort((a, b) => a.token.localeCompare(b.token));
+ for ( let i = 0; i < rules.length; i++ ) {
+ rules[i] = slots[i].rule;
+ }
+ };
+
+ const collapse = ( ) => {
+ if ( isCollapsed !== true ) { return; }
+ const diffs = getDiffer().diff_main(
+ thePanes.orig.modified.join('\n'),
+ thePanes.edit.modified.join('\n')
+ );
+ const ll = []; let il = 0, lellipsis = false;
+ const rr = []; let ir = 0, rellipsis = false;
+ for ( let i = 0; i < diffs.length; i++ ) {
+ const diff = diffs[i];
+ if ( diff[0] === 0 ) {
+ lellipsis = rellipsis = true;
+ il += 1; ir += 1;
+ continue;
+ }
+ if ( diff[0] < 0 ) {
+ if ( lellipsis ) {
+ ll.push('...');
+ if ( rellipsis ) { rr.push('...'); }
+ lellipsis = rellipsis = false;
+ }
+ ll.push(diff[1].trim());
+ il += 1;
+ continue;
+ }
+ /* diff[0] > 0 */
+ if ( rellipsis ) {
+ rr.push('...');
+ if ( lellipsis ) { ll.push('...'); }
+ lellipsis = rellipsis = false;
+ }
+ rr.push(diff[1].trim());
+ ir += 1;
+ }
+ if ( lellipsis ) { ll.push('...'); }
+ if ( rellipsis ) { rr.push('...'); }
+ thePanes.orig.modified = ll;
+ thePanes.edit.modified = rr;
+ };
+
+ return function(clearHistory) {
+ thePanes.orig.modified = thePanes.orig.original.slice();
+ thePanes.edit.modified = thePanes.edit.original.slice();
+ const select = document.querySelector('#ruleFilter select');
+ sortType = parseInt(select.value, 10);
+ if ( isNaN(sortType) ) { sortType = 1; }
+ thePanes.orig.doc.getMode().sortType = sortType;
+ thePanes.edit.doc.getMode().sortType = sortType;
+ sort(thePanes.orig.modified);
+ sort(thePanes.edit.modified);
+ collapse();
+ rulesToDoc(clearHistory);
+ onTextChanged(clearHistory);
+ };
+})();
/******************************************************************************/
@@ -506,9 +572,8 @@ const editSaveHandler = function() {
onTextChanged(true);
return;
}
- if ( differ === undefined ) { differ = new diff_match_patch(); }
const toAdd = [], toRemove = [];
- const diffs = differ.diff_main(cleanEditText, editText);
+ const diffs = getDiffer().diff_main(cleanEditText, editText);
for ( const diff of diffs ) {
if ( diff[0] === 1 ) {
toAdd.push(diff[1]);
@@ -545,9 +610,9 @@ self.hasUnsavedData = function() {
vAPI.messaging.send('dashboard', {
what: 'getRules',
}).then(details => {
- unfilteredRules.orig.rules = details.permanentRules;
- unfilteredRules.edit.rules = details.sessionRules;
- renderRules();
+ thePanes.orig.original = details.permanentRules;
+ thePanes.edit.original = details.sessionRules;
+ onPresentationChanged(true);
});
// Handle user interaction
@@ -558,7 +623,13 @@ uDom('#revertButton').on('click', revertAllHandler);
uDom('#commitButton').on('click', commitAllHandler);
uDom('#editSaveButton').on('click', editSaveHandler);
uDom('#ruleFilter input').on('input', onFilterChanged);
-uDom('#ruleFilter select').on('input', onSortChanged);
+uDom('#ruleFilter select').on('input', ( ) => {
+ onPresentationChanged(true);
+});
+uDom('#ruleFilter #diffCollapse').on('click', ev => {
+ isCollapsed = ev.target.classList.toggle('active');
+ onPresentationChanged(true);
+});
// https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs
mergeView.editor().on('updateDiff', ( ) => { onTextChanged(); });