diff --git a/edit/edit.js b/edit/edit.js index f13c300c..08cab747 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -103,7 +103,6 @@ lazyInit(); await editor.ready; editor.ready = true; - setTimeout(() => editor.getEditors().forEach(linter.enableForEditor)); // enabling after init to prevent flash of validation failure on an empty name $('#name').required = !editor.isUsercss; $('#save-button').onclick = editor.save; diff --git a/edit/editor-worker.js b/edit/editor-worker.js index 9a0ec6fd..559f9aaf 100644 --- a/edit/editor-worker.js +++ b/edit/editor-worker.js @@ -4,6 +4,7 @@ importScripts('/js/worker-util.js'); const {loadScript} = workerUtil; +/** @namespace EditorWorker */ workerUtil.createAPI({ csslint: (code, config) => { loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js'); diff --git a/edit/linter.js b/edit/linter.js index 52a525b8..35ad28a0 100644 --- a/edit/linter.js +++ b/edit/linter.js @@ -2,6 +2,7 @@ 'use strict'; /* exported editorWorker */ +/** @type {EditorWorker} */ const editorWorker = workerUtil.createWorker({ url: '/edit/editor-worker.js', }); @@ -14,20 +15,57 @@ const linter = (() => { const cms = new Set(); return { - register, - run, - enableForEditor, - disableForEditor, - onLintingUpdated, - onUnhook, + disableForEditor(cm) { + cm.setOption('lint', false); + cms.delete(cm); + for (const cb of unhookListeners) { + cb(cm); + } + }, + /** + * @param {Object} cm + * @param {string} [code] - to be used to avoid slowdowns when creating a lot of cms. + * Enables lint option only if there are problems, thus avoiding a _very_ costly layout + * update when lint gutter is added to a lot of editors simultaneously. + */ + enableForEditor(cm, code) { + if (code) return enableOnProblems(cm, code); + cm.setOption('lint', {getAnnotations, onUpdateLinting}); + cms.add(cm); + }, + onLintingUpdated(cb) { + lintingUpdatedListeners.push(cb); + }, + onUnhook(cb) { + unhookListeners.push(cb); + }, + register(linterFn) { + linters.push(linterFn); + }, + run() { + for (const cm of cms) { + cm.performLint(); + } + }, }; - function onUnhook(cb) { - unhookListeners.push(cb); + async function enableOnProblems(cm, code) { + const results = await getAnnotations(code, {}, cm); + if (results.length) { + cms.add(cm); + cm.setOption('lint', { + getAnnotations() { + cm.options.lint.getAnnotations = getAnnotations; + return results; + }, + onUpdateLinting, + }); + } } - function onLintingUpdated(cb) { - lintingUpdatedListeners.push(cb); + async function getAnnotations(...args) { + const results = await Promise.all(linters.map(fn => fn(...args))); + return [].concat(...results.filter(Boolean)); } function onUpdateLinting(...args) { @@ -35,32 +73,4 @@ const linter = (() => { cb(...args); } } - - function enableForEditor(cm) { - cm.setOption('lint', {onUpdateLinting, getAnnotations}); - cms.add(cm); - } - - function disableForEditor(cm) { - cm.setOption('lint', false); - cms.delete(cm); - for (const cb of unhookListeners) { - cb(cm); - } - } - - function register(linterFn) { - linters.push(linterFn); - } - - function run() { - for (const cm of cms) { - cm.performLint(); - } - } - - function getAnnotations(...args) { - return Promise.all(linters.map(fn => fn(...args))) - .then(results => [].concat(...results.filter(Boolean))); - } })(); diff --git a/edit/sections-editor.js b/edit/sections-editor.js index 0bda2466..4130b7c3 100644 --- a/edit/sections-editor.js +++ b/edit/sections-editor.js @@ -487,7 +487,7 @@ function SectionsEditor() { livePreview.update(getModel()); } - function initSections(src, { + async function initSections(src, { focusOn = 0, replace = false, pristine = false, @@ -497,10 +497,6 @@ function SectionsEditor() { sections.length = 0; container.textContent = ''; } - let done; - let index = 0; - let y = 0; - const total = src.length; let si = editor.scrollInfo; if (si && si.cms && si.cms.length === src.length) { si.scrollY2 = si.scrollY + window.innerHeight; @@ -510,29 +506,27 @@ function SectionsEditor() { } else { si = null; } - return new Promise(resolve => { - done = resolve; - chunk(!si); - }); - function chunk(forceRefresh) { - const t0 = performance.now(); - while (index < total && performance.now() - t0 < 100) { - if (si) forceRefresh = y < si.scrollY2 && (y += si.cms[index].parentHeight) > si.scrollY; - insertSectionAfter(src[index], undefined, forceRefresh, si && si.cms[index]); - if (pristine) dirty.clear(); - if (index === focusOn && !si) sections[index].cm.focus(); - index++; - } - setGlobalProgress(index, total); - if (index === total) { - setGlobalProgress(); - if (!si) requestAnimationFrame(fitToAvailableSpace); - container.style.removeProperty('height'); - done(); - } else { - setTimeout(chunk); + let forceRefresh = true; + let y = 0; + let tPrev; + for (let i = 0; i < src.length; i++) { + const t = performance.now(); + if (!tPrev) { + tPrev = t; + } else if (t - tPrev > 100) { + tPrev = 0; + forceRefresh = false; + await new Promise(setTimeout); } + if (si) forceRefresh = y < si.scrollY2 && (y += si.cms[i].parentHeight) > si.scrollY; + insertSectionAfter(src[i], null, forceRefresh, si && si.cms[i]); + setGlobalProgress(i, src.length); + if (pristine) dirty.clear(); + if (i === focusOn && !si) sections[i].cm.focus(); } + if (!si) requestAnimationFrame(fitToAvailableSpace); + container.style.removeProperty('height'); + setGlobalProgress(); } /** @param {EditorSection} section */ @@ -584,23 +578,23 @@ function SectionsEditor() { } const section = createSection(init, genId, si); const {cm} = section; + const {code} = init; const index = base ? sections.indexOf(base) + 1 : sections.length; sections.splice(index, 0, section); container.insertBefore(section.el, base ? base.el.nextSibling : null); - refreshOnView(cm, base || forceRefresh); + refreshOnView(cm, {code, force: base || forceRefresh}); registerEvents(section); - if ((!si || !si.height) && (!base || init.code)) { + if ((!si || !si.height) && (!base || code)) { // Fit a) during startup or b) when the clone button is clicked on a section with some code fitToContent(section); } if (base) { cm.focus(); editor.scrollToEditor(cm); - linter.enableForEditor(cm); } updateSectionOrder(); - section.onChange(updateLivePreview); updateLivePreview(); + section.onChange(updateLivePreview); } /** @param {EditorSection} section */ @@ -654,10 +648,12 @@ function SectionsEditor() { } } - function refreshOnView(cm, force) { - return force || !xo ? - cm.refresh() : + function refreshOnView(cm, {code, force} = {}) { + if (force || !xo) { + refreshOnViewNow(cm, code); + } else { xo.observe(cm.display.wrapper); + } } /** @param {IntersectionObserverEntry[]} entries */ @@ -668,14 +664,19 @@ function SectionsEditor() { xo.unobserve(e.target); const cm = e.target.CodeMirror; if (r.bottom > 0 && r.top < window.innerHeight) { - cm.refresh(); + refreshOnViewNow(cm); } else { - setTimeout(() => cm.refresh()); + setTimeout(refreshOnViewNow, 0, cm); } } } } + async function refreshOnViewNow(cm, code) { + cm.refresh(); + linter.enableForEditor(cm, code); + } + function toggleContextMenuDelete(event) { if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) { chrome.contextMenus.update('editor.contextDelete', { diff --git a/edit/source-editor.js b/edit/source-editor.js index c2b1cd93..b262bbdc 100644 --- a/edit/source-editor.js +++ b/edit/source-editor.js @@ -91,6 +91,7 @@ function SourceEditor() { linter.run(); updateLinterSwitch(); }); + setTimeout(linter.enableForEditor, 0, cm); if (!$.isTextInput(document.activeElement)) { cm.focus(); }