1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-11-02 00:42:45 +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 {
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 {

View File

@ -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);

View File

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

View File

@ -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 }
);
};

View File

@ -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);

View File

@ -4,16 +4,20 @@
<head>
<meta charset="utf-8">
<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/epicker-ui.css">
<link rel="stylesheet" href="../css/codemirror.css">
</head>
<body>
<aside>
<section>
<div>
<textarea lang="en" dir="ltr" spellcheck="false"></textarea>
<div>
<div class="codeMirrorContainer codeMirrorBreakAll"></div>
<div class="resultsetWidgets">
<span id="resultsetModifiers">
<span id="resultsetDepth" class="resultsetModifier">
<span><span></span><span></span><span></span></span>
@ -51,13 +55,20 @@
</aside>
<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-common.js"></script>
<script src="../js/vapi-client.js"></script>
<script src="../js/vapi-client-extra.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/epicker-ui.js"></script>
</body>
</html>