258 lines
7.8 KiB
JavaScript
258 lines
7.8 KiB
JavaScript
/* global CodeMirror CSSLint stylelint linterConfig */
|
|
'use strict';
|
|
|
|
(() => {
|
|
let config;
|
|
const cmpPos = CodeMirror.cmpPos;
|
|
|
|
CodeMirror.registerHelper('lint', 'csslint', (code, options, cm) =>
|
|
copyOldIssues(cm, lintChangedRanges(cm, csslintOnRange))
|
|
);
|
|
|
|
CodeMirror.registerHelper('lint', 'stylelint', (code, options, cm) =>
|
|
Promise.all(lintChangedRanges(cm, stylelintOnRange))
|
|
.then(results => copyOldIssues(cm, results))
|
|
);
|
|
|
|
function csslintOnRange(range) {
|
|
return CSSLint.verify(range.code, config).messages
|
|
.map(item =>
|
|
cookResult(
|
|
range,
|
|
item.line,
|
|
item.col,
|
|
item.message.replace(/ at line \d+, col \d+/, '') + ` (${item.rule.id})`,
|
|
item.type
|
|
)
|
|
);
|
|
}
|
|
|
|
function stylelintOnRange(range) {
|
|
return stylelint.lint({code: range.code, config})
|
|
.then(({results}) => ((results[0] || {}).warnings || [])
|
|
.map(item =>
|
|
cookResult(
|
|
range,
|
|
item.line,
|
|
item.column,
|
|
item.text
|
|
.replace('Unexpected ', '')
|
|
.replace(/^./, firstLetter => firstLetter.toUpperCase()),
|
|
item.severity
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
function cookResult(range, line, col, message, severity) {
|
|
line--;
|
|
col--;
|
|
const realL = line + range.from.line;
|
|
const realC = col + (line === 0 ? range.from.ch : 0);
|
|
return {
|
|
from: CodeMirror.Pos(realL, realC),
|
|
to: CodeMirror.Pos(realL, realC + 1),
|
|
message,
|
|
severity,
|
|
};
|
|
}
|
|
|
|
function lintChangedRanges(cm, lintFunction) {
|
|
const EOF = CodeMirror.Pos(cm.doc.size - 1, cm.getLine(cm.doc.size - 1).length);
|
|
// cache the config for subsequent *lintOnRange
|
|
config = deepCopy(linterConfig.getCurrent());
|
|
let ranges;
|
|
if (!cm.stylusChanges || !cm.stylusChanges.length) {
|
|
// first run: lint everything
|
|
// the temp monkeypatch in updateLintReport() is there
|
|
// only to allow sep=false that returns a line array
|
|
ranges = [{
|
|
code: cm.getValue(false).join('\n'),
|
|
from: {line: 0, ch: 0},
|
|
to: EOF,
|
|
}];
|
|
} else {
|
|
// sort by 'from' position in ascending order
|
|
const changes = cm.stylusChanges.sort((a, b) => cmpPos(a.from, b.from));
|
|
// merge pass 1
|
|
ranges = mergeRanges(changes);
|
|
// extend up to previous } and down to next }
|
|
for (const range of ranges) {
|
|
range.from = findBlockEndBefore(range.from, 2);
|
|
range.to = findBlockEndAfter(range.to, 2);
|
|
}
|
|
// merge pass 2 on the extended ranges
|
|
ranges = mergeRanges(ranges);
|
|
}
|
|
// fill the code and run lintFunction
|
|
const results = [];
|
|
for (const range of ranges) {
|
|
range.code = cm.getRange(range.from, range.to);
|
|
results.push(lintFunction(range));
|
|
}
|
|
// reset the changes queue and pass the ranges to updateLintReport
|
|
(cm.stylusChanges || []).length = 0;
|
|
cm.state.lint.changedRanges = ranges;
|
|
return results;
|
|
|
|
function findBlockEndBefore(pos, repetitions = 1) {
|
|
const PREV_CMT_END = find('*/', pos, -1);
|
|
const PREV_CMT_START = (prev => cmp(prev, pos) < 0 && prev)(find('/*', PREV_CMT_END, +1));
|
|
const NEXT_CMT_END = PREV_CMT_START && (find('*/', PREV_CMT_START, +1) || EOF);
|
|
const cursor = cm.getSearchCursor(/\/\*|\*\/|}/, pos, {caseFold: false});
|
|
let cmtStart = PREV_CMT_START;
|
|
let cmtEnd = cmtStart && cmp(NEXT_CMT_END, pos) > 0 && NEXT_CMT_END;
|
|
let blockEnd;
|
|
while (cursor.findPrevious()) {
|
|
switch (cursor.pos.match[0]) {
|
|
case '}':
|
|
if (!cmtStart || cmp(cmtStart, cursor.pos.to) > 0) {
|
|
blockEnd = cursor.pos.to;
|
|
if (--repetitions <= 0) {
|
|
return blockEnd;
|
|
}
|
|
}
|
|
break;
|
|
case '/*':
|
|
cmtStart = cursor.pos.to;
|
|
if (cmp(cmtEnd, blockEnd) > 0) {
|
|
blockEnd = null;
|
|
}
|
|
break;
|
|
case '*/':
|
|
cmtEnd = cursor.pos.to;
|
|
if (blockEnd && --repetitions <= 0) {
|
|
return blockEnd;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return blockEnd || {line: 0, ch: 0};
|
|
}
|
|
|
|
function findBlockEndAfter(pos, repetitions = 1) {
|
|
const PREV_CMT_END = find('*/', pos, -1);
|
|
const PREV_CMT_START = (prev => cmp(prev, pos) < 0 && prev)(find('/*', PREV_CMT_END, +1));
|
|
const cursor = cm.getSearchCursor(/\/\*|\*\/|}/, pos, {caseFold: false});
|
|
let cmtStart = PREV_CMT_START;
|
|
while (cursor.findNext()) {
|
|
switch (cursor.pos.match[0]) {
|
|
case '}':
|
|
if (!cmtStart && --repetitions <= 0) {
|
|
return cursor.pos.to;
|
|
}
|
|
break;
|
|
case '/*':
|
|
cmtStart = cmtStart || cursor.pos.from;
|
|
break;
|
|
case '*/':
|
|
cmtStart = null;
|
|
break;
|
|
}
|
|
}
|
|
return EOF;
|
|
}
|
|
|
|
function find(query, pos, direction) {
|
|
const cursor = cm.getSearchCursor(query, pos, {caseFold: false});
|
|
return direction > 0
|
|
? cursor.findNext() && cursor.from()
|
|
: cursor.findPrevious() && cursor.to();
|
|
}
|
|
|
|
function cmp(a, b) {
|
|
if (!a && !b) {
|
|
return 0;
|
|
}
|
|
if (!a) {
|
|
return -1;
|
|
}
|
|
if (!b) {
|
|
return 1;
|
|
}
|
|
return cmpPos(a, b);
|
|
}
|
|
}
|
|
|
|
function mergeRanges(sorted) {
|
|
const ranges = [];
|
|
let lastChange = {from: {}, to: {line: -1, ch: -1}};
|
|
for (const change of sorted) {
|
|
if (cmpPos(change.from, change.to) > 0) {
|
|
// straighten the inverted range
|
|
const from = change.from;
|
|
change.from = change.to;
|
|
change.to = from;
|
|
}
|
|
if (cmpPos(change.from, lastChange.to) > 0) {
|
|
ranges.push({
|
|
from: change.from,
|
|
to: change.to,
|
|
code: '',
|
|
});
|
|
} else if (cmpPos(change.to, lastChange.to) > 0) {
|
|
ranges[ranges.length - 1].to = change.to;
|
|
}
|
|
lastChange = change;
|
|
}
|
|
return ranges;
|
|
}
|
|
|
|
function copyOldIssues(cm, newAnns) {
|
|
const EOF = CodeMirror.Pos(cm.doc.size - 1, cm.getLine(cm.doc.size - 1).length);
|
|
|
|
const oldMarkers = cm.state.lint.marked;
|
|
let oldIndex = 0;
|
|
let oldAnn = (oldMarkers[0] || {}).__annotation;
|
|
|
|
const newRanges = cm.state.lint.changedRanges || [];
|
|
let newIndex = 0;
|
|
let newRange = newRanges[0];
|
|
|
|
const finalAnns = [];
|
|
const unique = new Set();
|
|
const pushUnique = item => {
|
|
const key = item.line + ' ' + item.ch + ' ' + item.message;
|
|
if (!unique.has(key)) {
|
|
unique.add(key);
|
|
finalAnns.push(item);
|
|
}
|
|
};
|
|
|
|
const t0 = performance.now();
|
|
while (oldAnn && cmpPos(oldAnn.from, EOF) < 0 || newRange) {
|
|
if (performance.now() - t0 > 500) {
|
|
console.error('infinite loop canceled',
|
|
JSON.stringify([
|
|
newAnns,
|
|
oldMarkers[0] && oldMarkers.map(m => ({from: m.__annotation.from, to: m.__annotation.to})),
|
|
newRanges.map(r => Object.assign(r, {code: undefined}))
|
|
])
|
|
);
|
|
break;
|
|
}
|
|
// copy old issues prior to current newRange
|
|
// eslint-disable-next-line no-unmodified-loop-condition
|
|
while (oldAnn && (!newRange || cmpPos(oldAnn.to, newRange.from) < 0)) {
|
|
pushUnique(oldAnn);
|
|
oldIndex++;
|
|
oldAnn = (oldMarkers[oldIndex] || {}).__annotation;
|
|
}
|
|
// skip all old issues within newRange
|
|
if (newRange) {
|
|
while (oldAnn && cmpPos(oldAnn.to, newRange.to) <= 0) {
|
|
oldAnn = (oldMarkers[oldIndex++] || {}).__annotation;
|
|
}
|
|
}
|
|
// copy all newRange prior to current oldAnn
|
|
// eslint-disable-next-line no-unmodified-loop-condition
|
|
while (newRange && (!oldAnn || cmpPos(newRange.to, oldAnn.from) <= 0)) {
|
|
newAnns[newIndex].forEach(pushUnique);
|
|
newIndex++;
|
|
newRange = newRanges[newIndex];
|
|
}
|
|
}
|
|
return finalAnns;
|
|
}
|
|
})();
|