diff --git a/src/css/epicker-ui.css b/src/css/epicker-ui.css index e45b935dd..8cb75747c 100644 --- a/src/css/epicker-ui.css +++ b/src/css/epicker-ui.css @@ -29,11 +29,6 @@ html#ublock0-epicker, #ublock0-epicker.paused:not(.zap) aside { display: block; } -#ublock0-epicker ul, -#ublock0-epicker li, -#ublock0-epicker div { - display: block; -} #ublock0-epicker #toolbar { cursor: grab; display: flex; @@ -79,36 +74,31 @@ html#ublock0-epicker, width: 100%; } #ublock0-epicker section > div:first-child { - border: 1px solid #aaa; + border: 1px solid var(--default-surface-border); margin: 0; position: relative; } #ublock0-epicker section.invalidFilter > div:first-child { border-color: red; } -#ublock0-epicker section textarea { - background-color: var(--default-surface); +#ublock0-epicker section .codeMirrorContainer { border: none; box-sizing: border-box; - color: var(--default-ink); font: 11px monospace; height: 8em; - margin: 0; - overflow: hidden; - overflow-y: auto; - padding: 2px 2px 1.2em 2px; - resize: none; + padding: 2px; width: 100%; - word-break: break-all; -} -#ublock0-epicker section textarea + div { - background-color: transparent; - bottom: 0; + } +.CodeMirror-lines, +.CodeMirror pre { + padding: 0; + } +.CodeMirror-vscrollbar { + z-index: 0; + } + +#ublock0-epicker section .resultsetWidgets { display: flex; - left: 0; - pointer-events: none; - position: absolute; - right: 0; } #resultsetModifiers { align-items: flex-end; @@ -196,7 +186,7 @@ html#ublock0-epicker, overflow: hidden; } #ublock0-epicker #candidateFilters { - max-height: 16em; + max-height: 14em; overflow-y: auto; } #ublock0-epicker #candidateFilters > li:first-of-type { diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index 859fe7233..b99faf1c7 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -42,7 +42,9 @@ const cmEditor = new CodeMirror(document.getElementById('userFilters'), { lineWrapping: true, matchBrackets: true, maxScanLines: 1, - styleActiveLine: true, + styleActiveLine: { + nonEmpty: true, + }, }); uBlockDashboard.patchCodeMirrorEditor(cmEditor); diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js index 1e03771c7..ecdef01c2 100644 --- a/src/js/asset-viewer.js +++ b/src/js/asset-viewer.js @@ -53,7 +53,9 @@ matchBrackets: true, maxScanLines: 1, readOnly: true, - styleActiveLine: true, + styleActiveLine: { + nonEmpty: true, + }, }); uBlockDashboard.patchCodeMirrorEditor(cmEditor); diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js index 1c268647a..5821af7c4 100644 --- a/src/js/codemirror/ubo-static-filtering.js +++ b/src/js/codemirror/ubo-static-filtering.js @@ -318,10 +318,12 @@ CodeMirror.defineMode('ubo-static-filtering', function() { return 'comment'; } if ( parser.category === parser.CATStaticExtFilter ) { - return colorExtSpan(stream); + const style = colorExtSpan(stream); + return style ? `ext ${style}` : 'ext'; } if ( parser.category === parser.CATStaticNetFilter ) { - return colorNetSpan(stream); + const style = colorNetSpan(stream); + return style ? `net ${style}` : 'net'; } stream.skipToEnd(); return null; @@ -330,13 +332,14 @@ CodeMirror.defineMode('ubo-static-filtering', function() { return { lineComment: '!', token: function(stream) { + let style = ''; if ( stream.sol() ) { parser.analyze(stream.string); parser.analyzeExtra(); parserSlot = 0; netOptionValueMode = false; } - let style = colorSpan(stream) || ''; + style += colorSpan(stream) || ''; if ( (parser.flavorBits & parser.BITFlavorError) !== 0 ) { style += ' line-background-error'; } @@ -615,24 +618,49 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => { const s = cm.getLine(line); const token = cm.getTokenTypeAt(pos); - let lmatch, rmatch; - let select = false; + let beg, end; // Select URL in comments - if ( token === 'comment link' ) { - lmatch = /\S+$/.exec(s.slice(0, ch)); - rmatch = /^\S+/.exec(s.slice(ch)); - select = lmatch !== null && rmatch !== null && - /^https?:\/\//.test(s.slice(lmatch.index)); + if ( /\bcomment\b/.test(token) && /\blink\b/.test(token) ) { + const l = /\S+$/.exec(s.slice(0, ch)); + if ( l && /^https?:\/\//.test(s.slice(l.index)) ) { + const r = /^\S+/.exec(s.slice(ch)); + if ( r ) { + beg = l.index; + end = ch + r[0].length; + } + } + } + + // Better word selection for cosmetic filters + if ( /\bext\b/.test(token) ) { + if ( /\bvalue\b/.test(token) ) { + const l = /[^,.]*$/i.exec(s.slice(0, ch)); + const r = /^[^#,]*/i.exec(s.slice(ch)); + if ( l && r ) { + beg = l.index; + end = ch + r[0].length; + } + } + if ( /\bvariable\b/.test(token) ) { + const l = /[#.]?[a-z0-9_-]+$/i.exec(s.slice(0, ch)); + const r = /^[a-z0-9_-]+/i.exec(s.slice(ch)); + if ( l && r ) { + beg = l.index; + end = ch + r[0].length; + if ( /\bdef\b/.test(cm.getTokenTypeAt({ line, ch: beg + 1 })) ) { + beg += 1; + } + } + } } // TODO: add more convenient word-matching cases here - // if ( select === false ) { ... } - if ( select === false ) { return Pass; } + if ( beg === undefined ) { return Pass; } cm.setSelection( - { line, ch: lmatch.index }, - { line, ch: ch + rmatch.index + rmatch[0].length } + { line, ch: beg }, + { line, ch: end } ); }; diff --git a/src/js/epicker-ui.js b/src/js/epicker-ui.js index 15eefafe0..bc81e9e72 100644 --- a/src/js/epicker-ui.js +++ b/src/js/epicker-ui.js @@ -19,6 +19,8 @@ Home: https://github.com/gorhill/uBlock */ +/* global CodeMirror */ + 'use strict'; /******************************************************************************/ @@ -36,7 +38,6 @@ const $storAll = selector => document.querySelectorAll(selector); const pickerRoot = document.documentElement; const dialog = $stor('aside'); -const taCandidate = $stor('textarea'); let staticFilteringParser; const svgRoot = $stor('svg'); @@ -66,11 +67,40 @@ let needBody = false; /******************************************************************************/ +const cmEditor = new CodeMirror(document.querySelector('.codeMirrorContainer'), { + autoCloseBrackets: true, + autofocus: true, + extraKeys: { + 'Ctrl-Space': 'autocomplete', + }, + lineWrapping: true, + matchBrackets: true, + maxScanLines: 1, +}); + +vAPI.messaging.send('dashboard', { + what: 'getAutoCompleteDetails' +}).then(response => { + if ( response instanceof Object === false ) { return; } + const mode = cmEditor.getMode(); + if ( mode.setHints instanceof Function ) { + mode.setHints(response); + } +}); + +/******************************************************************************/ + +const rawFilterFromTextarea = function() { + const text = cmEditor.getValue(); + const pos = text.indexOf('\n'); + return pos === -1 ? text : text.slice(0, pos); +}; + +/******************************************************************************/ + const filterFromTextarea = function() { - const s = taCandidate.value.trim(); - if ( s === '' ) { return ''; } - const pos = s.indexOf('\n'); - const filter = pos === -1 ? s.trim() : s.slice(0, pos).trim(); + const filter = rawFilterFromTextarea(); + if ( filter === '' ) { return ''; } const sfp = staticFilteringParser; sfp.analyze(filter); sfp.analyzeExtra(); @@ -256,7 +286,8 @@ const candidateFromFilterChoice = function(filterChoice) { const onCandidateOptimized = function(details) { $id('resultsetModifiers').classList.remove('hide'); computedCandidate = details.filter; - taCandidate.value = computedCandidate; + cmEditor.setValue(computedCandidate); + cmEditor.clearHistory(); onCandidateChanged(); }; @@ -393,9 +424,9 @@ const onCandidateChanged = function() { $id('resultsetCount').textContent = 'E'; $id('create').setAttribute('disabled', ''); } + const text = rawFilterFromTextarea(); $id('resultsetModifiers').classList.toggle( - 'hide', - taCandidate.value === '' || taCandidate.value !== computedCandidate + 'hide', text === '' || text !== computedCandidate ); vAPI.MessagingConnection.sendTo(epickerConnectionId, { what: 'dialogSetFilter', @@ -462,20 +493,22 @@ const onDepthChanged = function() { slot: max - value, }); if ( text === undefined ) { return; } - taCandidate.value = text; + cmEditor.setValue(text); + cmEditor.clearHistory(); onCandidateChanged(); }; /******************************************************************************/ const onSpecificityChanged = function() { - if ( taCandidate.value !== computedCandidate ) { return; } + if ( rawFilterFromTextarea() !== computedCandidate ) { return; } const text = candidateFromFilterChoice({ filters: cosmeticFilterCandidates, slot: computedCandidateSlot, }); if ( text === undefined ) { return; } - taCandidate.value = text; + cmEditor.setValue(text); + cmEditor.clearHistory(); onCandidateChanged(); }; @@ -496,7 +529,8 @@ const onCandidateClicked = function(ev) { } const text = candidateFromFilterChoice(choice); if ( text === undefined ) { return; } - taCandidate.value = text; + cmEditor.setValue(text); + cmEditor.clearHistory(); onCandidateChanged(); }; @@ -703,7 +737,7 @@ const showDialog = function(details) { // This is an issue which surfaced when the element picker code was // revisited to isolate the picker dialog DOM from the page DOM. if ( typeof filter !== 'object' || filter === null ) { - taCandidate.value = ''; + cmEditor.setValue(''); return; } @@ -714,7 +748,7 @@ const showDialog = function(details) { const text = candidateFromFilterChoice(filterChoice); if ( text === undefined ) { return; } - taCandidate.value = text; + cmEditor.setValue(text); onCandidateChanged(); }; @@ -749,7 +783,8 @@ const startPicker = function() { if ( pickerRoot.classList.contains('zap') ) { return; } - taCandidate.addEventListener('input', onCandidateChanged); + cmEditor.on('changes', onCandidateChanged); + $id('preview').addEventListener('click', onPreviewClicked); $id('create').addEventListener('click', onCreateClicked); $id('pick').addEventListener('click', onPickClicked); diff --git a/src/web_accessible_resources/epicker-ui.html b/src/web_accessible_resources/epicker-ui.html index 7f72300a9..24ee86595 100644 --- a/src/web_accessible_resources/epicker-ui.html +++ b/src/web_accessible_resources/epicker-ui.html @@ -4,16 +4,20 @@ uBlock Origin Element Picker + + + + + + + + + + + - +