worker for stylelint; hints in linter config popup

This commit is contained in:
tophf 2017-11-28 20:03:50 +03:00
parent c2d68612ec
commit 493c1a65c0
9 changed files with 331 additions and 193 deletions

View File

@ -215,6 +215,10 @@
"message": "Use default",
"description": "'Set to default' button in a confirm dialog"
},
"confirmDiscardChanges": {
"message": "Discard the changes?",
"description": "Generic label or title displayed when trying to close something (not a style) with unsaved changes"
},
"confirmSave": {
"message": "Save",
"description": "'Save' button in a confirm dialog"

View File

@ -1,3 +1,6 @@
.CodeMirror-hints {
z-index: 999;
}
.CodeMirror-hint:hover {
color: white;
background: #08f;

View File

@ -1718,6 +1718,7 @@ function fromMozillaFormat() {
popup.codebox.focus();
popup.codebox.on('changes', cm => {
popup.classList.toggle('ready', !cm.isBlank());
cm.markClean();
});
// overwrite default extraKeys as those are inapplicable in popup context
popup.codebox.options.extraKeys = {
@ -1885,15 +1886,18 @@ function showKeyMapHelp() {
}
}
function showHelp(title, body) {
function showHelp(title = '', body) {
const div = $('#help-popup');
div.classList.remove('big');
$('.contents', div).textContent = '';
$('.contents', div).appendChild(typeof body === 'string' ? tHTML(body) : body);
const contents = $('.contents', div);
contents.textContent = '';
if (body) {
contents.appendChild(typeof body === 'string' ? tHTML(body) : body);
}
$('.title', div).textContent = title;
if (getComputedStyle(div).display === 'none') {
document.addEventListener('keydown', closeHelp);
window.addEventListener('keydown', closeHelp, true);
// avoid chaining on multiple showHelp() calls
$('.dismiss', div).onclick = closeHelp;
}
@ -1902,16 +1906,19 @@ function showHelp(title, body) {
return div;
function closeHelp(e) {
if (
!e ||
e.type === 'click' ||
((e.keyCode || e.which) === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)
) {
if (!e || e.type === 'click' ||
(e.which === 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey &&
!$('.CodeMirror-hints, #message-box') && !(document.activeElement instanceof HTMLInputElement))) {
if (e && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
messageBox.confirm(t('confirmDiscardChanges')).then(ok => ok && closeHelp());
return;
}
div.style.display = '';
const contents = $('.contents');
contents.textContent = '';
clearTimeout(contents.timer);
document.removeEventListener('keydown', closeHelp);
window.removeEventListener('keydown', closeHelp, true);
window.dispatchEvent(new Event('closeHelp'));
(editors.lastActive || editors[0]).focus();
}
}
}

View File

@ -1,38 +1,26 @@
/* global CodeMirror CSSLint parserlib stylelint linterConfig */
/* global CodeMirror linterConfig */
'use strict';
CodeMirror.registerHelper('lint', 'csslint', code => new Promise(resolve => {
CSSLint.onmessage = ({data}) => {
resolve(
data.map(({line, col, message, rule, type}) => line && {
CodeMirror.registerHelper('lint', 'csslint', code =>
linterConfig.invokeWorker({code, config: linterConfig.getCurrent()}).then(results =>
results.map(({line, col: ch, message, rule, type: severity}) => line && {
message,
from: {line: line - 1, ch: col - 1},
to: {line: line - 1, ch: col},
from: {line: line - 1, ch: ch - 1},
to: {line: line - 1, ch},
rule: rule.id,
severity: type
}).filter(Boolean));
};
const config = deepCopy(linterConfig.getCurrent('csslint'));
CSSLint.postMessage({action: 'verify', code, config});
}));
severity,
}).filter(Boolean)));
CodeMirror.registerHelper('lint', 'stylelint', code =>
stylelint.lint({
code,
config: deepCopy(linterConfig.getCurrent('stylelint')),
}).then(({results}) => {
if (!results[0]) {
return [];
}
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
linterConfig.invokeWorker({code, config: linterConfig.getCurrent()}).then(({results}) =>
!results[0] && [] ||
results[0].warnings.map(({line, column:ch, text, severity}) => ({
from: {line: line - 1, ch: ch - 1},
to: {line: line - 1, ch},
message: text
.replace('Unexpected ', '')
.replace(/^./, firstLetter => firstLetter.toUpperCase())
.replace(/\s*\([^(]+\)$/, ''), // strip the rule,
rule: warning.text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
severity : warning.severity
}));
})
);
rule: text.replace(/^.*?\s*\(([^(]+)\)$/, '$1'),
severity,
}))));

View File

@ -1,5 +1,5 @@
/* global CodeMirror messageBox */
/* global editors makeSectionVisible showCodeMirrorPopup showHelp */
/* global editors makeSectionVisible showCodeMirrorPopup showHelp hotkeyRerouter */
/* global loadScript require CSSLint stylelint */
/* global makeLink */
'use strict';
@ -20,8 +20,16 @@ var linterConfig = {
csslint: 'editorCSSLintConfig',
stylelint: 'editorStylelintConfig',
},
worker: {
csslint: {path: '/vendor-overwrites/csslint/csslint-worker.js'},
stylelint: {path: '/vendor-overwrites/stylelint/stylelint-bundle.min.js'},
},
allRuleIds: {
csslint: null,
stylelint: null,
},
getDefault() {
getName() {
// some dirty hacks to override editor.linter getting from prefs
const linter = prefs.get('editor.linter');
if (linter && editors[0] && editors[0].getOption('mode') !== 'css') {
@ -30,11 +38,11 @@ var linterConfig = {
return linter;
},
getCurrent(linter = linterConfig.getDefault()) {
getCurrent(linter = linterConfig.getName()) {
return this.fallbackToDefaults(this[linter] || {});
},
getForCodeMirror(linter = linterConfig.getDefault()) {
getForCodeMirror(linter = linterConfig.getName()) {
return CodeMirror.lint && CodeMirror.lint[linter] ? {
getAnnotations: CodeMirror.lint[linter],
delay: prefs.get('editor.lintDelay'),
@ -44,7 +52,7 @@ var linterConfig = {
} : false;
},
fallbackToDefaults(config, linter = linterConfig.getDefault()) {
fallbackToDefaults(config, linter = linterConfig.getName()) {
if (config && Object.keys(config).length) {
if (linter === 'stylelint') {
// always use default syntax because we don't expose it in config UI
@ -56,33 +64,52 @@ var linterConfig = {
}
},
setLinter(linter = linterConfig.getDefault()) {
setLinter(linter = linterConfig.getName()) {
linter = linter.toLowerCase();
linter = linter === 'csslint' || linter === 'stylelint' ? linter : '';
if (linterConfig.getDefault() !== linter) {
if (linterConfig.getName() !== linter) {
prefs.set('editor.linter', linter);
}
return linter;
},
findInvalidRules(config, linter = linterConfig.getDefault()) {
const rules = linter === 'stylelint' ? config.rules : config;
return new Promise(resolve => {
if (linter === 'stylelint') {
resolve(Object.keys(stylelint.rules));
} else {
CSSLint.onmessage = ({data}) =>
resolve(data.map(rule => rule.id));
CSSLint.postMessage({action: 'getRules'});
invokeWorker(message) {
const worker = linterConfig.worker[message.linter || linterConfig.getName()];
if (!worker.queue) {
worker.queue = [];
worker.instance.onmessage = ({data}) => {
worker.queue.shift().resolve(data);
if (worker.queue.length) {
worker.instance.postMessage(worker.queue[0].message);
}
}).then(allRules => {
allRules = new Set(allRules);
return Object.keys(rules).filter(rule => !allRules.has(rule));
};
}
return new Promise(resolve => {
worker.queue.push({message, resolve});
if (worker.queue.length === 1) {
worker.instance.postMessage(message);
}
});
},
getAllRuleIds(linter = linterConfig.getName()) {
return Promise.resolve(
this.allRuleIds[linter] ||
this.invokeWorker({linter, action: 'getAllRuleIds'})
.then(ids => (this.allRuleIds[linter] = ids.sort()))
);
},
findInvalidRules(config, linter = linterConfig.getName()) {
return this.getAllRuleIds(linter).then(allRuleIds => {
const allRuleIdsSet = new Set(allRuleIds);
const rules = linter === 'stylelint' ? config.rules : config;
return Object.keys(rules).filter(rule => !allRuleIdsSet.has(rule));
});
},
stringify(config = this.getCurrent()) {
if (linterConfig.getDefault() === 'stylelint') {
if (linterConfig.getName() === 'stylelint') {
config.syntax = undefined;
}
return JSON.stringify(config, null, 2)
@ -91,7 +118,7 @@ var linterConfig = {
save(config) {
config = this.fallbackToDefaults(config);
const linter = linterConfig.getDefault();
const linter = linterConfig.getName();
this[linter] = config;
BG.chromeSync.setLZValue(this.storageName[linter], config);
return config;
@ -155,7 +182,7 @@ function initLint() {
prefs.subscribe(['editor.linter'], updateLinter);
}
function updateLinter({immediately, linter = linterConfig.getDefault()} = {}) {
function updateLinter({immediately, linter = linterConfig.getName()} = {}) {
if (!immediately) {
debounce(updateLinter, 0, {immediately: true, linter});
return;
@ -358,17 +385,16 @@ function gotoLintIssue(event) {
}
function showLintHelp() {
const linter = linterConfig.getDefault();
const linter = linterConfig.getName();
const baseUrl = linter === 'stylelint'
? 'https://stylelint.io/user-guide/rules/'
// some CSSLint rules do not have a url
: 'https://github.com/CSSLint/csslint/issues/535';
let headerLink, template;
if (linter === 'csslint') {
const CSSLintRules = CSSLint.getRules();
headerLink = makeLink('https://github.com/CSSLint/csslint/wiki/Rules-by-ID', 'CSSLint');
template = ruleID => {
const rule = CSSLintRules.find(rule => rule.id === ruleID);
const rule = linterConfig.allRuleIds.csslint.find(rule => rule.id === ruleID);
return rule &&
$element({tag: 'li', appendChild: [
$element({tag: 'b', appendChild: makeLink(rule.url || baseUrl, rule.name)}),
@ -398,91 +424,79 @@ function showLintHelp() {
);
}
function showLinterErrorMessage(title, contents) {
function showLinterErrorMessage(title, contents, popup) {
messageBox({
title,
contents,
className: 'danger center lint-config',
buttons: [t('confirmOK')],
});
}
function setupLinterSettingsEvents(popup) {
$('.save', popup).addEventListener('click', event => {
event.preventDefault();
const linter = linterConfig.setLinter(event.target.dataset.linter);
const json = tryJSONparse(popup.codebox.getValue());
if (json) {
showLinterErrorMessage(linter, t('linterJSONError'));
popup.codebox.focus();
return;
}
linterConfig.findInvalidRules(json, linter).then(invalid => {
if (invalid.length) {
showLinterErrorMessage(linter, [
t('linterInvalidConfigError'),
$element({
tag: 'ul',
appendChild: invalid.map(name =>
$element({tag: 'li', textContent: name})),
}),
]);
return;
}
linterConfig.save(json);
linterConfig.showSavedMessage();
popup.codebox.markClean();
popup.codebox.focus();
});
});
$('.reset', popup).addEventListener('click', event => {
event.preventDefault();
const linter = linterConfig.setLinter(event.target.dataset.linter);
popup.codebox.setValue(linterConfig.stringify(linterConfig.defaults[linter] || {}));
popup.codebox.focus();
});
$('.cancel', popup).addEventListener('click', event => {
event.preventDefault();
$('.dismiss').dispatchEvent(new Event('click'));
});
}).then(() => popup && popup.codebox.focus());
}
function setupLinterPopup(config) {
const linter = linterConfig.getDefault();
const linter = linterConfig.getName();
const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint';
const defaultConfig = linterConfig.stringify(linterConfig.defaults[linter] || {});
const title = t('linterConfigPopupTitle', linterTitle);
const popup = showCodeMirrorPopup(title, null, {
lint: false,
extraKeys: {'Ctrl-Enter': save},
hintOptions: {hint},
});
$('.contents', popup).appendChild(makeFooter());
function makeButton(className, text, options = {}) {
return $element(Object.assign(options, {
tag: 'button',
const cm = popup.codebox;
cm.focus();
cm.setValue(config);
cm.clearHistory();
cm.markClean();
cm.on('changes', updateButtonState);
updateButtonState();
hotkeyRerouter.setState(false);
window.addEventListener('closeHelp', function _() {
window.removeEventListener('closeHelp', _);
hotkeyRerouter.setState(true);
});
loadScript([
'/vendor/codemirror/mode/javascript/javascript.js',
'/vendor/codemirror/addon/lint/json-lint.js',
'/vendor/jsonlint/jsonlint.js'
]).then(() => {
cm.setOption('mode', 'application/json');
cm.setOption('lint', 'json');
});
function makeFooter() {
const makeButton = (className, onclick, text, options = {}) =>
$element(Object.assign(options, {
className,
onclick,
tag: 'button',
type: 'button',
textContent: t(text),
dataset: {linter}
}));
}
function makeLink(url, textContent) {
return $element({tag: 'a', target: '_blank', href: url, textContent});
}
const title = t('linterConfigPopupTitle', linterTitle);
const contents = $element({
return $element({
appendChild: [
$element({
tag: 'p',
appendChild: [
t('linterRulesLink') + ' ',
makeLink(
linter === 'stylelint'
$element({
tag: 'a',
target: '_blank',
href: linter === 'stylelint'
? 'https://stylelint.io/user-guide/rules/'
: 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID',
linterTitle
),
textContent: linterTitle
}),
linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : ''
]
}),
makeButton('save', 'styleSaveLabel', {disabled: true}),
makeButton('cancel', 'confirmCancel'),
makeButton('reset', 'genericResetLabel', {title: t('linterResetMessage')}),
makeButton('save', save, 'styleSaveLabel', {title: 'Ctrl-Enter'}),
makeButton('cancel', cancel, 'confirmClose'),
makeButton('reset', reset, 'genericResetLabel', {title: t('linterResetMessage')}),
$element({
tag: 'span',
className: 'saved-message',
@ -490,58 +504,122 @@ function setupLinterPopup(config) {
})
]
});
const popup = showCodeMirrorPopup(title, contents, {lint: false});
contents.parentNode.appendChild(contents);
popup.codebox.focus();
popup.codebox.setValue(config);
popup.codebox.clearHistory();
popup.codebox.markClean();
popup.codebox.on('change', cm => {
$('.save', popup).disabled = cm.isClean();
});
setupLinterSettingsEvents(popup);
loadScript([
'/vendor/codemirror/mode/javascript/javascript.js',
'/vendor/codemirror/addon/lint/json-lint.js',
'/vendor/jsonlint/jsonlint.js'
]).then(() => {
popup.codebox.setOption('mode', 'application/json');
popup.codebox.setOption('lint', 'json');
});
}
function loadLinterAssets(name = linterConfig.getDefault()) {
if (!name) {
return Promise.resolve();
}
return loadLibrary().then(loadAddon);
function loadLibrary() {
if (name === 'csslint' && !window.CSSLint) {
window.CSSLint = new Worker('/vendor-overwrites/csslint/csslint-worker.js');
return loadScript([
'/edit/lint-defaults-csslint.js'
]);
}
if (name === 'stylelint' && !window.stylelint) {
return loadScript([
'/vendor-overwrites/stylelint/stylelint-bundle.min.js',
'/edit/lint-defaults-stylelint.js'
]).then(() => (window.stylelint = require('stylelint')));
}
return Promise.resolve();
}
function loadAddon() {
if (CodeMirror.lint) {
function save(event) {
if (event instanceof Event) {
event.preventDefault();
}
const json = tryJSONparse(cm.getValue());
if (!json) {
showLinterErrorMessage(linter, t('linterJSONError'), popup);
cm.focus();
}
linterConfig.findInvalidRules(json, linter).then(invalid => {
if (invalid.length) {
showLinterErrorMessage(linter, [
t('linterInvalidConfigError'),
$element({tag: 'ul', appendChild: invalid.map(name =>
$element({tag: 'li', textContent: name})),
}),
], popup);
return;
}
return loadScript([
linterConfig.setLinter(linter);
linterConfig.save(json);
linterConfig.showSavedMessage();
cm.markClean();
cm.focus();
updateButtonState();
});
}
function reset(event) {
event.preventDefault();
if (linterConfig.getName() !== linter) {
linterConfig.setLinter(linter);
}
cm.setValue(defaultConfig);
cm.focus();
updateButtonState();
}
function cancel(event) {
event.preventDefault();
$('.dismiss').dispatchEvent(new Event('click'));
}
function updateButtonState() {
$('.save', popup).disabled = cm.isClean();
$('.reset', popup).disabled = cm.getValue() === defaultConfig;
$('.cancel', popup).textContent = t(cm.isClean() ? 'confirmClose' : 'confirmCancel');
}
function hint(cm) {
return Promise.all([
linterConfig.getAllRuleIds(linter),
linter !== 'stylelint' || hint.allOptions ||
linterConfig.invokeWorker({action: 'getAllRuleOptions', linter})
.then(options => (hint.allOptions = options)),
])
.then(([ruleIds, options]) => {
const cursor = cm.getCursor();
const {start, end, string, type, state: {lexical}} = cm.getTokenAt(cursor);
const {line, ch} = cursor;
const quoted = string.startsWith('"');
const leftPart = string.slice(quoted ? 1 : 0, ch - start).trim();
const depth = getLexicalDepth(lexical);
const search = cm.getSearchCursor(/"([-\w]+)"/, {line, ch: start - 1});
let [, prevWord] = search.find(true) || [];
let words = [];
if (depth === 1 && linter === 'stylelint') {
words = quoted ? ['rules'] : [];
} else if ((depth === 1 || depth === 2) && type && type.includes('property')) {
words = ruleIds;
} else if (depth === 2 || depth === 3 && lexical.type === ']') {
words = !quoted ? ['true', 'false', 'null'] :
ruleIds.includes(prevWord) && (options[prevWord] || [])[0] || [];
} else if (depth === 4 && prevWord === 'severity') {
words = ['error', 'warning'];
} else if (depth === 4) {
words = ['ignore', 'ignoreAtRules', 'except', 'severity'];
} else if (depth === 5 && lexical.type === ']' && quoted) {
while (prevWord && !ruleIds.includes(prevWord)) {
prevWord = (search.find(true) || [])[1];
}
words = (options[prevWord] || []).slice(-1)[0] || ruleIds;
}
return {
list: words.filter(word => word.startsWith(leftPart)),
from: {line, ch: start + (quoted ? 1 : 0)},
to: {line, ch: string.endsWith('"') ? end - 1 : end},
};
});
}
function getLexicalDepth(lexicalState) {
let depth = 0;
while ((lexicalState = lexicalState.prev)) {
depth++;
}
return depth;
}
}
function loadLinterAssets(name = linterConfig.getName()) {
const worker = linterConfig.worker[name];
return !name || !worker || worker.instance ? Promise.resolve() :
loadScript((worker.instance ? [] : [
(worker.instance = new Worker(worker.path)),
`/edit/lint-defaults-${name}.js`,
]).concat(CodeMirror.lint ? [] : [
'/vendor/codemirror/addon/lint/lint.css',
'/msgbox/msgbox.css',
'/vendor/codemirror/addon/lint/lint.js',
'/edit/lint-codemirror-helper.js',
'/msgbox/msgbox.js'
]);
}
]));
}

View File

@ -70,7 +70,7 @@ function createSourceEditor(style) {
update();
function update() {
linterEl.value = linterConfig.getDefault();
linterEl.value = linterConfig.getName();
const cssLintOption = linterEl.querySelector('[value="csslint"]');
if (cm.getOption('mode') !== 'css') {

View File

@ -15,6 +15,7 @@ function messageBox({
if (onshow) {
onshow(messageBox.element);
}
messageBox.element.focus();
return new Promise(_resolve => {
messageBox.resolve = _resolve;
});

View File

@ -10950,14 +10950,15 @@ if (!CSSLint.suppressUsoVarError) {
});
}
self.onmessage = ({data: {action, code, config}}) => {
self.onmessage = ({data: {action = 'run', code, config}}) => {
switch (action) {
case 'getRules':
self.postMessage(CSSLint.getRules());
case 'getAllRuleIds':
// the functions are non-tranferable and we need only an id
self.postMessage(CSSLint.getRules().map(rule => rule.id));
return;
case 'verify':
case 'run':
Object.defineProperty(config, 'errors', {get: () => 0, set: () => 0});
config['uso-vars'] = 1;
self.postMessage(CSSLint.verify(code, config).messages.map(m => {

View File

@ -1673,3 +1673,59 @@ N,R,y-N,"inline"])):(K.lastIndex=G+1,K.test(E),y=0===K.lastIndex?E.length-1:K.la
Object.keys(f).forEach(function(a){d[a]=f[a]});return d}if(a&&f)return k(a)(f);if("function"!==typeof a)throw new TypeError("need wrapper function");Object.keys(a).forEach(function(c){d[c]=a[c]});return d}l.exports=k},{}],611:[function(a,l,g){var k=a("fs"),h=a("path"),f=a("mkdirp");l.exports=function(a,c,b){var d=h.dirname(a);k.exists(d,function(g){g?k.writeFile(a,c,b):f(d,function(d){if(d)return b(d);k.writeFile(a,c,b)})})};l.exports.sync=function(a,c){var b=h.dirname(a);k.existsSync(b)||f.sync(b);
k.writeFileSync(a,c)};l.exports.stream=function(a){var c=h.dirname(a);k.existsSync(c)||f.sync(c);return k.createWriteStream(a)}},{fs:1,mkdirp:173,path:14}],stylelint:[function(a,l,g){g=a("./utils/checkAgainstRule");var k=a("./createPlugin"),h=a("./createStylelint"),f=a("./formatters"),d=a("./postcssPlugin"),c=a("./utils/report"),b=a("./utils/ruleMessages"),p=a("./rules"),m=a("./standalone");a=a("./utils/validateOptions");d.utils={report:c,ruleMessages:b,validateOptions:a,checkAgainstRule:g};d.lint=
m;d.rules=p;d.formatters=f;d.createPlugin=k;d.createLinter=h;l.exports=d},{"./createPlugin":334,"./createStylelint":335,"./formatters":338,"./postcssPlugin":346,"./rules":429,"./standalone":520,"./utils/checkAgainstRule":529,"./utils/report":592,"./utils/ruleMessages":593,"./utils/validateOptions":595}]},{},[]);
(() => {
const stylelint = require('stylelint');
self.onmessage = ({data: {action = 'run', code, config}}) => {
switch (action) {
case 'getAllRuleIds':
// the functions are non-tranferable
self.postMessage(Object.keys(stylelint.rules));
return;
case 'getAllRuleOptions':
self.postMessage(getAllRuleOptions());
return;
case 'run':
stylelint.lint({code, config}).then(results =>
self.postMessage(results));
return;
}
};
function getAllRuleOptions() {
const options = {};
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
const rxString = /"([-\w\s]{3,}?)"/g;
for (const id of Object.keys(stylelint.rules)) {
const ruleCode = String(stylelint.rules[id]);
const sets = [];
let m, mStr;
while ((m = rxPossible.exec(ruleCode))) {
const possible = m[1];
const set = [];
while ((mStr = rxString.exec(possible))) {
const s = mStr[1];
if (s.includes(' ')) {
set.push(...s.split(/\s+/));
} else {
set.push(s);
}
}
if (possible.includes('ignoreAtRules')) {
set.push('ignoreAtRules');
}
if (possible.includes('ignoreShorthands')) {
set.push('ignoreShorthands');
}
if (set.length) {
sets.push(set);
}
}
if (sets.length) {
options[id] = sets;
}
}
return options;
}
})();