Add ability to collpase unchanged rules in _My rules_ pane

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/604
This commit is contained in:
Raymond Hill 2020-08-25 13:23:30 -04:00
parent 5415b980b1
commit 532ed5c390
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
4 changed files with 206 additions and 123 deletions

View File

@ -136,6 +136,9 @@ div.CodeMirror span.CodeMirror-matchingbracket {
.CodeMirror-merge-gap { .CodeMirror-merge-gap {
vertical-align: top; vertical-align: top;
} }
.CodeMirror-merge-spacer {
background-color: var(--bg-code);
}
.CodeMirror-hints { .CodeMirror-hints {
z-index: 10000; z-index: 10000;

View File

@ -35,13 +35,6 @@ body {
#diff .ruleActions .fieldset-header { #diff .ruleActions .fieldset-header {
margin: 0.5em 0; margin: 0.5em 0;
} }
#ruleFilter {
direction: ltr;
text-align: center;
}
#ruleFilter .fa {
color: var(--fg-0-60);
}
#revertButton, #revertButton,
#commitButton, #commitButton,
body.editing #diff #exportButton, body.editing #diff #exportButton,
@ -65,6 +58,22 @@ body:not(.editing) #diff.dirty #commitButton:hover {
background-color: var(--button-surface-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 { .codeMirrorContainer {
flex-grow: 1; flex-grow: 1;
} }

View File

@ -35,7 +35,7 @@
<button type="button" class="iconifiable important disabled" id="editSaveButton"><span class="fa">&#xf0c7;</span><span data-i18n="rulesEditSave"></span></button> <button type="button" class="iconifiable important disabled" id="editSaveButton"><span class="fa">&#xf0c7;</span><span data-i18n="rulesEditSave"></span></button>
</div> </div>
<div id="ruleFilter"> <div id="ruleFilter">
<span><span class="fa-icon">filter</span>&nbsp;<input type="search" size="20"></span>&emsp;Sort: <select><option value="0">Rule type<option value="1" selected>Source<option value="2">Destination</select> <span><span class="fa-icon">filter</span>&nbsp;<input type="search" size="20"></span>&emsp;Sort:&nbsp;<select><option value="0" selected>Rule type<option value="1">Source<option value="2">Destination</select>&emsp;<span id="diffCollapse" class="fa-icon">double-angle-up</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -48,15 +48,22 @@ mergeView.leftOriginal().setOption('readOnly', 'nocursor');
uBlockDashboard.patchCodeMirrorEditor(mergeView.editor()); uBlockDashboard.patchCodeMirrorEditor(mergeView.editor());
const unfilteredRules = { const thePanes = {
orig: { doc: mergeView.leftOriginal(), rules: [] }, orig: {
edit: { doc: mergeView.editor(), rules: [] } doc: mergeView.leftOriginal(),
original: [],
modified: [],
},
edit: {
doc: mergeView.editor(),
original: [],
modified: [],
},
}; };
let cleanEditToken = 0; let cleanEditToken = 0;
let cleanEditText = ''; let cleanEditText = '';
let isCollapsed = false;
let differ;
/******************************************************************************/ /******************************************************************************/
@ -97,6 +104,16 @@ let differ;
/******************************************************************************/ /******************************************************************************/
const getDiffer = (( ) => {
let differ;
return ( ) => {
if ( differ === undefined ) { differ = new diff_match_patch(); }
return differ;
};
})();
/******************************************************************************/
// Borrowed from... // Borrowed from...
// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22 // https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22
// ... and modified as needed. // ... and modified as needed.
@ -135,13 +152,14 @@ const updateOverlay = (( ) => {
// - Minimum amount of text updated // - Minimum amount of text updated
const rulesToDoc = function(clearHistory) { const rulesToDoc = function(clearHistory) {
const orig = unfilteredRules.orig.doc; const orig = thePanes.orig.doc;
const edit = unfilteredRules.edit.doc; const edit = thePanes.edit.doc;
orig.startOperation(); orig.startOperation();
edit.startOperation(); edit.startOperation();
for ( const key in unfilteredRules ) {
if ( unfilteredRules.hasOwnProperty(key) === false ) { continue; } for ( const key in thePanes ) {
const doc = unfilteredRules[key].doc; if ( thePanes.hasOwnProperty(key) === false ) { continue; }
const doc = thePanes[key].doc;
const rules = filterRules(key); const rules = filterRules(key);
if ( if (
clearHistory || clearHistory ||
@ -151,7 +169,6 @@ const rulesToDoc = function(clearHistory) {
doc.setValue(rules.length !== 0 ? rules.join('\n') + '\n' : ''); doc.setValue(rules.length !== 0 ? rules.join('\n') + '\n' : '');
continue; continue;
} }
if ( differ === undefined ) { differ = new diff_match_patch(); }
// https://github.com/uBlockOrigin/uBlock-issues/issues/593 // https://github.com/uBlockOrigin/uBlock-issues/issues/593
// Ensure the text content always ends with an empty line to avoid // Ensure the text content always ends with an empty line to avoid
// spurious diff entries. // spurious diff entries.
@ -161,7 +178,7 @@ const rulesToDoc = function(clearHistory) {
let beforeText = doc.getValue(); let beforeText = doc.getValue();
let afterText = rules.join('\n').trim(); let afterText = rules.join('\n').trim();
if ( afterText !== '' ) { afterText += '\n'; } if ( afterText !== '' ) { afterText += '\n'; }
const diffs = differ.diff_main(beforeText, afterText); const diffs = getDiffer().diff_main(beforeText, afterText);
let i = diffs.length; let i = diffs.length;
let iedit = beforeText.length; let iedit = beforeText.length;
while ( i-- ) { while ( i-- ) {
@ -181,97 +198,36 @@ const rulesToDoc = function(clearHistory) {
doc.replaceRange('', beg, end); 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(); orig.endOperation();
edit.endOperation(); edit.endOperation();
cleanEditText = mergeView.editor().getValue().trim(); cleanEditText = mergeView.editor().getValue().trim();
cleanEditToken = mergeView.editor().changeGeneration(); cleanEditToken = mergeView.editor().changeGeneration();
if ( clearHistory ) {
if ( clearHistory !== true ) { return; }
mergeView.editor().clearHistory(); mergeView.editor().clearHistory();
}
};
/******************************************************************************/
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);
}
}
}
return rules;
};
/******************************************************************************/
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(); const chunks = mergeView.leftChunks();
if ( chunks.length !== 0 ) { if ( chunks.length === 0 ) { return; }
const ldoc = unfilteredRules.orig.doc; const ldoc = thePanes.orig.doc;
const { clientHeight } = ldoc.getScrollInfo(); const { clientHeight } = ldoc.getScrollInfo();
const line = Math.min(chunks[0].editFrom, chunks[0].origFrom); const line = Math.min(chunks[0].editFrom, chunks[0].origFrom);
ldoc.setCursor(line, 0); ldoc.setCursor(line, 0);
@ -279,11 +235,21 @@ const renderRules = (( ) => {
{ line, ch: 0 }, { line, ch: 0 },
(clientHeight - ldoc.defaultTextHeight()) / 2 (clientHeight - ldoc.defaultTextHeight()) / 2
); );
};
/******************************************************************************/
const filterRules = function(key) {
const filter = uDom.nodeFromSelector('#ruleFilter input').value;
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 out;
onTextChanged(true); };
};
})();
/******************************************************************************/ /******************************************************************************/
@ -294,9 +260,9 @@ const applyDiff = async function(permanent, toAdd, toRemove) {
toAdd: toAdd, toAdd: toAdd,
toRemove: toRemove, toRemove: toRemove,
}); });
unfilteredRules.orig.rules = details.permanentRules; thePanes.orig.original = details.permanentRules;
unfilteredRules.edit.rules = details.sessionRules; thePanes.edit.original = details.sessionRules;
renderRules(); onPresentationChanged();
}; };
/******************************************************************************/ /******************************************************************************/
@ -414,9 +380,109 @@ const onFilterChanged = (( ) => {
/******************************************************************************/ /******************************************************************************/
const onSortChanged = function() { const onPresentationChanged = (( ) => {
renderRules(true); 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); onTextChanged(true);
return; return;
} }
if ( differ === undefined ) { differ = new diff_match_patch(); }
const toAdd = [], toRemove = []; const toAdd = [], toRemove = [];
const diffs = differ.diff_main(cleanEditText, editText); const diffs = getDiffer().diff_main(cleanEditText, editText);
for ( const diff of diffs ) { for ( const diff of diffs ) {
if ( diff[0] === 1 ) { if ( diff[0] === 1 ) {
toAdd.push(diff[1]); toAdd.push(diff[1]);
@ -545,9 +610,9 @@ self.hasUnsavedData = function() {
vAPI.messaging.send('dashboard', { vAPI.messaging.send('dashboard', {
what: 'getRules', what: 'getRules',
}).then(details => { }).then(details => {
unfilteredRules.orig.rules = details.permanentRules; thePanes.orig.original = details.permanentRules;
unfilteredRules.edit.rules = details.sessionRules; thePanes.edit.original = details.sessionRules;
renderRules(); onPresentationChanged(true);
}); });
// Handle user interaction // Handle user interaction
@ -558,7 +623,13 @@ uDom('#revertButton').on('click', revertAllHandler);
uDom('#commitButton').on('click', commitAllHandler); uDom('#commitButton').on('click', commitAllHandler);
uDom('#editSaveButton').on('click', editSaveHandler); uDom('#editSaveButton').on('click', editSaveHandler);
uDom('#ruleFilter input').on('input', onFilterChanged); 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 // https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs
mergeView.editor().on('updateDiff', ( ) => { onTextChanged(); }); mergeView.editor().on('updateDiff', ( ) => { onTextChanged(); });