From c9cfd62c2149d9c9033a4784b2557b0e634d3f05 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Mon, 15 Jun 2020 19:05:39 -0400 Subject: [PATCH] Add auto-completion capability for filter options Related commit: - https://github.com/gorhill/uBlock/commit/3e72a47c1fa52af3163f370facf5b9046a7b10b0 Use ctrl-space to auto-complete filter options and `redirect=` resources in _"My filters"_ pane. --- src/js/codemirror/ubo-static-filtering.js | 141 +++++++++++++++++----- src/js/redirect-engine.js | 6 +- src/js/static-filtering-parser.js | 111 +++++++++-------- 3 files changed, 181 insertions(+), 77 deletions(-) diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index b34c900c8..5d5f35920 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -26,7 +26,12 @@ /******************************************************************************/ 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/; let parserSlot = 0; let netOptionValueMode = false; @@ -245,46 +250,95 @@ CodeMirror.defineMode('ubo-static-filtering', function() { // Following code is for auto-completion. Reference: // https://codemirror.net/demo/complete.html -// -// TODO: implement auto-completion for `redirect=` (( ) => { 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', { - what: 'getResourceDetails' - }).then(response => { - if ( Array.isArray(response) === false ) { return; } - resourceNames = new Map(response); - }); + const parser = new StaticFilteringParser(); + const redirectNames = new Map(); + const scriptletNames = new Map(); - const parser = new vAPI.StaticFilteringParser(); - - const getHints = function(cm) { - const cursor = cm.getCursor(); - const line = cm.getLine(cursor.line); - parser.analyze(line); - if ( parser.category !== parser.CATStaticExtFilter ) { - return; + const getNetOptionHint = function(cursor, isNegated, seedLeft, seedRight) { + const assignPos = seedRight.indexOf('='); + if ( assignPos !== -1 ) { seedRight = seedRight.slice(0, assignPos); } + const seed = (seedLeft + seedRight).trim(); + const isException = parser.isException(); + const out = []; + for ( let [ name, bits ] of parser.netOptionTokens ) { + 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 matchLeft = /#\+\js\(([^,]*)$/.exec(line.slice(0, beg)); const matchRight = /^([^,)]*)/.exec(line.slice(beg)); if ( matchLeft === null || matchRight === null ) { return; } const seed = (matchLeft[1] + matchRight[1]).trim(); const out = []; - for ( const [ name, details ] of resourceNames ) { - if ( name.startsWith(seed) === false ) { continue; } - if ( details.hasData !== true ) { continue; } - if ( name.endsWith('.js') === false ) { continue; } - const hint = { text: name.slice(0, -3) }; - if ( details.aliasOf !== '' ) { - hint.displayText = `${hint.text} (${details.aliasOf})`; + for ( const [ text, displayText ] of scriptletNames ) { + if ( text.startsWith(seed) === false ) { continue; } + const hint = { text }; + if ( displayText !== '' ) { + hint.displayText = displayText; } 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); + }); })(); /******************************************************************************/ diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index 6da2e1e87..9e0f30446 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -784,7 +784,11 @@ RedirectEngine.prototype.loadBuiltinResources = function() { RedirectEngine.prototype.getResourceDetails = function() { const out = new Map(); 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 ) { const original = out.get(name); diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js index fa0c2b78b..25aa2b19f 100644 --- a/src/js/static-filtering-parser.js +++ b/src/js/static-filtering-parser.js @@ -1933,6 +1933,69 @@ Parser.prototype.OPTTokenXhr = OPTTokenXhr; Parser.prototype.OPTTokenWebrtc = OPTTokenWebrtc; 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 { @@ -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