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(); });