Add auto-completion capability for filter options

Related commit:
- 3e72a47c1f

Use ctrl-space to auto-complete filter options and
`redirect=` resources in _"My filters"_ pane.
This commit is contained in:
Raymond Hill 2020-06-15 19:05:39 -04:00
parent 5cb2283736
commit c9cfd62c21
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
3 changed files with 181 additions and 77 deletions

View File

@ -26,7 +26,12 @@
/******************************************************************************/ /******************************************************************************/
CodeMirror.defineMode('ubo-static-filtering', function() { CodeMirror.defineMode('ubo-static-filtering', function() {
const parser = new vAPI.StaticFilteringParser({ interactive: true }); const StaticFilteringParser = typeof vAPI === 'object'
? vAPI.StaticFilteringParser
: self.StaticFilteringParser;
if ( StaticFilteringParser instanceof Object === false ) { return; }
const parser = new StaticFilteringParser({ interactive: true });
const reDirective = /^!#(?:if|endif|include)\b/; const reDirective = /^!#(?:if|endif|include)\b/;
let parserSlot = 0; let parserSlot = 0;
let netOptionValueMode = false; let netOptionValueMode = false;
@ -245,46 +250,95 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
// Following code is for auto-completion. Reference: // Following code is for auto-completion. Reference:
// https://codemirror.net/demo/complete.html // https://codemirror.net/demo/complete.html
//
// TODO: implement auto-completion for `redirect=`
(( ) => { (( ) => {
if ( typeof vAPI !== 'object' ) { return; } if ( typeof vAPI !== 'object' ) { return; }
let resourceNames = new Map(); const StaticFilteringParser = typeof vAPI === 'object'
? vAPI.StaticFilteringParser
: self.StaticFilteringParser;
if ( StaticFilteringParser instanceof Object === false ) { return; }
vAPI.messaging.send('dashboard', { const parser = new StaticFilteringParser();
what: 'getResourceDetails' const redirectNames = new Map();
}).then(response => { const scriptletNames = new Map();
if ( Array.isArray(response) === false ) { return; }
resourceNames = new Map(response);
});
const parser = new vAPI.StaticFilteringParser(); const getNetOptionHint = function(cursor, isNegated, seedLeft, seedRight) {
const assignPos = seedRight.indexOf('=');
const getHints = function(cm) { if ( assignPos !== -1 ) { seedRight = seedRight.slice(0, assignPos); }
const cursor = cm.getCursor(); const seed = (seedLeft + seedRight).trim();
const line = cm.getLine(cursor.line); const isException = parser.isException();
parser.analyze(line); const out = [];
if ( parser.category !== parser.CATStaticExtFilter ) { for ( let [ name, bits ] of parser.netOptionTokens ) {
return; if ( name.startsWith(seed) === false ) { continue; }
if ( isNegated && (bits & parser.OPTCanNegate) === 0 ) { continue; }
if ( isException ) {
if ( (bits & parser.OPTBlockOnly) !== 0 ) { continue; }
} else {
if ( (bits & parser.OPTAllowOnly) !== 0 ) { continue; }
if ( (assignPos === -1) && (bits & parser.OPTMustAssign) !== 0 ) {
name += '=';
}
}
out.push(name);
} }
if ( parser.hasFlavor(parser.BITFlavorExtScriptlet) === false ) { return {
return; from: { line: cursor.line, ch: cursor.ch - seedLeft.length },
to: { line: cursor.line, ch: cursor.ch + seedRight.length },
list: out,
};
};
const getNetRedirectHint = function(cursor, seedLeft, seedRight) {
const seed = (seedLeft + seedRight).trim();
const out = [];
for ( let text of redirectNames.keys() ) {
if ( text.startsWith(seed) === false ) { continue; }
out.push(text);
} }
return {
from: { line: cursor.line, ch: cursor.ch - seedLeft.length },
to: { line: cursor.line, ch: cursor.ch + seedRight.length },
list: out,
};
};
const getNetHint = function(cursor, line) {
const beg = cursor.ch;
if ( beg < parser.optionsSpan ) { return; }
const lineBefore = line.slice(0, beg);
const lineAfter = line.slice(beg);
let matchLeft = /~?([^$,~]*)$/.exec(lineBefore);
let matchRight = /^([^,]*)/.exec(lineAfter);
if ( matchLeft === null || matchRight === null ) { return; }
let pos = matchLeft[1].indexOf('=');
if ( pos === -1 ) {
return getNetOptionHint(
cursor,
matchLeft[0].startsWith('~'),
matchLeft[1],
matchRight[1]
);
}
return getNetRedirectHint(
cursor,
matchLeft[1].slice(pos + 1),
matchRight[1]
);
};
const getExtScriptletHint = function(cursor, line) {
const beg = cursor.ch; const beg = cursor.ch;
const matchLeft = /#\+\js\(([^,]*)$/.exec(line.slice(0, beg)); const matchLeft = /#\+\js\(([^,]*)$/.exec(line.slice(0, beg));
const matchRight = /^([^,)]*)/.exec(line.slice(beg)); const matchRight = /^([^,)]*)/.exec(line.slice(beg));
if ( matchLeft === null || matchRight === null ) { return; } if ( matchLeft === null || matchRight === null ) { return; }
const seed = (matchLeft[1] + matchRight[1]).trim(); const seed = (matchLeft[1] + matchRight[1]).trim();
const out = []; const out = [];
for ( const [ name, details ] of resourceNames ) { for ( const [ text, displayText ] of scriptletNames ) {
if ( name.startsWith(seed) === false ) { continue; } if ( text.startsWith(seed) === false ) { continue; }
if ( details.hasData !== true ) { continue; } const hint = { text };
if ( name.endsWith('.js') === false ) { continue; } if ( displayText !== '' ) {
const hint = { text: name.slice(0, -3) }; hint.displayText = displayText;
if ( details.aliasOf !== '' ) {
hint.displayText = `${hint.text} (${details.aliasOf})`;
} }
out.push(hint); out.push(hint);
} }
@ -295,7 +349,38 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
}; };
}; };
CodeMirror.registerHelper('hint', 'ubo-static-filtering', getHints); const getHints = function(cm) {
const cursor = cm.getCursor();
const line = cm.getLine(cursor.line);
parser.analyze(line);
if (
parser.category === parser.CATStaticExtFilter &&
parser.hasFlavor(parser.BITFlavorExtScriptlet)
) {
return getExtScriptletHint(cursor, line);
}
if ( parser.category === parser.CATStaticNetFilter ) {
return getNetHint(cursor, line);
}
};
vAPI.messaging.send('dashboard', {
what: 'getResourceDetails'
}).then(response => {
if ( Array.isArray(response) === false ) { return; }
for ( const [ name, details ] of response ) {
const displayText = details.aliasOf !== ''
? `${name} (${details.aliasOf})`
: '';
if ( details.canRedirect ) {
redirectNames.set(name, displayText);
}
if ( details.canInject && name.endsWith('.js') ) {
scriptletNames.set(name.slice(0, -3), displayText);
}
}
CodeMirror.registerHelper('hint', 'ubo-static-filtering', getHints);
});
})(); })();
/******************************************************************************/ /******************************************************************************/

