re-lint only changed rules

This commit is contained in:
tophf 2017-08-29 00:12:42 +03:00
parent edc2ab0ead
commit 61351bcb2f
3 changed files with 267 additions and 31 deletions

View File

@ -182,7 +182,8 @@ function initCodeMirror() {
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
hintOptions: {},
lint: linterConfig.getForCodeMirror(),
lintReportDelay: prefs.get('editor.lintReportDelay'),
lintReportDelay: 500,
//lintReportDelay: prefs.get('editor.lintReportDelay'),
styleActiveLine: true,
theme: 'default',
keyMap: prefs.get('editor.keyMap'),
@ -461,10 +462,14 @@ function setupCodeMirror(textarea, index) {
return cm;
}
function indicateCodeChange(cm) {
function indicateCodeChange(cm, change) {
const section = cm.getSection();
setCleanItem(section, cm.isClean(section.savedValue));
updateTitle();
if (change) {
cm.stylusChanges = cm.stylusChanges || [];
cm.stylusChanges.push(change);
}
updateLintReportIfEnabled(cm);
}

View File

@ -1,31 +1,257 @@
/* global CodeMirror CSSLint stylelint linterConfig */
'use strict';
CodeMirror.registerHelper('lint', 'csslint', code =>
CSSLint.verify(code, deepCopy(linterConfig.getCurrent('csslint')))
.messages.map(message => ({
from: CodeMirror.Pos(message.line - 1, message.col - 1),
to: CodeMirror.Pos(message.line - 1, message.col),
message: message.message + ` (${message.rule.id})`,
severity : message.type
}))
);
(() => {
let config;
const cmpPos = CodeMirror.cmpPos;
CodeMirror.registerHelper('lint', 'stylelint', code =>
stylelint.lint({
code,
config: deepCopy(linterConfig.getCurrent('stylelint')),
}).then(({results}) => {
if (!results[0]) {
return [];
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);
}
return results[0].warnings.map(warning => ({
from: CodeMirror.Pos(warning.line - 1, warning.column - 1),
to: CodeMirror.Pos(warning.line - 1, warning.column),
message: warning.text
.replace('Unexpected ', '')
.replace(/^./, firstLetter => firstLetter.toUpperCase()),
severity : warning.severity
}));
})
);
// 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;
}
})();

View File

@ -196,17 +196,22 @@ function updateLintReport(cm, delay) {
update(cm);
return;
}
const state = cm.state.lint;
if (delay > 0) {
setTimeout(cm => {
clearTimeout((state || {}).lintTimeout);
(state || {}).lintTimeout = setTimeout(cm => {
// the temp monkeypatch only allows sep=false that returns a line array
// because during editing this is what we need, not the combined text
const _getValue = cm.getValue;
cm.getValue = sep => (sep === false ? _getValue.call(cm, sep) : '');
if (cm.performLint) {
cm.performLint();
cm.getValue = _getValue;
update(cm);
}
}, delay, cm);
return;
}
// eslint-disable-next-line no-var
var state = cm.state.lint;
if (!state) {
return;
}