From dd655473f6af7919fe5756f18c773d252f90b48e Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Mon, 24 Aug 2020 12:39:07 -0400 Subject: [PATCH] Add ability to sort rules in _My rules_ pane Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/1055 --- src/css/codemirror.css | 19 ++- src/dyna-rules.html | 4 +- src/js/codemirror/ubo-dynamic-filtering.js | 105 +++++++--------- src/js/dyna-rules.js | 138 +++++++++++++++------ 4 files changed, 165 insertions(+), 101 deletions(-) diff --git a/src/css/codemirror.css b/src/css/codemirror.css index 3b7c1f20e..c048286e9 100644 --- a/src/css/codemirror.css +++ b/src/css/codemirror.css @@ -48,6 +48,23 @@ .cm-staticnetAllow { color: #004f00; } .cm-staticOpt { background-color: #ddd; font-weight: bold; } +/* Rules */ +.cm-s-default .cm-allowrule { + color: green; + font-weight: bold; + } +.cm-s-default .cm-blockrule { + color: red; + font-weight: bold; + } +.cm-s-default .cm-nooprule { + color: darkslategray; + font-weight: bold; + } +.cm-s-default .cm-sortkey { + color: #708; + } + div.CodeMirror span.CodeMirror-matchingbracket { color: unset; } @@ -111,11 +128,9 @@ div.CodeMirror span.CodeMirror-matchingbracket { .CodeMirror-merge-l-deleted { background-image: none; - font-weight: bold; } .CodeMirror-merge-l-inserted { background-image: none; - font-weight: bold; } /* This probably needs to be added to CodeMirror repo */ .CodeMirror-merge-gap { diff --git a/src/dyna-rules.html b/src/dyna-rules.html index 40cbcc17b..a6c063c90 100644 --- a/src/dyna-rules.html +++ b/src/dyna-rules.html @@ -34,7 +34,9 @@ -
+
+ filter  Sort: +
diff --git a/src/js/codemirror/ubo-dynamic-filtering.js b/src/js/codemirror/ubo-dynamic-filtering.js index 4d1d252cc..158e583bd 100644 --- a/src/js/codemirror/ubo-dynamic-filtering.js +++ b/src/js/codemirror/ubo-dynamic-filtering.js @@ -57,7 +57,10 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => { 'noop', ]); const reIsNotHostname = /[:/#?*]/; + const slices = []; + let sliceIndex = 0; const tokens = []; + let tokenIndex = 0; const isSwitchRule = ( ) => { const token = tokens[0]; @@ -73,35 +76,54 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => { return style; }; - const token = stream => { - if ( stream.sol() ) { tokens.length = 0; } - stream.eatSpace(); - const match = stream.match(/\S+/); - if ( Array.isArray(match) === false ) { - return skipToEnd(stream); + const token = function(stream) { + if ( stream.sol() ) { + slices.length = 0; + tokens.length = 0; + const reTokens = /\S+/g; + for (;;) { + const lastIndex = reTokens.lastIndex; + const match = reTokens.exec(stream.string); + if ( match === null ) { break; } + const l = match.index; + const r = reTokens.lastIndex; + if ( l !== lastIndex ) { + slices.push({ t: false, l: lastIndex, r: l }); + } + slices.push({ t: true, l, r }); + tokens.push(stream.string.slice(l, r)); + } + sliceIndex = tokenIndex = 0; } - if ( tokens.length === 4 ) { - return skipToEnd(stream, 'error'); + if ( sliceIndex >= slices.length ) { + return stream.skipToEnd(stream); } - const token = match[0]; - tokens.push(token); + const slice = slices[sliceIndex++]; + stream.pos = slice.r; + if ( slice.t !== true ) { return null; } + const token = tokens[tokenIndex++]; // Field 1: per-site switch or hostname - if ( tokens.length === 1 ) { + if ( tokenIndex === 1 ) { if ( isSwitchRule(token) ) { if ( validSwitches.has(token) === false ) { return skipToEnd(stream, 'error'); } - } else if ( reIsNotHostname.test(token) && token !== '*' ) { + if ( this.sortType === 0 ) { return 'sortkey'; } + return null; + } + if ( reIsNotHostname.test(token) && token !== '*' ) { return skipToEnd(stream, 'error'); } + if ( this.sortType === 1 ) { return 'sortkey'; } return null; } // Field 2: hostname or url - if ( tokens.length === 2 ) { + if ( tokenIndex === 2 ) { if ( isSwitchRule(tokens[0]) ) { if ( reIsNotHostname.test(token) && token !== '*' ) { return skipToEnd(stream, 'error'); } + if ( this.sortType === 1 ) { return 'sortkey'; } } if ( reIsNotHostname.test(token) && @@ -110,15 +132,18 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => { ) { return skipToEnd(stream, 'error'); } + if ( this.sortType === 2 ) { return 'sortkey'; } return null; } // Field 3 - if ( tokens.length === 3 ) { + if ( tokenIndex === 3 ) { // Switch rule if ( isSwitchRule(tokens[0]) ) { if ( validSwitcheStates.has(token) === false ) { return skipToEnd(stream, 'error'); } + if ( token === 'true' ) { return 'blockrule'; } + if ( token === 'false' ) { return 'allowrule'; } return null; } // Hostname rule @@ -141,61 +166,19 @@ CodeMirror.defineMode('ubo-dynamic-filtering', ( ) => { return null; } // Field 4 - if ( tokens.length === 4 ) { + if ( tokenIndex === 4 ) { if ( isSwitchRule(tokens[0]) || validActions.has(token) === false ) { return skipToEnd(stream, 'error'); } - return null; + if ( token === 'allow' ) { return 'allowrule'; } + if ( token === 'block' ) { return 'blockrule'; } + return 'nooprule'; } return skipToEnd(stream); }; - return { token }; + return { token, sortType: 1 }; }); - -/* -Code below is to address -https://github.com/uBlockOrigin/uMatrix-issues/issues/128 - -But this needs fixing because glitchiness in some cases. -I may end up having to create a custom merge view rather -than using the existing CodeMirror one. - -CodeMirror.registerHelper('fold', 'ubo-dynamic-filtering', (cm, start) => { - function isHeader(lineNo) { - const tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)); - return tokentype && /\bheader\b/.test(tokentype); - } - - function headerLevel(lineNo, line, nextLine) { - let match = line && line.match(/^#+/); - if (match && isHeader(lineNo)) return match[0].length; - match = nextLine && nextLine.match(/^[=\-]+\s*$/); - if (match && isHeader(lineNo + 1)) return nextLine[0] === '=' ? 1 : 2; - return 100; - } - - const firstLine = cm.getLine(start.line); - let nextLine = cm.getLine(start.line + 1); - const level = headerLevel(start.line, firstLine, nextLine); - if ( level === 100 ) { return; } - - const lastLineNo = cm.lastLine(); - let end = start.line, - nextNextLine = cm.getLine(end + 2); - while ( end < lastLineNo ) { - if ( headerLevel(end + 1, nextLine, nextNextLine) <= level ) { break; } - ++end; - nextLine = nextNextLine; - nextNextLine = cm.getLine(end + 2); - } - - return { - from: CodeMirror.Pos(start.line, firstLine.length), - to: CodeMirror.Pos(end, cm.getLine(end).length), - }; -}); -*/ diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js index 39c4349a0..9194ec27d 100644 --- a/src/js/dyna-rules.js +++ b/src/js/dyna-rules.js @@ -101,7 +101,7 @@ let differ; // https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22 // ... and modified as needed. -const updateOverlay = (function() { +const updateOverlay = (( ) => { let reFilter; const mode = { token: function(stream) { @@ -135,11 +135,16 @@ const updateOverlay = (function() { // - Minimum amount of text updated const rulesToDoc = function(clearHistory) { + const orig = unfilteredRules.orig.doc; + const edit = unfilteredRules.edit.doc; + orig.startOperation(); + edit.startOperation(); for ( const key in unfilteredRules ) { if ( unfilteredRules.hasOwnProperty(key) === false ) { continue; } const doc = unfilteredRules[key].doc; const rules = filterRules(key); if ( + clearHistory || doc.lineCount() === 1 && doc.getValue() === '' || rules.length === 0 ) { @@ -157,27 +162,27 @@ const rulesToDoc = function(clearHistory) { let afterText = rules.join('\n').trim(); if ( afterText !== '' ) { afterText += '\n'; } const diffs = differ.diff_main(beforeText, afterText); - doc.startOperation(); - let i = diffs.length, - iedit = beforeText.length; + let i = diffs.length; + let iedit = beforeText.length; while ( i-- ) { - let diff = diffs[i]; + const diff = diffs[i]; if ( diff[0] === 0 ) { iedit -= diff[1].length; continue; } - let end = doc.posFromIndex(iedit); + const end = doc.posFromIndex(iedit); if ( diff[0] === 1 ) { doc.replaceRange(diff[1], end, end); continue; } /* diff[0] === -1 */ iedit -= diff[1].length; - let beg = doc.posFromIndex(iedit); + const beg = doc.posFromIndex(iedit); doc.replaceRange('', beg, end); } - doc.endOperation(); } + orig.endOperation(); + edit.endOperation(); cleanEditText = mergeView.editor().getValue().trim(); cleanEditToken = mergeView.editor().changeGeneration(); if ( clearHistory ) { @@ -205,27 +210,75 @@ const filterRules = function(key) { /******************************************************************************/ const renderRules = (( ) => { - const reIsSwitchRule = /^[a-z-]+: /; let firstVisit = true; + let sortType = 1; - // Switches always listed at the top. - const customSort = (a, b) => { - const aIsSwitch = reIsSwitchRule.test(a); - if ( reIsSwitchRule.test(b) === aIsSwitch ) { - return a.localeCompare(b); - } - return aIsSwitch ? -1 : 1; + const reSwRule = /^([^/]+): ([^/ ]+) ([^ ]+)/; + const reRule = /^([^ ]+) ([^/ ]+) ([^ ]+ [^ ]+)/; + const reUrlRule = /^([^ ]+) ([^ ]+) ([^ ]+ [^ ]+)/; + + const reverseHn = function(hn) { + return hn.split('.').reverse().join('.'); }; - return function(details) { - details.permanentRules.sort(customSort); - details.sessionRules.sort(customSort); - unfilteredRules.orig.rules = details.permanentRules; - unfilteredRules.edit.rules = details.sessionRules; - rulesToDoc(firstVisit); - if ( firstVisit ) { + 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) || 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; - mergeView.editor().execCommand('goNextDiff'); + 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); }; @@ -240,7 +293,9 @@ const applyDiff = async function(permanent, toAdd, toRemove) { toAdd: toAdd, toRemove: toRemove, }); - renderRules(details); + unfilteredRules.orig.rules = details.permanentRules; + unfilteredRules.edit.rules = details.sessionRules; + renderRules(); }; /******************************************************************************/ @@ -326,10 +381,10 @@ function exportUserRulesToFile() { /******************************************************************************/ -const onFilterChanged = (function() { - let timer, - overlay = null, - last = ''; +const onFilterChanged = (( ) => { + let timer; + let overlay = null; + let last = ''; const process = function() { timer = undefined; @@ -351,22 +406,28 @@ const onFilterChanged = (function() { }; return function() { - if ( timer !== undefined ) { clearTimeout(timer); } - timer = vAPI.setTimeout(process, 773); + if ( timer !== undefined ) { self.cancelIdleCallback(timer); } + timer = self.requestIdleCallback(process, { timeout: 773 }); }; })(); /******************************************************************************/ +const onSortChanged = function() { + renderRules(true); +}; + +/******************************************************************************/ + const onTextChanged = (( ) => { let timer; - const process = now => { + const process = details => { timer = undefined; const diff = document.getElementById('diff'); let isClean = mergeView.editor().isClean(cleanEditToken); if ( - now && + details === undefined && isClean === false && mergeView.editor().getValue().trim() === cleanEditText ) { @@ -388,8 +449,8 @@ const onTextChanged = (( ) => { }; return function(now) { - if ( timer !== undefined ) { clearTimeout(timer); } - timer = now ? process(now) : vAPI.setTimeout(process, 57); + if ( timer !== undefined ) { self.cancelIdleCallback(timer); } + timer = now ? process() : self.requestIdleCallback(process, { timeout: 57 }); }; })(); @@ -483,7 +544,9 @@ self.hasUnsavedData = function() { vAPI.messaging.send('dashboard', { what: 'getRules', }).then(details => { - renderRules(details); + unfilteredRules.orig.rules = details.permanentRules; + unfilteredRules.edit.rules = details.sessionRules; + renderRules(); }); // Handle user interaction @@ -494,9 +557,10 @@ 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); // https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs -mergeView.editor().on('updateDiff', function() { onTextChanged(); }); +mergeView.editor().on('updateDiff', ( ) => { onTextChanged(); }); /******************************************************************************/