Skip to content

Commit

Permalink
Use a CodeMirror editor instance in element picker
Browse files Browse the repository at this point in the history
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:

- #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:
- uBlockOrigin/uBlock-issues#1134 (comment)
  • Loading branch information
gorhill committed Oct 14, 2020
1 parent 9994033 commit a095b83
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 58 deletions.
38 changes: 14 additions & 24 deletions src/css/epicker-ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion src/js/1p-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion src/js/asset-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
matchBrackets: true,
maxScanLines: 1,
readOnly: true,
styleActiveLine: true,
styleActiveLine: {
nonEmpty: true,
},
});

uBlockDashboard.patchCodeMirrorEditor(cmEditor);
Expand Down
56 changes: 42 additions & 14 deletions src/js/codemirror/ubo-static-filtering.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
}
Expand Down Expand Up @@ -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 }
);
};

Expand Down
65 changes: 50 additions & 15 deletions src/js/epicker-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
Home: https://github.com/gorhill/uBlock
*/

/* global CodeMirror */

'use strict';

/******************************************************************************/
Expand All @@ -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');
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
};

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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();
};

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

Expand Down Expand Up @@ -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;
}

Expand All @@ -714,7 +748,7 @@ const showDialog = function(details) {

const text = candidateFromFilterChoice(filterChoice);
if ( text === undefined ) { return; }
taCandidate.value = text;
cmEditor.setValue(text);
onCandidateChanged();
};

Expand Down Expand Up @@ -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);
Expand Down
17 changes: 14 additions & 3 deletions src/web_accessible_resources/epicker-ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>

0 comments on commit a095b83

Please sign in to comment.