View File

@ -784,7 +784,11 @@ RedirectEngine.prototype.loadBuiltinResources = function() {
RedirectEngine.prototype.getResourceDetails = function() { RedirectEngine.prototype.getResourceDetails = function() {
const out = new Map(); const out = new Map();
for ( const [ name, entry ] of this.resources ) { for ( const [ name, entry ] of this.resources ) {
out.set(name, { hasData: entry.data !== '', aliasOf: '' }); out.set(name, {
canInject: typeof entry.data === 'string',
canRedirect: entry.warURL !== undefined,
aliasOf: '',
});
} }
for ( const [ alias, name ] of this.aliases ) { for ( const [ alias, name ] of this.aliases ) {
const original = out.get(name); const original = out.get(name);

View File

@ -1933,6 +1933,69 @@ Parser.prototype.OPTTokenXhr = OPTTokenXhr;
Parser.prototype.OPTTokenWebrtc = OPTTokenWebrtc; Parser.prototype.OPTTokenWebrtc = OPTTokenWebrtc;
Parser.prototype.OPTTokenWebsocket = OPTTokenWebsocket; Parser.prototype.OPTTokenWebsocket = OPTTokenWebsocket;
Parser.prototype.OPTCanNegate = OPTCanNegate;
Parser.prototype.OPTBlockOnly = OPTBlockOnly;
Parser.prototype.OPTAllowOnly = OPTAllowOnly;
Parser.prototype.OPTMustAssign = OPTMustAssign;
Parser.prototype.OPTAllowMayAssign = OPTAllowMayAssign;
Parser.prototype.OPTDomainList = OPTDomainList;
Parser.prototype.OPTType = OPTType;
Parser.prototype.OPTNetworkType = OPTNetworkType;
Parser.prototype.OPTRedirectType = OPTRedirectType;
Parser.prototype.OPTNotSupported = OPTNotSupported;
/******************************************************************************/
const netOptionTokens = new Map([
[ '1p', OPTToken1p | OPTCanNegate ],
[ 'first-party', OPTToken1p | OPTCanNegate ],
[ '3p', OPTToken3p | OPTCanNegate ],
[ 'third-party', OPTToken3p | OPTCanNegate ],
[ 'all', OPTTokenAll | OPTType | OPTNetworkType ],
[ 'badfilter', OPTTokenBadfilter ],
[ 'cname', OPTTokenCname | OPTAllowOnly | OPTType ],
[ 'csp', OPTTokenCsp | OPTMustAssign | OPTAllowMayAssign ],
[ 'css', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'stylesheet', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'denyallow', OPTTokenDenyAllow | OPTMustAssign | OPTDomainList ],
[ 'doc', OPTTokenDoc | OPTType | OPTNetworkType ],
[ 'document', OPTTokenDoc | OPTType | OPTNetworkType ],
[ 'domain', OPTTokenDomain | OPTMustAssign | OPTDomainList ],
[ 'ehide', OPTTokenEhide | OPTType ],
[ 'elemhide', OPTTokenEhide | OPTType ],
[ 'empty', OPTTokenEmpty | OPTBlockOnly | OPTType | OPTNetworkType | OPTBlockOnly | OPTRedirectType ],
[ 'frame', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'subdocument', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'font', OPTTokenFont | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'genericblock', OPTTokenGenericblock | OPTNotSupported ],
[ 'ghide', OPTTokenGhide | OPTType ],
[ 'generichide', OPTTokenGhide | OPTType ],
[ 'image', OPTTokenImage | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'important', OPTTokenImportant | OPTBlockOnly ],
[ 'inline-font', OPTTokenInlineFont | OPTType ],
[ 'inline-script', OPTTokenInlineScript | OPTType ],
[ 'media', OPTTokenMedia | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'mp4', OPTTokenMp4 | OPTType | OPTNetworkType | OPTBlockOnly | OPTRedirectType ],
[ 'object', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'object-subrequest', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'other', OPTTokenOther | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'ping', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'beacon', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'popunder', OPTTokenPopunder | OPTType ],
[ 'popup', OPTTokenPopup | OPTType ],
[ 'redirect', OPTTokenRedirect | OPTMustAssign | OPTBlockOnly | OPTRedirectType ],
[ 'redirect-rule', OPTTokenRedirectRule | OPTMustAssign | OPTBlockOnly | OPTRedirectType ],
[ 'script', OPTTokenScript | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'shide', OPTTokenShide | OPTType ],
[ 'specifichide', OPTTokenShide | OPTType ],
[ 'xhr', OPTTokenXhr | OPTCanNegate| OPTType | OPTNetworkType ],
[ 'xmlhttprequest', OPTTokenXhr | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'webrtc', OPTTokenWebrtc | OPTNotSupported ],
[ 'websocket', OPTTokenWebsocket | OPTCanNegate | OPTType | OPTNetworkType ],
]);
Parser.prototype.netOptionTokens = netOptionTokens;
/******************************************************************************/ /******************************************************************************/
const Span = class { const Span = class {
@ -2160,54 +2223,6 @@ const NetOptionsIterator = class {
} }
}; };
const netOptionTokens = new Map([
[ '1p', OPTToken1p | OPTCanNegate ],
[ 'first-party', OPTToken1p | OPTCanNegate ],
[ '3p', OPTToken3p | OPTCanNegate ],
[ 'third-party', OPTToken3p | OPTCanNegate ],
[ 'all', OPTTokenAll | OPTType | OPTNetworkType ],
[ 'badfilter', OPTTokenBadfilter ],
[ 'cname', OPTTokenCname | OPTAllowOnly | OPTType ],
[ 'csp', OPTTokenCsp | OPTMustAssign | OPTAllowMayAssign ],
[ 'css', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'stylesheet', OPTTokenCss | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'denyallow', OPTTokenDenyAllow | OPTMustAssign | OPTDomainList ],
[ 'doc', OPTTokenDoc | OPTType | OPTNetworkType ],
[ 'document', OPTTokenDoc | OPTType | OPTNetworkType ],
[ 'domain', OPTTokenDomain | OPTMustAssign | OPTDomainList ],
[ 'ehide', OPTTokenEhide | OPTType ],
[ 'elemhide', OPTTokenEhide | OPTType ],
[ 'empty', OPTTokenEmpty | OPTBlockOnly | OPTType | OPTNetworkType | OPTBlockOnly | OPTRedirectType ],
[ 'frame', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'subdocument', OPTTokenFrame | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'font', OPTTokenFont | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'genericblock', OPTTokenGenericblock | OPTNotSupported ],
[ 'ghide', OPTTokenGhide | OPTType ],
[ 'generichide', OPTTokenGhide | OPTType ],
[ 'image', OPTTokenImage | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'important', OPTTokenImportant | OPTBlockOnly ],
[ 'inline-font', OPTTokenInlineFont | OPTType ],
[ 'inline-script', OPTTokenInlineScript | OPTType ],
[ 'media', OPTTokenMedia | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'mp4', OPTTokenMp4 | OPTType | OPTNetworkType | OPTBlockOnly | OPTRedirectType ],
[ 'object', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'object-subrequest', OPTTokenObject | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'other', OPTTokenOther | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'ping', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'beacon', OPTTokenPing | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'popunder', OPTTokenPopunder | OPTType ],
[ 'popup', OPTTokenPopup | OPTType ],
[ 'redirect', OPTTokenRedirect | OPTMustAssign | OPTBlockOnly | OPTRedirectType ],
[ 'redirect-rule', OPTTokenRedirectRule | OPTMustAssign | OPTBlockOnly | OPTRedirectType ],
[ 'script', OPTTokenScript | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'shide', OPTTokenShide | OPTType ],
[ 'specifichide', OPTTokenShide | OPTType ],
[ 'xhr', OPTTokenXhr | OPTCanNegate| OPTType | OPTNetworkType ],
[ 'xmlhttprequest', OPTTokenXhr | OPTCanNegate | OPTType | OPTNetworkType ],
[ 'webrtc', OPTTokenWebrtc | OPTNotSupported ],
[ 'websocket', OPTTokenWebsocket | OPTCanNegate | OPTType | OPTNetworkType ],
]);
/******************************************************************************/ /******************************************************************************/
// https://github.com/gorhill/uBlock/issues/997 // https://github.com/gorhill/uBlock/issues/997