From 2e66ecca18ed7ae6b65215c1cf6b932b63190e6e Mon Sep 17 00:00:00 2001 From: eight Date: Sat, 1 Sep 2018 18:19:39 +0800 Subject: [PATCH] Add: linter-config-dialog, cacheFn --- edit.html | 1 + edit/linter-config-dialog.js | 196 +++++++++++++++++++++++++++++++++++ edit/linter-report.js | 3 - edit/util.js | 13 +++ 4 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 edit/linter-config-dialog.js diff --git a/edit.html b/edit.html index da93b705..897e9601 100644 --- a/edit.html +++ b/edit.html @@ -95,6 +95,7 @@ + diff --git a/edit/linter-config-dialog.js b/edit/linter-config-dialog.js new file mode 100644 index 00000000..d4bde537 --- /dev/null +++ b/edit/linter-config-dialog.js @@ -0,0 +1,196 @@ +/* global cacheFn editorWorker stylelint csslint showCodeMirrorPopup loadScript messageBox */ +'use strict'; + +(() => { + document.addEventListener('DOMContentLoaded', () => { + $('#linter-settings').addEventListener('click', showLintConfig); + }, {once: true}); + + function stringifyConfig(config) { + return JSON.stringify(config, null, 2) + .replace(/,\n\s+\{\n\s+("severity":\s"\w+")\n\s+\}/g, ', {$1}'); + } + + function showLinterErrorMessage(title, contents, popup) { + messageBox({ + title, + contents, + className: 'danger center lint-config', + buttons: [t('confirmOK')], + }).then(() => popup && popup.codebox && popup.codebox.focus()); + } + + function showLintConfig() { + const linter = $('#editor.linter').value; + if (!linter) { + return; + } + const storageName = linter === 'styleint' ? 'editorStylelintConfig' : 'editorCSSLintConfig'; + const getRules = cacheFn(linter === 'stylelint' ? + editorWorker.getStylelintRules : editorWorker.getCsslintRules); + const linterTitle = linter === 'stylelint' ? 'Stylelint' : 'CSSLint'; + const defaultConfig = stringifyConfig( + linter === 'stylelint' ? stylelint.DEFAULT : csslint.DEFAULT + ); + const title = t('linterConfigPopupTitle', linterTitle); + const popup = showCodeMirrorPopup(title, null, { + lint: false, + extraKeys: {'Ctrl-Enter': save}, + hintOptions: {hint}, + }); + $('.contents', popup).appendChild(makeFooter()); + + let cm = popup.codebox; + cm.focus(); + chromeSync.getLZValue(storageName).then(config => { + cm.setValue(config ? stringifyConfig(config) : defaultConfig); + cm.clearHistory(); + cm.markClean(); + updateButtonState(); + }); + cm.on('changes', updateButtonState); + + cm.rerouteHotkeys(false); + window.addEventListener('closeHelp', function _() { + window.removeEventListener('closeHelp', _); + cm.rerouteHotkeys(true); + cm = null; + }); + + 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', true); + }); + + function findInvalidRules(config, linter) { + return getRules() + .then(rules => { + if (linter === 'stylelint') { + return Object.keys(config.rules).filter(k => !rules.hasOwnProperty(k)); + } + const ruleSet = new Set(rules.map(r => r.id)); + return Object.keys(config).filter(k => !ruleSet.has(k)); + }); + } + + function makeFooter() { + return $create('div', [ + $create('p', [ + $createLink( + linter === 'stylelint' + ? 'https://stylelint.io/user-guide/rules/' + : 'https://github.com/CSSLint/csslint/wiki/Rules-by-ID', + t('linterRulesLink')), + linter === 'csslint' ? ' ' + t('linterCSSLintSettings') : '', + ]), + $create('.buttons', [ + $create('button.save', {onclick: save, title: 'Ctrl-Enter'}, t('styleSaveLabel')), + $create('button.cancel', {onclick: cancel}, t('confirmClose')), + $create('button.reset', {onclick: reset, title: t('linterResetMessage')}, t('genericResetLabel')), + ]), + ]); + } + + function save(event) { + if (event instanceof Event) { + event.preventDefault(); + } + const json = tryJSONparse(cm.getValue()); + if (!json) { + showLinterErrorMessage(linter, t('linterJSONError'), popup); + cm.focus(); + return; + } + findInvalidRules(json, linter).then(invalid => { + if (invalid.length) { + showLinterErrorMessage(linter, [ + t('linterInvalidConfigError'), + $create('ul', invalid.map(name => $create('li', name))), + ], popup); + return; + } + chromeSync.setLZValue(storageName, json); + cm.markClean(); + cm.focus(); + updateButtonState(); + }); + } + + function reset(event) { + event.preventDefault(); + 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 getRules().then(rules => { + let ruleIds, options; + if (linter === 'stylelint') { + ruleIds = Object.keys(rules); + options = rules; + } else { + ruleIds = rules.map(r => r.id); + 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; + } + } +})(); diff --git a/edit/linter-report.js b/edit/linter-report.js index 4800223a..370c0d2a 100644 --- a/edit/linter-report.js +++ b/edit/linter-report.js @@ -33,7 +33,6 @@ var linterReport = (() => { // eslint-disable-line no-var document.addEventListener('DOMContentLoaded', () => { $('#lint-help').addEventListener('click', helpDialog.show); - $('#linter-settings').addEventListener('click', showLintConfig); }, {once: true}); return {refresh}; @@ -152,6 +151,4 @@ var linterReport = (() => { // eslint-disable-line no-var cm.focus(); cm.setSelection(anno.from); } - - function showLintConfig() {} })(); diff --git a/edit/util.js b/edit/util.js index f44d5d95..a796067b 100644 --- a/edit/util.js +++ b/edit/util.js @@ -121,3 +121,16 @@ function sectionsToMozFormat(style) { function clipString(str, limit = 100) { return str.length <= limit ? str : str.substr(0, limit) + '...'; } + +// cache the first call +function cacheFn(fn) { + let cached = false; + let result; + return (...args) => { + if (!cached) { + result = fn(...args); + cached = true; + } + return result; + }; +}