1
0
mirror of https://github.com/gorhill/uBlock.git synced 2025-02-01 04:31:36 +01:00

Use a CodeMirror editor instance in element picker

This allows to bring in all the benefits of
syntax highlighting and enhanced editing
features in the element picker, like auto-
completion, etc.

This is also a necessary step to possibly solve
the following issue:

- https://github.com/gorhill/uBlock/issues/2035

Additionally, incrementally improved the behavior
of uBO's custom CodeMirror static filtering syntax
mode when double-clicking somewhere in a static
extended filter:

- on a class/id string will cause the whole
  class/id string to be   selected, including the
  prepending `.`/`#`.

- somewhere in a hostname/entity will cause all
  the labels from the cursor position to the
  right-most label to be selected (subject to
  change/fine-tune as per feedback of filter
  list maintainers).

Related feedback:
- https://github.com/uBlockOrigin/uBlock-issues/issues/1134#issuecomment-679421316
This commit is contained in:
Raymond Hill 2020-10-14 10:21:30 -04:00
parent 9994033629
commit a095b83250
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
6 changed files with 126 additions and 58 deletions

View File

@ -29,11 +29,6 @@ html#ublock0-epicker,
#ublock0-epicker.paused:not(.zap) aside { #ublock0-epicker.paused:not(.zap) aside {
display: block; display: block;
} }
#ublock0-epicker ul,
#ublock0-epicker li,
#ublock0-epicker div {
display: block;
}
#ublock0-epicker #toolbar { #ublock0-epicker #toolbar {
cursor: grab; cursor: grab;
display: flex; display: flex;
@ -79,36 +74,31 @@ html#ublock0-epicker,
width: 100%; width: 100%;
} }
#ublock0-epicker section > div:first-child { #ublock0-epicker section > div:first-child {
border: 1px solid #aaa; border: 1px solid var(--default-surface-border);
margin: 0; margin: 0;
position: relative; position: relative;
} }
#ublock0-epicker section.invalidFilter > div:first-child { #ublock0-epicker section.invalidFilter > div:first-child {
border-color: red; border-color: red;
} }
#ublock0-epicker section textarea { #ublock0-epicker section .codeMirrorContainer {
background-color: var(--default-surface);
border: none; border: none;
box-sizing: border-box; box-sizing: border-box;
color: var(--default-ink);
font: 11px monospace; font: 11px monospace;
height: 8em; height: 8em;
margin: 0; padding: 2px;
overflow: hidden;
overflow-y: auto;
padding: 2px 2px 1.2em 2px;
resize: none;
width: 100%; width: 100%;
word-break: break-all; }
} .CodeMirror-lines,
#ublock0-epicker section textarea + div { .CodeMirror pre {
background-color: transparent; padding: 0;
bottom: 0; }
.CodeMirror-vscrollbar {
z-index: 0;
}
#ublock0-epicker section .resultsetWidgets {
display: flex; display: flex;
left: 0;
pointer-events: none;
position: absolute;
right: 0;
} }
#resultsetModifiers { #resultsetModifiers {
align-items: flex-end; align-items: flex-end;
@ -196,7 +186,7 @@ html#ublock0-epicker,
overflow: hidden; overflow: hidden;
} }
#ublock0-epicker #candidateFilters { #ublock0-epicker #candidateFilters {
max-height: 16em; max-height: 14em;
overflow-y: auto; overflow-y: auto;
} }
#ublock0-epicker #candidateFilters > li:first-of-type { #ublock0-epicker #candidateFilters > li:first-of-type {

View File

@ -42,7 +42,9 @@ const cmEditor = new CodeMirror(document.getElementById('userFilters'), {
lineWrapping: true, lineWrapping: true,
matchBrackets: true, matchBrackets: true,
maxScanLines: 1, maxScanLines: 1,
styleActiveLine: true, styleActiveLine: {
nonEmpty: true,
},
}); });
uBlockDashboard.patchCodeMirrorEditor(cmEditor); uBlockDashboard.patchCodeMirrorEditor(cmEditor);

View File

@ -53,7 +53,9 @@
matchBrackets: true, matchBrackets: true,
maxScanLines: 1, maxScanLines: 1,
readOnly: true, readOnly: true,
styleActiveLine: true, styleActiveLine: {
nonEmpty: true,
},
}); });
uBlockDashboard.patchCodeMirrorEditor(cmEditor); uBlockDashboard.patchCodeMirrorEditor(cmEditor);

View File

