diff --git a/src/1p-filters.html b/src/1p-filters.html
index f3479e41f..7d5cb9664 100644
--- a/src/1p-filters.html
+++ b/src/1p-filters.html
@@ -52,10 +52,10 @@
-
+
-
+
diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 7c6d804b5..d4e5b5801 100644
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -1265,6 +1265,10 @@
"message": "Click to load",
"description": "Message used in frame placeholders"
},
+ "linterMainReport": {
+ "message": "Errors: {{count}}",
+ "description": "Summary of number of errors as reported by the linter "
+ },
"dummy": {
"message": "This entry must be the last one",
"description": "so we dont need to deal with comma for last entry"
diff --git a/src/asset-viewer.html b/src/asset-viewer.html
index d2ff422fd..70858725a 100644
--- a/src/asset-viewer.html
+++ b/src/asset-viewer.html
@@ -35,10 +35,10 @@
-
+
-
+
diff --git a/src/code-viewer.html b/src/code-viewer.html
index 36805b328..3f4896df4 100644
--- a/src/code-viewer.html
+++ b/src/code-viewer.html
@@ -36,10 +36,10 @@
-
+
-
+
diff --git a/src/css/codemirror.css b/src/css/codemirror.css
index ed6c51254..c3fecb067 100644
--- a/src/css/codemirror.css
+++ b/src/css/codemirror.css
@@ -164,6 +164,13 @@
-webkit-user-select: none;
z-index: 1000;
}
+.cm-search-widget > * {
+ flex-grow: 1;
+ }
+.cm-search-widget > :last-child {
+ justify-content: flex-end;
+ }
+
.cm-search-widget-input {
display: inline-flex;
flex-grow: 1;
@@ -196,8 +203,21 @@
color: #000;
}
.cm-search-widget .sourceURL[href=""] {
- display: none;
+ visibility: hidden;
}
+
+.cm-linter-widget {
+ display: none;
+ flex-grow: 1;
+ }
+.cm-linter-widget.hasErrors {
+ display: initial;
+ }
+.cm-linter-widget .cm-linter-widget-count {
+ color: var(--accent-surface-1);
+ fill: var(--accent-surface-1);
+ }
+
.cm-searching.cm-overlay {
background-color: var(--cm-searching-surface);
border: 0;
@@ -247,3 +267,27 @@
.CodeMirror-activeline-background {
background-color: var(--cm-active-line);
}
+
+.CodeMirror-lintmarker {
+ background-color: var(--sf-error-ink);
+ height: calc(var(--font-size) - 2px);
+ margin-top: 1px;
+ position: relative;
+ }
+.CodeMirror-lintmarker > span {
+ display: none;
+ }
+.CodeMirror-lintmarker > span {
+ background-color: var(--surface-0);
+ border: 1px solid var(--sf-error-ink);
+ color: var(--ink-1);
+ left: 100%;
+ padding: var(--default-gap-xsmall);
+ position: absolute;
+ top: -2px;
+ white-space: pre;
+ }
+.CodeMirror-lintmarker:hover > span,
+.CodeMirror-lintmarker > span:hover {
+ display: initial;
+ }
diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js
index 097ab2d75..c33acfd32 100644
--- a/src/js/1p-filters.js
+++ b/src/js/1p-filters.js
@@ -37,7 +37,11 @@ const cmEditor = new CodeMirror(qs$('#userFilters'), {
'Tab': 'toggleComment',
},
foldGutter: true,
- gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ],
+ gutters: [
+ 'CodeMirror-foldgutter',
+ 'CodeMirror-linenumbers',
+ { className: 'CodeMirror-lintgutter', style: 'width: 10px' },
+ ],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js
index 5331aad74..20f1cff66 100644
--- a/src/js/asset-viewer.js
+++ b/src/js/asset-viewer.js
@@ -52,7 +52,11 @@ import './codemirror/ubo-static-filtering.js';
const cmEditor = new CodeMirror(qs$('#content'), {
autofocus: true,
foldGutter: true,
- gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ],
+ gutters: [
+ 'CodeMirror-foldgutter',
+ 'CodeMirror-linenumbers',
+ { className: 'CodeMirror-lintgutter', style: 'width: 10px' },
+ ],
lineNumbers: true,
lineWrapping: true,
matchBrackets: true,
diff --git a/src/js/codemirror/search.js b/src/js/codemirror/search.js
index f05f6d2fc..595345bfc 100644
--- a/src/js/codemirror/search.js
+++ b/src/js/codemirror/search.js
@@ -27,6 +27,9 @@
'use strict';
+import { dom } from '../dom.js';
+import { i18n$ } from '../i18n.js';
+
{
const CodeMirror = self.CodeMirror;
@@ -101,6 +104,10 @@
findNext(cm, -1);
} else if ( tcl.contains('cm-search-widget-down') ) {
findNext(cm, 1);
+ } else if ( tcl.contains('cm-linter-widget-up') ) {
+ findNextError(cm, -1);
+ } else if ( tcl.contains('cm-linter-widget-down') ) {
+ findNextError(cm, 1);
}
if ( ev.target.localName !== 'input' ) {
ev.preventDefault();
@@ -137,6 +144,7 @@
this.queryTimer = null;
this.dirty = true;
this.lines = [];
+ this.errorLines = [];
cm.on('changes', (cm, changes) => {
for ( const change of changes ) {
if ( change.text.length !== 0 || change.removed !== 0 ) {
@@ -365,6 +373,26 @@
});
};
+ const findNextError = function(cm, dir) {
+ const state = getSearchState(cm);
+ const lines = state.errorLines;
+ if ( lines.length === 0 ) { return; }
+ const cursor = cm.getCursor('from');
+ const start = cursor.line;
+ const next = lines.reduce((best, v) => {
+ if ( dir < 0 ) {
+ if ( v < start && (best === -1 || v > best) ) { return v; }
+ return best;
+ }
+ if ( v > start && (best === -1 || v < best) ) { return v; }
+ return best;
+ }, -1);
+ if ( next === -1 || next === start ) { return; }
+ cm.getDoc().setCursor(next);
+ const { clientHeight } = cm.getScrollInfo();
+ cm.scrollIntoView({ line: next, ch: 0 }, clientHeight >>> 1);
+ };
+
const clearSearch = function(cm, hard) {
cm.operation(function() {
const state = getSearchState(cm);
@@ -444,6 +472,11 @@
'angle-up ' +
'' +
'' +
+ '' +
+ ' ' +
+ 'angle-up ' +
+ 'angle-up ' +
+ '' +
'external-link' +
'' +
'';
@@ -459,5 +492,14 @@
CodeMirror.defineInitHook(function(cm) {
getSearchState(cm);
+ cm.on('linterDone', details => {
+ const count = details.lines.length;
+ getSearchState(cm).errorLines = details.lines;
+ dom.cl.toggle('.cm-linter-widget', 'hasErrors', count !== 0);
+ dom.text(
+ '.cm-linter-widget .cm-linter-widget-count',
+ i18n$('linterMainReport').replace('{{count}}', count.toLocaleString())
+ );
+ });
});
}
diff --git a/src/js/codemirror/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js
index 04e0abafa..5ea175af1 100644
--- a/src/js/codemirror/ubo-static-filtering.js
+++ b/src/js/codemirror/ubo-static-filtering.js
@@ -39,7 +39,6 @@ let hintHelperRegistered = false;
/******************************************************************************/
CodeMirror.defineMode('ubo-static-filtering', function() {
- if ( sfp.AstFilterParser instanceof Object === false ) { return; }
const astParser = new sfp.AstFilterParser({
interactive: true,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
@@ -300,8 +299,6 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
// https://codemirror.net/demo/complete.html
const initHints = function() {
- if ( sfp.AstFilterParser instanceof Object === false ) { return; }
-
const astParser = new sfp.AstFilterParser({
interactive: true,
nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
@@ -666,6 +663,146 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => {
/******************************************************************************/
+// Linter
+
+{
+ const astParser = new sfp.AstFilterParser({
+ interactive: true,
+ nativeCssHas: vAPI.webextFlavor.env.includes('native_css_has'),
+ });
+
+ const markedset = [];
+ let markedsetStart = 0;
+ let markedsetTimer;
+
+ const processMarkedsetAsync = doc => {
+ if ( markedsetTimer !== undefined ) { return; }
+ markedsetTimer = self.requestIdleCallback(deadline => {
+ markedsetTimer = undefined;
+ processMarkedset(doc, deadline);
+ });
+ };
+
+ const processMarkedset = (doc, deadline) => {
+ const lineCount = doc.lineCount();
+ doc.eachLine(markedsetStart, lineCount, lineHandle => {
+ const line = markedsetStart++;
+ const markers = lineHandle.gutterMarkers || null;
+ if ( markers && markers['CodeMirror-lintgutter'] ) {
+ markedset.push(lineHandle.lineNo());
+ }
+ if ( (line & 0x0F) === 0 && deadline.timeRemaining() === 0 ) {
+ processMarkedsetAsync(doc);
+ return true;
+ }
+ if ( markedsetStart === lineCount ) {
+ CodeMirror.signal(
+ doc.getEditor(),
+ 'linterDone',
+ { lines: markedset }
+ );
+ }
+ });
+ };
+
+ const changeset = [];
+ let changesetTimer;
+
+ const addChanges = (doc, change) => {
+ changeset.push(change);
+ processChangesetAsync(doc);
+ };
+
+ const processChangesetAsync = doc => {
+ if ( changesetTimer !== undefined ) { return; }
+ if ( markedsetTimer ) {
+ self.cancelIdleCallback(markedsetTimer);
+ markedsetTimer = undefined;
+ }
+ changesetTimer = self.requestIdleCallback(deadline => {
+ changesetTimer = undefined;
+ processChangeset(doc, deadline);
+ });
+ };
+
+ const extractError = ( ) => {
+ if ( astParser.isComment() ) { return; }
+ if ( astParser.isFilter() === false ) { return; }
+ if ( astParser.hasError() === false ) { return; }
+ let error = 'Invalid filter';
+ if ( astParser.isCosmeticFilter() && astParser.result.error ) {
+ error = `${error}: ${astParser.result.error}`;
+ }
+ return error;
+ };
+
+ const extractMarker = lineInfo => {
+ if ( lineInfo.gutterMarkers instanceof Object === false ) { return; }
+ return lineInfo.gutterMarkers['CodeMirror-lintgutter'];
+ };
+
+ const markerTemplate = (( ) => {
+ const marker = document.createElement('div');
+ marker.classList.add('CodeMirror-lintmarker');
+ marker.textContent = '\xA0';
+ const info = document.createElement('span');
+ marker.append(info);
+ return marker;
+ })();
+
+ const makeMarker = (doc, line, marker, error) => {
+ if ( marker === undefined ) {
+ marker = markerTemplate.cloneNode(true);
+ doc.setGutterMarker(line, 'CodeMirror-lintgutter', marker);
+ }
+ marker.children[0].textContent = error;
+ };
+
+ const processChangeset = (doc, deadline) => {
+ const cm = doc.getEditor();
+ cm.startOperation();
+ while ( changeset.length !== 0 ) {
+ const { from, to } = changeset.shift();
+ for ( let line = from; line < to; line++ ) {
+ const lineInfo = doc.lineInfo(line);
+ if ( lineInfo === null ) { continue; }
+ astParser.parse(lineInfo.text);
+ const error = extractError();
+ let marker = extractMarker(lineInfo);
+ if ( error === undefined && marker ) {
+ doc.setGutterMarker(line, 'CodeMirror-lintgutter', null);
+ } else if ( error !== undefined ) {
+ makeMarker(doc, line, marker, error);
+ }
+ if ( (line & 0x0F) === 0 && deadline.timeRemaining() === 0 ) {
+ changeset.unshift({ doc, from: line+1, to });
+ break;
+ }
+ }
+ }
+ cm.endOperation();
+ if ( changeset.length !== 0 ) {
+ return processChangesetAsync(doc);
+ }
+ markedset.length = 0;
+ markedsetStart = 0;
+ processMarkedsetAsync(doc);
+ };
+
+ CodeMirror.defineInitHook(cm => {
+ cm.on('changes', function(cm, changes) {
+ const doc = cm.getDoc();
+ for ( const change of changes ) {
+ const from = change.from.line;
+ const to = from + change.text.length;
+ addChanges(doc, { from, to });
+ }
+ });
+ });
+}
+
+/******************************************************************************/
+
// Enhanced word selection
{
diff --git a/src/js/static-filtering-parser.js b/src/js/static-filtering-parser.js
index 28a6033b9..cc9063db7 100644
--- a/src/js/static-filtering-parser.js
+++ b/src/js/static-filtering-parser.js
@@ -705,7 +705,7 @@ export class AstFilterParser {
this.badTypes = new Set(options.badTypes || []);
this.maxTokenLength = options.maxTokenLength || 7;
// TODO: rethink this
- this.result = { exception: false, raw: '', compiled: '' };
+ this.result = { exception: false, raw: '', compiled: '', error: undefined };
this.selectorCompiler = new ExtSelectorCompiler(options);
// Regexes
this.reWhitespaceStart = /^\s+/;
@@ -2967,6 +2967,7 @@ class ExtSelectorCompiler {
this.nativeCssHas = instanceOptions.nativeCssHas === true;
// https://www.w3.org/TR/css-syntax-3/#typedef-ident-token
this.reInvalidIdentifier = /^\d/;
+ this.error = undefined;
}
compile(raw, out, compileOptions = {}) {
@@ -3016,7 +3017,10 @@ class ExtSelectorCompiler {
}
out.compiled = this.compileSelector(raw);
- if ( out.compiled === undefined ) { return false; }
+ if ( out.compiled === undefined ) {
+ out.error = this.error || undefined;
+ return false;
+ }
if ( out.compiled instanceof Object ) {
out.compiled.raw = raw;
@@ -3060,6 +3064,7 @@ class ExtSelectorCompiler {
parseValue: false,
});
} catch(reason) {
+ this.error = reason && reason.message || undefined;
return;
}
const parts = [];