Merge 00866fb740
into 4c616442f6
This commit is contained in:
commit
35adb238ed
56
edit/edit.js
56
edit/edit.js
|
@ -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'),
|
||||
|
@ -462,11 +463,15 @@ function setupCodeMirror(textarea, index) {
|
|||
return cm;
|
||||
}
|
||||
|
||||
function indicateCodeChange(cm) {
|
||||
function indicateCodeChange(cm, change) {
|
||||
const section = cm.getSection();
|
||||
setCleanItem(section, cm.isClean(section.savedValue));
|
||||
updateTitle();
|
||||
updateLintReportIfEnabled(cm);
|
||||
if (change) {
|
||||
cm.stylusChanges = cm.stylusChanges || [];
|
||||
cm.stylusChanges.push(change);
|
||||
}
|
||||
updateLintReport(cm);
|
||||
}
|
||||
|
||||
function getSectionForChild(e) {
|
||||
|
@ -593,7 +598,7 @@ window.onbeforeunload = () => {
|
|||
if (isCleanGlobal()) {
|
||||
return;
|
||||
}
|
||||
updateLintReportIfEnabled(null, 0);
|
||||
updateLintReport(null, 0);
|
||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||
return t('styleChangesNotSaved');
|
||||
};
|
||||
|
@ -1242,14 +1247,13 @@ function init() {
|
|||
section[CssToProperty[i]] = [params[i]];
|
||||
}
|
||||
}
|
||||
window.onload = () => {
|
||||
window.onload = null;
|
||||
addSection(null, section);
|
||||
editors[0].setOption('lint', CodeMirror.defaults.lint);
|
||||
// default to enabled
|
||||
$('#enabled').checked = true;
|
||||
initHooks();
|
||||
};
|
||||
addSection(null, section);
|
||||
editors[0].setOption('lint', CodeMirror.defaults.lint);
|
||||
// default to enabled
|
||||
$('#enabled').checked = true;
|
||||
initHooks();
|
||||
setCleanGlobal();
|
||||
updateTitle();
|
||||
return;
|
||||
}
|
||||
// This is an edit
|
||||
|
@ -1287,11 +1291,12 @@ function initWithStyle({style, codeIsUpdated}) {
|
|||
updateTitle();
|
||||
return;
|
||||
}
|
||||
|
||||
// if this was done in response to an update, we need to clear existing sections
|
||||
getSections().forEach(div => { div.remove(); });
|
||||
editors.length = 0;
|
||||
getSections().forEach(div => div.remove());
|
||||
const queue = style.sections.length ? style.sections.slice() : [{code: ''}];
|
||||
const queueStart = new Date().getTime();
|
||||
maximizeCodeHeight.stats = null;
|
||||
// after 100ms the sections will be added asynchronously
|
||||
while (new Date().getTime() - queueStart <= 100 && queue.length) {
|
||||
add();
|
||||
|
@ -1303,21 +1308,20 @@ function initWithStyle({style, codeIsUpdated}) {
|
|||
}
|
||||
})();
|
||||
initHooks();
|
||||
setCleanGlobal();
|
||||
updateTitle();
|
||||
|
||||
function add() {
|
||||
const sectionDiv = addSection(null, queue.shift());
|
||||
maximizeCodeHeight(sectionDiv, !queue.length);
|
||||
const cm = sectionDiv.CodeMirror;
|
||||
if (CodeMirror.lint) {
|
||||
setTimeout(() => {
|
||||
cm.setOption('lint', CodeMirror.defaults.lint);
|
||||
updateLintReport(cm, 0);
|
||||
}, prefs.get('editor.lintDelay'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initHooks() {
|
||||
if (initHooks.alreadyDone) {
|
||||
return;
|
||||
}
|
||||
initHooks.alreadyDone = true;
|
||||
$$('#header .style-contributor').forEach(node => {
|
||||
node.addEventListener('change', onChange);
|
||||
node.addEventListener('input', onChange);
|
||||
|
@ -1349,8 +1353,6 @@ function initHooks() {
|
|||
});
|
||||
|
||||
setupGlobalSearch();
|
||||
setCleanGlobal();
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
|
||||
|
@ -1451,14 +1453,8 @@ function validate() {
|
|||
return null;
|
||||
}
|
||||
|
||||
function updateLintReportIfEnabled(cm, time) {
|
||||
if (CodeMirror.lint) {
|
||||
updateLintReport(cm, time);
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
updateLintReportIfEnabled(null, 0);
|
||||
updateLintReport(null, 0);
|
||||
|
||||
// save the contents of the CodeMirror editors back into the textareas
|
||||
for (let i = 0; i < editors.length; i++) {
|
||||
|
|
|
@ -1,31 +1,288 @@
|
|||
/* 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 ||
|
||||
cm.stylusChanges.some(change => change.origin === 'setValue')
|
||||
) {
|
||||
// first run: lint everything
|
||||
cm.state.lint.marked = [];
|
||||
// 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));
|
||||
// extend ranges with pasted text
|
||||
for (const change of changes) {
|
||||
const addedLines = Math.max(0, change.text.length - 1);
|
||||
const removedLines = Math.max(0, change.removed.length - 1);
|
||||
const delta = addedLines - removedLines;
|
||||
change.to = CodeMirror.Pos(
|
||||
Math.max(0, change.to.line + delta),
|
||||
Math.max(0, change.to.ch + change.text.last.length - change.removed.last.length + 1)
|
||||
);
|
||||
}
|
||||
// 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.from, 4);
|
||||
}
|
||||
// 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 blockStart;
|
||||
let blockEnd;
|
||||
while (cursor.findPrevious()) {
|
||||
switch (cursor.pos.match[0]) {
|
||||
case '{':
|
||||
if (!cmtStart || cmp(cmtStart, cursor.pos.to) > 0) {
|
||||
blockStart = cursor.pos.from;
|
||||
}
|
||||
break;
|
||||
case '}':
|
||||
if (!cmtStart || cmp(cmtStart, cursor.pos.to) > 0) {
|
||||
blockEnd = cursor.pos.to;
|
||||
if (--repetitions <= 0 || !blockStart) {
|
||||
return blockEnd;
|
||||
}
|
||||
blockStart = null;
|
||||
}
|
||||
break;
|
||||
case '/*':
|
||||
cmtStart = cursor.pos.to;
|
||||
if (cmp(cmtEnd, blockEnd) > 0) {
|
||||
blockEnd = null;
|
||||
}
|
||||
if (cmp(cmtEnd, blockStart) > 0) {
|
||||
blockStart = 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;
|
||||
let depth = 0;
|
||||
while (cursor.findNext()) {
|
||||
switch (cursor.pos.match[0]) {
|
||||
case '{':
|
||||
if (!cmtStart) {
|
||||
depth++;
|
||||
}
|
||||
break;
|
||||
case '}':
|
||||
if (!cmtStart && (--depth <= 0 && --repetitions <= 0)) {
|
||||
return depth < 0 ? cursor.pos.from : 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.from.line + ' ' + item.from.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;
|
||||
}
|
||||
})();
|
||||
|
|
34
edit/lint.js
34
edit/lint.js
|
@ -191,29 +191,49 @@ function updateLinter({immediately} = {}) {
|
|||
}
|
||||
|
||||
function updateLintReport(cm, delay) {
|
||||
if (!CodeMirror.defaults.lint) {
|
||||
return;
|
||||
}
|
||||
if (cm && !cm.options.lint) {
|
||||
setTimeout(() => {
|
||||
if (cm.options.lint) {
|
||||
return;
|
||||
}
|
||||
cm.setOption('lint', linterConfig.getForCodeMirror());
|
||||
if (!delay) {
|
||||
setTimeout(() => {
|
||||
clearTimeout((cm.state.lint || {}).renderTimeout);
|
||||
renderLintReport();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
const state = cm && cm.state && cm.state.lint || {};
|
||||
if (delay === 0) {
|
||||
// immediately show pending csslint/stylelint messages in onbeforeunload and save
|
||||
clearTimeout(state.lintTimeout);
|
||||
update(cm);
|
||||
return;
|
||||
}
|
||||
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();
|
||||
update(cm);
|
||||
}
|
||||
cm.getValue = _getValue;
|
||||
}, delay, cm);
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-var
|
||||
var state = cm.state.lint;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
// user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms)
|
||||
// or update it as soon as possible (default: 500ms lint + 100ms) in case an existing issue was just fixed
|
||||
clearTimeout(state.reportTimeout);
|
||||
state.reportTimeout = setTimeout(update, state.options.delay + 100, cm);
|
||||
state.reportTimeout = setTimeout(update, (state.options || {}).delay + 100, cm);
|
||||
state.postponeNewIssues = delay === undefined || delay === null;
|
||||
|
||||
function update(cm) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user