@ -318,10 +318,12 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
return 'comment'; return 'comment';
} }
if ( parser.category === parser.CATStaticExtFilter ) { if ( parser.category === parser.CATStaticExtFilter ) {
return colorExtSpan(stream); const style = colorExtSpan(stream);
return style ? `ext ${style}` : 'ext';
} }
if ( parser.category === parser.CATStaticNetFilter ) { if ( parser.category === parser.CATStaticNetFilter ) {
return colorNetSpan(stream); const style = colorNetSpan(stream);
return style ? `net ${style}` : 'net';
} }
stream.skipToEnd(); stream.skipToEnd();
return null; return null;
@ -330,13 +332,14 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
return { return {
lineComment: '!', lineComment: '!',
token: function(stream) { token: function(stream) {
let style = '';
if ( stream.sol() ) { if ( stream.sol() ) {
parser.analyze(stream.string); parser.analyze(stream.string);
parser.analyzeExtra(); parser.analyzeExtra();
parserSlot = 0; parserSlot = 0;
netOptionValueMode = false; netOptionValueMode = false;
} }
let style = colorSpan(stream) || ''; style += colorSpan(stream) || '';
if ( (parser.flavorBits & parser.BITFlavorError) !== 0 ) { if ( (parser.flavorBits & parser.BITFlavorError) !== 0 ) {
style += ' line-background-error'; style += ' line-background-error';
} }
@ -615,24 +618,49 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => {
const s = cm.getLine(line); const s = cm.getLine(line);
const token = cm.getTokenTypeAt(pos); const token = cm.getTokenTypeAt(pos);
let lmatch, rmatch; let beg, end;
let select = false;
// Select URL in comments // Select URL in comments
if ( token === 'comment link' ) { if ( /\bcomment\b/.test(token) && /\blink\b/.test(token) ) {
lmatch = /\S+$/.exec(s.slice(0, ch)); const l = /\S+$/.exec(s.slice(0, ch));
rmatch = /^\S+/.exec(s.slice(ch)); if ( l && /^https?:\/\//.test(s.slice(l.index)) ) {
select = lmatch !== null && rmatch !== null && const r = /^\S+/.exec(s.slice(ch));
/^https?:\/\//.test(s.slice(lmatch.index)); 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 // TODO: add more convenient word-matching cases here
// if ( select === false ) { ... }
if ( select === false ) { return Pass; } if ( beg === undefined ) { return Pass; }
cm.setSelection( cm.setSelection(
{ line, ch: lmatch.index }, { line, ch: beg },
{ line, ch: ch + rmatch.index + rmatch[0].length } { line, ch: end }
); );
}; };

View File

@ -19,6 +19,8 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
/* global CodeMirror */
'use strict'; 'use strict';
/******************************************************************************/ /******************************************************************************/
@ -36,7 +38,6 @@ const $storAll = selector => document.querySelectorAll(selector);
const pickerRoot = document.documentElement; const pickerRoot = document.documentElement;
const dialog = $stor('aside'); const dialog = $stor('aside');
const taCandidate = $stor('textarea');
let staticFilteringParser; let staticFilteringParser;
const svgRoot = $stor('svg'); 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 filterFromTextarea = function() {
const s = taCandidate.value.trim(); const filter = rawFilterFromTextarea();
if ( s === '' ) { return ''; } if ( filter === '' ) { return ''; }
const pos = s.indexOf('\n');
const filter = pos === -1 ? s.trim() : s.slice(0, pos).trim();
const sfp = staticFilteringParser; const sfp = staticFilteringParser;
sfp.analyze(filter); sfp.analyze(filter);
sfp.analyzeExtra(); sfp.analyzeExtra();
@ -256,7 +286,8 @@ const candidateFromFilterChoice = function(filterChoice) {
const onCandidateOptimized = function(details) { const onCandidateOptimized = function(details) {
$id('resultsetModifiers').classList.remove('hide'); $id('resultsetModifiers').classList.remove('hide');
computedCandidate = details.filter; computedCandidate = details.filter;
taCandidate.value = computedCandidate; cmEditor.setValue(computedCandidate);
cmEditor.clearHistory();
onCandidateChanged(); onCandidateChanged();
}; };
@ -393,9 +424,9 @@ const onCandidateChanged = function() {
$id('resultsetCount').textContent = 'E'; $id('resultsetCount').textContent = 'E';
$id('create').setAttribute('disabled', ''); $id('create').setAttribute('disabled', '');
} }
const text = rawFilterFromTextarea();
$id('resultsetModifiers').classList.toggle( $id('resultsetModifiers').classList.toggle(
'hide', 'hide', text === '' || text !== computedCandidate
taCandidate.value === '' || taCandidate.value !== computedCandidate
); );
vAPI.MessagingConnection.sendTo(epickerConnectionId, { vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'dialogSetFilter', what: 'dialogSetFilter',
@ -462,20 +493,22 @@ const onDepthChanged = function() {
slot: max - value, slot: max - value,
}); });
if ( text === undefined ) { return; } if ( text === undefined ) { return; }
taCandidate.value = text; cmEditor.setValue(text);
cmEditor.clearHistory();
onCandidateChanged(); onCandidateChanged();
}; };
/******************************************************************************/ /******************************************************************************/
const onSpecificityChanged = function() { const onSpecificityChanged = function() {
if ( taCandidate.value !== computedCandidate ) { return; } if ( rawFilterFromTextarea() !== computedCandidate ) { return; }
const text = candidateFromFilterChoice({ const text = candidateFromFilterChoice({
filters: cosmeticFilterCandidates, filters: cosmeticFilterCandidates,
slot: computedCandidateSlot, slot: computedCandidateSlot,
}); });
if ( text === undefined ) { return; } if ( text === undefined ) { return; }
taCandidate.value = text; cmEditor.setValue(text);
cmEditor.clearHistory();
onCandidateChanged(); onCandidateChanged();
}; };
@ -496,7 +529,8 @@ const onCandidateClicked = function(ev) {
} }
const text = candidateFromFilterChoice(choice); const text = candidateFromFilterChoice(choice);
if ( text === undefined ) { return; } if ( text === undefined ) { return; }
taCandidate.value = text; cmEditor.setValue(text);
cmEditor.clearHistory();
onCandidateChanged(); onCandidateChanged();
}; };
@ -703,7 +737,7 @@ const showDialog = function(details) {
// This is an issue which surfaced when the element picker code was // This is an issue which surfaced when the element picker code was
// revisited to isolate the picker dialog DOM from the page DOM. // revisited to isolate the picker dialog DOM from the page DOM.
if ( typeof filter !== 'object' || filter === null ) { if ( typeof filter !== 'object' || filter === null ) {
taCandidate.value = ''; cmEditor.setValue('');
return; return;
} }
@ -714,7 +748,7 @@ const showDialog = function(details) {
const text = candidateFromFilterChoice(filterChoice); const text = candidateFromFilterChoice(filterChoice);
if ( text === undefined ) { return; } if ( text === undefined ) { return; }
taCandidate.value = text; cmEditor.setValue(text);
onCandidateChanged(); onCandidateChanged();
}; };
@ -749,7 +783,8 @@ const startPicker = function() {
if ( pickerRoot.classList.contains('zap') ) { return; } if ( pickerRoot.classList.contains('zap') ) { return; }
taCandidate.addEventListener('input', onCandidateChanged); cmEditor.on('changes', onCandidateChanged);
$id('preview').addEventListener('click', onPreviewClicked); $id('preview').addEventListener('click', onPreviewClicked);
$id('create').addEventListener('click', onCreateClicked); $id('create').addEventListener('click', onCreateClicked);
$id('pick').addEventListener('click', onPickClicked); $id('pick').addEventListener('click', onPickClicked);

View File

@ -4,16 +4,20 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>uBlock Origin Element Picker</title> <title>uBlock Origin Element Picker</title>
<link rel="stylesheet" href="../lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="../lib/codemirror/addon/hint/show-hint.css">
<link rel="stylesheet" href="../css/themes/default.css"> <link rel="stylesheet" href="../css/themes/default.css">
<link rel="stylesheet" href="../css/epicker-ui.css"> <link rel="stylesheet" href="../css/epicker-ui.css">
<link rel="stylesheet" href="../css/codemirror.css">
</head> </head>
<body> <body>
<aside> <aside>
<section> <section>
<div> <div>
<textarea lang="en" dir="ltr" spellcheck="false"></textarea> <div class="codeMirrorContainer codeMirrorBreakAll"></div>
<div> <div class="resultsetWidgets">
<span id="resultsetModifiers"> <span id="resultsetModifiers">
<span id="resultsetDepth" class="resultsetModifier"> <span id="resultsetDepth" class="resultsetModifier">
<span><span></span><span></span><span></span></span> <span><span></span><span></span><span></span></span>
@ -51,13 +55,20 @@
</aside> </aside>
<svg><path d></path><path d></path></svg> <svg><path d></path><path d></path></svg>
<script src="../lib/codemirror/lib/codemirror.js"></script>
<script src="../lib/codemirror/addon/edit/closebrackets.js"></script>
<script src="../lib/codemirror/addon/edit/matchbrackets.js"></script>
<script src="../lib/codemirror/addon/hint/show-hint.js"></script>
<script src="../js/codemirror/ubo-static-filtering.js"></script>
<script src="../js/vapi.js"></script> <script src="../js/vapi.js"></script>
<script src="../js/vapi-common.js"></script> <script src="../js/vapi-common.js"></script>
<script src="../js/vapi-client.js"></script> <script src="../js/vapi-client.js"></script>
<script src="../js/vapi-client-extra.js"></script> <script src="../js/vapi-client-extra.js"></script>
<script src="../js/i18n.js"></script> <script src="../js/i18n.js"></script>
<script src="../js/epicker-ui.js"></script>
<script src="../js/static-filtering-parser.js"></script> <script src="../js/static-filtering-parser.js"></script>
<script src="../js/epicker-ui.js"></script>
</body> </body>
</html> </html>