diff --git a/src/1p-filters.html b/src/1p-filters.html index c27c21ff97c4f..b5b0d25d922bf 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -6,6 +6,7 @@ uBlock — Your filters + @@ -42,6 +43,8 @@ + + diff --git a/src/asset-viewer.html b/src/asset-viewer.html index 42874e0e2787a..0aca82e511b2c 100644 --- a/src/asset-viewer.html +++ b/src/asset-viewer.html @@ -5,6 +5,7 @@ + @@ -31,6 +32,8 @@ + + diff --git a/src/css/codemirror.css b/src/css/codemirror.css index 337ce9a7dc801..411804f7fb057 100644 --- a/src/css/codemirror.css +++ b/src/css/codemirror.css @@ -52,6 +52,7 @@ div.CodeMirror span.CodeMirror-matchingbracket { color: unset; } .CodeMirror-matchingbracket { + background-color: #afa; color: inherit !important; font-weight: bold; } diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index 5874598d851cf..859fe72336006 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -36,6 +36,8 @@ const cmEditor = new CodeMirror(document.getElementById('userFilters'), { 'Ctrl-Space': 'autocomplete', 'Tab': 'toggleComment', }, + foldGutter: true, + gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ], lineNumbers: true, lineWrapping: true, matchBrackets: true, diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index b2ff291356717..85cff605c9300 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -32,6 +32,8 @@ const cmEditor = new CodeMirror(document.getElementById('content'), { autofocus: true, + foldGutter: true, + gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ], lineNumbers: true, lineWrapping: true, matchBrackets: true, diff --git a/src/js/assets.js b/src/js/assets.js index be85a9e47d206..aac3126e15f33 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -276,7 +276,7 @@ api.fetchFilterList = async function(mainlistURL) { if ( sublistURLs.has(subURL) ) { continue; } sublistURLs.add(subURL); out.push( - slice.slice(lastIndex, match.index), + slice.slice(lastIndex, match.index + match[0].length), `! >>>>>>>> ${subURL}`, api.fetchText(subURL), `! <<<<<<<< ${subURL}` diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index bbccf41a7a068..de9af1febbe92 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -32,7 +32,7 @@ const redirectNames = new Map(); const scriptletNames = new Map(); -const preparseDirectiveTokens = new Set(); +const preparseDirectiveTokens = new Map(); const preparseDirectiveHints = []; /******************************************************************************/ @@ -44,8 +44,8 @@ CodeMirror.defineMode('ubo-static-filtering', function() { if ( StaticFilteringParser instanceof Object === false ) { return; } const parser = new StaticFilteringParser({ interactive: true }); - const rePreparseDirectives = /^!#(?:if|endif|include)\b/; - const rePreparseIfDirective = /^(!#if !?)(.+)$/; + const rePreparseDirectives = /^!#(?:if|endif|include )\b/; + const rePreparseIfDirective = /^(!#if ?)(.*)$/; let parserSlot = 0; let netOptionValueMode = false; @@ -60,17 +60,28 @@ CodeMirror.defineMode('ubo-static-filtering', function() { return 'variable strong'; } if ( stream.pos < match[1].length ) { - stream.pos = match[1].length; + stream.pos += match[1].length; return 'variable strong'; } stream.skipToEnd(); - if ( - preparseDirectiveTokens.size === 0 || - preparseDirectiveTokens.has(match[2].trim()) - ) { - return 'variable strong'; + if ( match[1].endsWith(' ') === false ) { + return 'error strong'; + } + if ( preparseDirectiveTokens.size === 0 ) { + return 'positive strong'; + } + let token = match[2]; + const not = token.startsWith('!'); + if ( not ) { + token = token.slice(1); + } + if ( preparseDirectiveTokens.has(token) === false ) { + return 'error strong'; } - return 'error strong'; + if ( not !== preparseDirectiveTokens.get(token) ) { + return 'positive strong'; + } + return 'negative strong'; }; const colorExtHTMLPatternSpan = function(stream) { @@ -289,8 +300,8 @@ CodeMirror.defineMode('ubo-static-filtering', function() { scriptletNames.set(name.slice(0, -3), displayText); } } - details.preparseDirectiveTokens.forEach(a => { - preparseDirectiveTokens.add(a); + details.preparseDirectiveTokens.forEach(([ a, b ]) => { + preparseDirectiveTokens.set(a, b); }); preparseDirectiveHints.push(...details.preparseDirectiveHints); initHints(); @@ -471,6 +482,63 @@ const initHints = function() { /******************************************************************************/ +CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { + + const foldIfEndif = function(startLineNo, startLine, cm) { + const lastLineNo = cm.lastLine(); + let endLineNo = startLineNo; + let depth = 1; + while ( endLineNo < lastLineNo ) { + endLineNo += 1; + const line = cm.getLine(endLineNo); + if ( line.startsWith('!#endif') ) { + depth -= 1; + if ( depth === 0 ) { + return { + from: CodeMirror.Pos(startLineNo, startLine.length), + to: CodeMirror.Pos(endLineNo, 0) + }; + } + } + if ( line.startsWith('!#if') ) { + depth += 1; + } + } + }; + + const foldInclude = function(startLineNo, startLine, cm) { + const lastLineNo = cm.lastLine(); + let endLineNo = startLineNo + 1; + if ( endLineNo >= lastLineNo ) { return; } + if ( cm.getLine(endLineNo).startsWith('! >>>>>>>> ') === false ) { + return; + } + while ( endLineNo < lastLineNo ) { + endLineNo += 1; + const line = cm.getLine(endLineNo); + if ( line.startsWith('! <<<<<<<< ') ) { + return { + from: CodeMirror.Pos(startLineNo, startLine.length), + to: CodeMirror.Pos(endLineNo, line.length) + }; + } + } + }; + + return function(cm, start) { + const startLineNo = start.line; + const startLine = cm.getLine(startLineNo); + if ( startLine.startsWith('!#if') ) { + return foldIfEndif(startLineNo, startLine, cm); + } + if ( startLine.startsWith('!#include ') ) { + return foldInclude(startLineNo, startLine, cm); + } + }; +})()); + +/******************************************************************************/ + // <<<<< end of local scope } diff --git a/src/js/dashboard-common.js b/src/js/dashboard-common.js index 99c14dc3f5149..1df07a912e619 100644 --- a/src/js/dashboard-common.js +++ b/src/js/dashboard-common.js @@ -149,24 +149,37 @@ self.uBlockDashboard.patchCodeMirrorEditor = (function() { let lastGutterClick = 0; let lastGutterLine = 0; - const onGutterClicked = function(cm, line) { + const onGutterClicked = function(cm, line, gutter) { + if ( gutter !== 'CodeMirror-linenumbers' ) { return; } + grabFocusAsync(cm); const delta = Date.now() - lastGutterClick; + // Single click if ( delta >= 500 || line !== lastGutterLine ) { cm.setSelection( - { line: line, ch: 0 }, + { line, ch: 0 }, { line: line + 1, ch: 0 } ); lastGutterClick = Date.now(); lastGutterLine = line; - } else { - cm.setSelection( - { line: 0, ch: 0 }, - { line: cm.lineCount(), ch: 0 }, - { scroll: false } - ); - lastGutterClick = 0; + return; } - grabFocusAsync(cm); + // Double click: select fold-able block or all + let lineFrom = 0; + let lineTo = cm.lineCount(); + const foldFn = cm.getHelper({ line, ch: 0 }, 'fold'); + if ( foldFn instanceof Function ) { + const range = foldFn(cm, { line, ch: 0 }); + if ( range !== undefined ) { + lineFrom = range.from.line; + lineTo = range.to.line + 1; + } + } + cm.setSelection( + { line: lineFrom, ch: 0 }, + { line: lineTo, ch: 0 }, + { scroll: false } + ); + lastGutterClick = 0; }; return function(cm) { diff --git a/src/js/storage.js b/src/js/storage.js index 7500791947de5..a6a546a4e0526 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -862,6 +862,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { // the content string which should alternatively be parsed and discarded. split: function(content) { const reIf = /^!#(if|endif)\b([^\n]*)(?:[\n\r]+|$)/gm; + const soup = vAPI.webextFlavor.soup; const stack = []; const shouldDiscard = ( ) => stack.some(v => v); const parts = [ 0 ]; @@ -878,10 +879,8 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { if ( target ) { expr = expr.slice(1); } const token = this.tokens.get(expr); const startDiscard = - token === 'false' && - target === false || - token !== undefined && - vAPI.webextFlavor.soup.has(token) === target; + token === 'false' && target === false || + token !== undefined && soup.has(token) === target; if ( discard === false && startDiscard ) { parts.push(match.index); discard = true; @@ -930,7 +929,12 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { }, getTokens: function() { - return Array.from(this.tokens.keys()); + const out = new Map(); + const soup = vAPI.webextFlavor.soup; + for ( const [ key, val ] of this.tokens ) { + out.set(key, val !== 'false' && soup.has(val)); + } + return Array.from(out); }, tokens: new Map([ @@ -947,6 +951,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { // Compatibility with other blockers // https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#adguard-specific [ 'adguard', 'adguard' ], + [ 'adguard_app_windows', 'false' ], [ 'adguard_ext_chromium', 'chromium' ], [ 'adguard_ext_edge', 'edge' ], [ 'adguard_ext_firefox', 'firefox' ], diff --git a/src/lib/codemirror/addon/fold/foldcode.js b/src/lib/codemirror/addon/fold/foldcode.js new file mode 100644 index 0000000000000..e146fb9f3ee94 --- /dev/null +++ b/src/lib/codemirror/addon/fold/foldcode.js @@ -0,0 +1,152 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function doFold(cm, pos, options, force) { + if (options && options.call) { + var finder = options; + options = null; + } else { + var finder = getOption(cm, options, "rangeFinder"); + } + if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); + var minSize = getOption(cm, options, "minFoldSize"); + + function getRange(allowFolded) { + var range = finder(cm, pos); + if (!range || range.to.line - range.from.line < minSize) return null; + var marks = cm.findMarksAt(range.from); + for (var i = 0; i < marks.length; ++i) { + if (marks[i].__isFold && force !== "fold") { + if (!allowFolded) return null; + range.cleared = true; + marks[i].clear(); + } + } + return range; + } + + var range = getRange(true); + if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { + pos = CodeMirror.Pos(pos.line - 1, 0); + range = getRange(false); + } + if (!range || range.cleared || force === "unfold") return; + + var myWidget = makeWidget(cm, options); + CodeMirror.on(myWidget, "mousedown", function(e) { + myRange.clear(); + CodeMirror.e_preventDefault(e); + }); + var myRange = cm.markText(range.from, range.to, { + replacedWith: myWidget, + clearOnEnter: getOption(cm, options, "clearOnEnter"), + __isFold: true + }); + myRange.on("clear", function(from, to) { + CodeMirror.signal(cm, "unfold", cm, from, to); + }); + CodeMirror.signal(cm, "fold", cm, range.from, range.to); + } + + function makeWidget(cm, options) { + var widget = getOption(cm, options, "widget"); + if (typeof widget == "string") { + var text = document.createTextNode(widget); + widget = document.createElement("span"); + widget.appendChild(text); + widget.className = "CodeMirror-foldmarker"; + } else if (widget) { + widget = widget.cloneNode(true) + } + return widget; + } + + // Clumsy backwards-compatible interface + CodeMirror.newFoldFunction = function(rangeFinder, widget) { + return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; + }; + + // New-style interface + CodeMirror.defineExtension("foldCode", function(pos, options, force) { + doFold(this, pos, options, force); + }); + + CodeMirror.defineExtension("isFolded", function(pos) { + var marks = this.findMarksAt(pos); + for (var i = 0; i < marks.length; ++i) + if (marks[i].__isFold) return true; + }); + + CodeMirror.commands.toggleFold = function(cm) { + cm.foldCode(cm.getCursor()); + }; + CodeMirror.commands.fold = function(cm) { + cm.foldCode(cm.getCursor(), null, "fold"); + }; + CodeMirror.commands.unfold = function(cm) { + cm.foldCode(cm.getCursor(), null, "unfold"); + }; + CodeMirror.commands.foldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "fold"); + }); + }; + CodeMirror.commands.unfoldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold"); + }); + }; + + CodeMirror.registerHelper("fold", "combine", function() { + var funcs = Array.prototype.slice.call(arguments, 0); + return function(cm, start) { + for (var i = 0; i < funcs.length; ++i) { + var found = funcs[i](cm, start); + if (found) return found; + } + }; + }); + + CodeMirror.registerHelper("fold", "auto", function(cm, start) { + var helpers = cm.getHelpers(start, "fold"); + for (var i = 0; i < helpers.length; i++) { + var cur = helpers[i](cm, start); + if (cur) return cur; + } + }); + + var defaultOptions = { + rangeFinder: CodeMirror.fold.auto, + widget: "\u2194", + minFoldSize: 0, + scanUp: false, + clearOnEnter: true + }; + + CodeMirror.defineOption("foldOptions", null); + + function getOption(cm, options, name) { + if (options && options[name] !== undefined) + return options[name]; + var editorOptions = cm.options.foldOptions; + if (editorOptions && editorOptions[name] !== undefined) + return editorOptions[name]; + return defaultOptions[name]; + } + + CodeMirror.defineExtension("foldOption", function(options, name) { + return getOption(this, options, name); + }); +}); diff --git a/src/lib/codemirror/addon/fold/foldgutter.css b/src/lib/codemirror/addon/fold/foldgutter.css new file mode 100644 index 0000000000000..ad19ae2d3ee19 --- /dev/null +++ b/src/lib/codemirror/addon/fold/foldgutter.css @@ -0,0 +1,20 @@ +.CodeMirror-foldmarker { + color: blue; + text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; + font-family: arial; + line-height: .3; + cursor: pointer; +} +.CodeMirror-foldgutter { + width: .7em; +} +.CodeMirror-foldgutter-open, +.CodeMirror-foldgutter-folded { + cursor: pointer; +} +.CodeMirror-foldgutter-open:after { + content: "\25BE"; +} +.CodeMirror-foldgutter-folded:after { + content: "\25B8"; +} diff --git a/src/lib/codemirror/addon/fold/foldgutter.js b/src/lib/codemirror/addon/fold/foldgutter.js new file mode 100644 index 0000000000000..988c67c450617 --- /dev/null +++ b/src/lib/codemirror/addon/fold/foldgutter.js @@ -0,0 +1,146 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./foldcode")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./foldcode"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("foldGutter", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.clearGutter(cm.state.foldGutter.options.gutter); + cm.state.foldGutter = null; + cm.off("gutterClick", onGutterClick); + cm.off("change", onChange); + cm.off("viewportChange", onViewportChange); + cm.off("fold", onFold); + cm.off("unfold", onFold); + cm.off("swapDoc", onChange); + } + if (val) { + cm.state.foldGutter = new State(parseOptions(val)); + updateInViewport(cm); + cm.on("gutterClick", onGutterClick); + cm.on("change", onChange); + cm.on("viewportChange", onViewportChange); + cm.on("fold", onFold); + cm.on("unfold", onFold); + cm.on("swapDoc", onChange); + } + }); + + var Pos = CodeMirror.Pos; + + function State(options) { + this.options = options; + this.from = this.to = 0; + } + + function parseOptions(opts) { + if (opts === true) opts = {}; + if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter"; + if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open"; + if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded"; + return opts; + } + + function isFolded(cm, line) { + var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); + for (var i = 0; i < marks.length; ++i) + if (marks[i].__isFold && marks[i].find().from.line == line) return marks[i]; + } + + function marker(spec) { + if (typeof spec == "string") { + var elt = document.createElement("div"); + elt.className = spec + " CodeMirror-guttermarker-subtle"; + return elt; + } else { + return spec.cloneNode(true); + } + } + + function updateFoldInfo(cm, from, to) { + var opts = cm.state.foldGutter.options, cur = from; + var minSize = cm.foldOption(opts, "minFoldSize"); + var func = cm.foldOption(opts, "rangeFinder"); + cm.eachLine(from, to, function(line) { + var mark = null; + if (isFolded(cm, cur)) { + mark = marker(opts.indicatorFolded); + } else { + var pos = Pos(cur, 0); + var range = func && func(cm, pos); + if (range && range.to.line - range.from.line >= minSize) + mark = marker(opts.indicatorOpen); + } + cm.setGutterMarker(line, opts.gutter, mark); + ++cur; + }); + } + + function updateInViewport(cm) { + var vp = cm.getViewport(), state = cm.state.foldGutter; + if (!state) return; + cm.operation(function() { + updateFoldInfo(cm, vp.from, vp.to); + }); + state.from = vp.from; state.to = vp.to; + } + + function onGutterClick(cm, line, gutter) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + if (gutter != opts.gutter) return; + var folded = isFolded(cm, line); + if (folded) folded.clear(); + else cm.foldCode(Pos(line, 0), opts.rangeFinder); + } + + function onChange(cm) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + state.from = state.to = 0; + clearTimeout(state.changeUpdate); + state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600); + } + + function onViewportChange(cm) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + clearTimeout(state.changeUpdate); + state.changeUpdate = setTimeout(function() { + var vp = cm.getViewport(); + if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { + updateInViewport(cm); + } else { + cm.operation(function() { + if (vp.from < state.from) { + updateFoldInfo(cm, vp.from, state.from); + state.from = vp.from; + } + if (vp.to > state.to) { + updateFoldInfo(cm, state.to, vp.to); + state.to = vp.to; + } + }); + } + }, opts.updateViewportTimeSpan || 400); + } + + function onFold(cm, from) { + var state = cm.state.foldGutter; + if (!state) return; + var line = from.line; + if (line >= state.from && line < state.to) + updateFoldInfo(cm, line, line + 1); + } +});