diff --git a/edit/applies-to-line-widget.js b/edit/applies-to-line-widget.js
index df83dcfa..af0df7be 100644
--- a/edit/applies-to-line-widget.js
+++ b/edit/applies-to-line-widget.js
@@ -179,8 +179,11 @@ function createAppliesToLineWidget(cm) {
     fromLine = Math.max(fromLine || 0, cm.display.viewFrom);
     toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo || toLine);
     const visible = {fromLine, toLine};
+    const {curOp} = cm;
     if (fromLine >= cm.display.viewFrom && toLine <= (cm.display.viewTo || toLine)) {
-      cm.operation(doUpdate);
+      if (!curOp) cm.startOperation();
+      doUpdate();
+      if (!curOp) cm.endOperation();
     }
     if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) {
       setTimeout(updateInvisible, 0, changed, visible);
diff --git a/edit/colorpicker-helper.js b/edit/colorpicker-helper.js
index 547bd1c1..d368a7fd 100644
--- a/edit/colorpicker-helper.js
+++ b/edit/colorpicker-helper.js
@@ -2,7 +2,6 @@
 'use strict';
 
 onDOMscriptReady('/colorview.js').then(() => {
-  initOverlayHooks();
   onDOMready().then(() => {
     $('#colorpicker-settings').onclick = configureColorpicker;
   });
@@ -23,7 +22,7 @@ onDOMscriptReady('/colorview.js').then(() => {
       defaults.colorpicker = {
         forceUpdate: editors.length > 0,
         tooltip: t('colorpickerTooltip'),
-        popupOptions: {
+        popup: {
           tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
           hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
           hideDelay: 5000,
@@ -35,7 +34,6 @@ onDOMscriptReady('/colorview.js').then(() => {
         },
       };
     } else {
-      CodeMirror.modeExtensions.css.unregisterColorviewHooks();
       if (defaults.extraKeys) {
         delete defaults.extraKeys[keyName];
       }
@@ -114,47 +112,4 @@ onDOMscriptReady('/colorview.js').then(() => {
     }
     input.focus();
   }
-
-  function initOverlayHooks() {
-    const COLORVIEW_DISABLED_SUFFIX = ' colorview-disabled';
-    const COLORVIEW_NEXT_DISABLED_SUFFIX = ' colorview-next-disabled';
-    const originalAddOverlay = CodeMirror.prototype.addOverlay;
-    CodeMirror.prototype.addOverlay = addOverlayHook;
-
-    function addOverlayHook(overlay) {
-      if (overlay.token !== tokenHook && (
-          overlay === (this.state.matchHighlighter || {}).overlay ||
-          overlay === (this.state.search || {}).overlay)) {
-        overlay.colopickerHelper = {token: overlay.token};
-        overlay.token = tokenHook;
-      }
-      originalAddOverlay.apply(this, arguments);
-    }
-
-    function tokenHook(stream) {
-      const style = this.colopickerHelper.token.apply(this, arguments);
-      if (!style) {
-        return style;
-      }
-      const {start, pos, lineOracle: {baseTokens}} = stream;
-      if (!baseTokens) {
-        return style;
-      }
-      for (let prev = 0, i = 1; i < baseTokens.length; i += 2) {
-        const end = baseTokens[i];
-        if (prev <= start && start <= end) {
-          const base = baseTokens[i + 1];
-          if (base && base.includes('colorview')) {
-            return style +
-              (start > prev ? COLORVIEW_DISABLED_SUFFIX : '') +
-              (pos < end ? COLORVIEW_NEXT_DISABLED_SUFFIX : '');
-          }
-        } else if (end > pos) {
-          break;
-        }
-        prev = end;
-      }
-      return style;
-    }
-  }
 });
diff --git a/edit/source-editor.js b/edit/source-editor.js
index 4be93c9e..da42c394 100644
--- a/edit/source-editor.js
+++ b/edit/source-editor.js
@@ -29,20 +29,15 @@ function createSourceEditor(style) {
     style = deepCopy(style);
   }
 
-  const cm = CodeMirror($('.single-editor'));
+  const cm = CodeMirror($('.single-editor'), {value: style.sourceCode});
   editors.push(cm);
+  savedGeneration = cm.changeGeneration();
+
+  cm.operation(initAppliesToLineWidget);
   updateMeta().then(() => {
     initLint();
     initLinterSwitch();
-
-    cm.setValue(style.sourceCode);
-    cm.clearHistory();
-    cm.markClean();
-    savedGeneration = cm.changeGeneration();
-
     initHooks();
-    initAppliesToLineWidget();
-
     setTimeout(() => {
       if ((document.activeElement || {}).localName !== 'input') {
         cm.focus();
diff --git a/manage/config-dialog.css b/manage/config-dialog.css
index 46f5d4c3..9a895641 100644
--- a/manage/config-dialog.css
+++ b/manage/config-dialog.css
@@ -11,11 +11,16 @@
 }
 
 .config-heading {
-  float: right;
-  margin: -1.25rem 0 0 0;
+  top: -1em;
+  position: relative;
+  text-align: right;
   font-size: 0.9em;
 }
 
+#stylus-popup .config-heading {
+  top: -.25em;
+}
+
 .config-body label {
   display: flex;
   padding: .75em 0;
@@ -97,6 +102,10 @@
   visibility: hidden;
 }
 
+.config-reset-icon {
+  height: 16px;
+}
+
 .config-reset-icon .svg-icon {
   cursor: pointer;
   fill: #aaa;
@@ -140,14 +149,22 @@
   animation: fadein .5s;
 }
 
-.cm-colorview::before,
-.color-swatch {
-  width: var(--onoffswitch-width) !important;
-  height: 20px !important;
+#message-box .colorview-swatch {
+  padding: 0;
+  box-sizing: content-box;
 }
 
-.cm-colorview::before {
-  margin: 1px !important;
+#message-box .colorview-swatch,
+#message-box .colorview-swatch::before,
+#message-box .colorview-swatch::after,
+.color-swatch {
+  width: var(--onoffswitch-width);
+  height: 20px;
+  left: 0;
+  margin: 0;
+  border: 1px solid transparent;
+  box-sizing: content-box;
+  background-position: unset;
 }
 
 .color-swatch {
@@ -157,6 +174,7 @@
   border: 1px solid gray;
   cursor: pointer;
   opacity: 1;
+  z-index: 2;
 }
 
 .colorpicker-popup {
diff --git a/manage/config-dialog.js b/manage/config-dialog.js
index 503c5b39..04fe1128 100644
--- a/manage/config-dialog.js
+++ b/manage/config-dialog.js
@@ -210,7 +210,7 @@ function configDialog(style) {
       switch (va.type) {
         case 'color':
           children = [
-            $create('.cm-colorview.config-value', [
+            $create('.colorview-swatch.config-value', [
               va.input = $create('a.color-swatch', {
                 va,
                 href: '#',
diff --git a/vendor-overwrites/colorpicker/colorpicker.css b/vendor-overwrites/colorpicker/colorpicker.css
index 4f6bb9d9..f3cb9b26 100644
--- a/vendor-overwrites/colorpicker/colorpicker.css
+++ b/vendor-overwrites/colorpicker/colorpicker.css
@@ -1,39 +1,38 @@
 /* codemirror colorview */
 
-.cm-colorview {
-  position: relative;
-  white-space: nowrap;
-}
-
-.cm-colorview:not(.cm-colorview-disabled)::before {
-  content: "";
+.colorview-swatch {
+  padding-left: 14px;
   position: relative;
   display: inline-block;
-  box-sizing: content-box;
-  margin: 0 3px;
-  width: 8px;
-  height: 8px;
-  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC");
-  background-repeat: repeat;
-}
-.CodeMirror-lint-mark-warning + .cm-colorview::before,
-.cm-colorview-next-disabled + .cm-colorview::before {
-  content: none;
 }
 
-.codemirror-colorview-background {
+.colorview-swatch::before,
+.colorview-swatch::after {
+  content: "";
   position: absolute;
+  display: inline-block;
   left: 2px;
-  top: 2px;
+  top: 0;
+  bottom: 0;
+  margin: auto;
   width: 10px;
   height: 10px;
   box-sizing: border-box;
-  border: 1px solid #8e8e8e;
-  content: "";
-  cursor: pointer;
 }
 
-.codemirror-colorview-background:hover {
+.colorview-swatch::before {
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAJElEQVQYV2NctWrVfwYkEBYWxojMZ6SDAmT7QGx0K1EcRBsFAADeG/3M/HteAAAAAElFTkSuQmCC");
+  background-repeat: repeat;
+  background-position: center;
+}
+
+.colorview-swatch::after {
+  border: 1px solid #8e8e8e;
+  cursor: pointer;
+  background-color: var(--colorview-swatch);
+}
+
+.colorview-swatch:hover::after {
   border-color: #494949;
 }
 
@@ -180,6 +179,7 @@
   -webkit-box-sizing: border-box;
   -moz-box-sizing: border-box;
   box-sizing: border-box;
+  border: 1px solid var(--input-border-color);
 }
 
 .colorpicker-empty {
diff --git a/vendor-overwrites/colorpicker/colorpicker.js b/vendor-overwrites/colorpicker/colorpicker.js
index a6f8ce6f..59cc8095 100644
--- a/vendor-overwrites/colorpicker/colorpicker.js
+++ b/vendor-overwrites/colorpicker/colorpicker.js
@@ -1,6 +1,159 @@
 /* global CodeMirror */
 'use strict';
 
+const NAMED_COLORS = new Map([
+  ['transparent', 'rgba(0, 0, 0, 0)'],
+  // CSS4 named colors
+  ['aliceblue', '#f0f8ff'],
+  ['antiquewhite', '#faebd7'],
+  ['aqua', '#00ffff'],
+  ['aquamarine', '#7fffd4'],
+  ['azure', '#f0ffff'],
+  ['beige', '#f5f5dc'],
+  ['bisque', '#ffe4c4'],
+  ['black', '#000000'],
+  ['blanchedalmond', '#ffebcd'],
+  ['blue', '#0000ff'],
+  ['blueviolet', '#8a2be2'],
+  ['brown', '#a52a2a'],
+  ['burlywood', '#deb887'],
+  ['cadetblue', '#5f9ea0'],
+  ['chartreuse', '#7fff00'],
+  ['chocolate', '#d2691e'],
+  ['coral', '#ff7f50'],
+  ['cornflowerblue', '#6495ed'],
+  ['cornsilk', '#fff8dc'],
+  ['crimson', '#dc143c'],
+  ['cyan', '#00ffff'],
+  ['darkblue', '#00008b'],
+  ['darkcyan', '#008b8b'],
+  ['darkgoldenrod', '#b8860b'],
+  ['darkgray', '#a9a9a9'],
+  ['darkgrey', '#a9a9a9'],
+  ['darkgreen', '#006400'],
+  ['darkkhaki', '#bdb76b'],
+  ['darkmagenta', '#8b008b'],
+  ['darkolivegreen', '#556b2f'],
+  ['darkorange', '#ff8c00'],
+  ['darkorchid', '#9932cc'],
+  ['darkred', '#8b0000'],
+  ['darksalmon', '#e9967a'],
+  ['darkseagreen', '#8fbc8f'],
+  ['darkslateblue', '#483d8b'],
+  ['darkslategray', '#2f4f4f'],
+  ['darkslategrey', '#2f4f4f'],
+  ['darkturquoise', '#00ced1'],
+  ['darkviolet', '#9400d3'],
+  ['deeppink', '#ff1493'],
+  ['deepskyblue', '#00bfff'],
+  ['dimgray', '#696969'],
+  ['dimgrey', '#696969'],
+  ['dodgerblue', '#1e90ff'],
+  ['firebrick', '#b22222'],
+  ['floralwhite', '#fffaf0'],
+  ['forestgreen', '#228b22'],
+  ['fuchsia', '#ff00ff'],
+  ['gainsboro', '#dcdcdc'],
+  ['ghostwhite', '#f8f8ff'],
+  ['gold', '#ffd700'],
+  ['goldenrod', '#daa520'],
+  ['gray', '#808080'],
+  ['grey', '#808080'],
+  ['green', '#008000'],
+  ['greenyellow', '#adff2f'],
+  ['honeydew', '#f0fff0'],
+  ['hotpink', '#ff69b4'],
+  ['indianred', '#cd5c5c'],
+  ['indigo', '#4b0082'],
+  ['ivory', '#fffff0'],
+  ['khaki', '#f0e68c'],
+  ['lavender', '#e6e6fa'],
+  ['lavenderblush', '#fff0f5'],
+  ['lawngreen', '#7cfc00'],
+  ['lemonchiffon', '#fffacd'],
+  ['lightblue', '#add8e6'],
+  ['lightcoral', '#f08080'],
+  ['lightcyan', '#e0ffff'],
+  ['lightgoldenrodyellow', '#fafad2'],
+  ['lightgray', '#d3d3d3'],
+  ['lightgrey', '#d3d3d3'],
+  ['lightgreen', '#90ee90'],
+  ['lightpink', '#ffb6c1'],
+  ['lightsalmon', '#ffa07a'],
+  ['lightseagreen', '#20b2aa'],
+  ['lightskyblue', '#87cefa'],
+  ['lightslategray', '#778899'],
+  ['lightslategrey', '#778899'],
+  ['lightsteelblue', '#b0c4de'],
+  ['lightyellow', '#ffffe0'],
+  ['lime', '#00ff00'],
+  ['limegreen', '#32cd32'],
+  ['linen', '#faf0e6'],
+  ['magenta', '#ff00ff'],
+  ['maroon', '#800000'],
+  ['mediumaquamarine', '#66cdaa'],
+  ['mediumblue', '#0000cd'],
+  ['mediumorchid', '#ba55d3'],
+  ['mediumpurple', '#9370db'],
+  ['mediumseagreen', '#3cb371'],
+  ['mediumslateblue', '#7b68ee'],
+  ['mediumspringgreen', '#00fa9a'],
+  ['mediumturquoise', '#48d1cc'],
+  ['mediumvioletred', '#c71585'],
+  ['midnightblue', '#191970'],
+  ['mintcream', '#f5fffa'],
+  ['mistyrose', '#ffe4e1'],
+  ['moccasin', '#ffe4b5'],
+  ['navajowhite', '#ffdead'],
+  ['navy', '#000080'],
+  ['oldlace', '#fdf5e6'],
+  ['olive', '#808000'],
+  ['olivedrab', '#6b8e23'],
+  ['orange', '#ffa500'],
+  ['orangered', '#ff4500'],
+  ['orchid', '#da70d6'],
+  ['palegoldenrod', '#eee8aa'],
+  ['palegreen', '#98fb98'],
+  ['paleturquoise', '#afeeee'],
+  ['palevioletred', '#db7093'],
+  ['papayawhip', '#ffefd5'],
+  ['peachpuff', '#ffdab9'],
+  ['peru', '#cd853f'],
+  ['pink', '#ffc0cb'],
+  ['plum', '#dda0dd'],
+  ['powderblue', '#b0e0e6'],
+  ['purple', '#800080'],
+  ['rebeccapurple', '#663399'],
+  ['red', '#ff0000'],
+  ['rosybrown', '#bc8f8f'],
+  ['royalblue', '#4169e1'],
+  ['saddlebrown', '#8b4513'],
+  ['salmon', '#fa8072'],
+  ['sandybrown', '#f4a460'],
+  ['seagreen', '#2e8b57'],
+  ['seashell', '#fff5ee'],
+  ['sienna', '#a0522d'],
+  ['silver', '#c0c0c0'],
+  ['skyblue', '#87ceeb'],
+  ['slateblue', '#6a5acd'],
+  ['slategray', '#708090'],
+  ['slategrey', '#708090'],
+  ['snow', '#fffafa'],
+  ['springgreen', '#00ff7f'],
+  ['steelblue', '#4682b4'],
+  ['tan', '#d2b48c'],
+  ['teal', '#008080'],
+  ['thistle', '#d8bfd8'],
+  ['tomato', '#ff6347'],
+  ['turquoise', '#40e0d0'],
+  ['violet', '#ee82ee'],
+  ['wheat', '#f5deb3'],
+  ['white', '#ffffff'],
+  ['whitesmoke', '#f5f5f5'],
+  ['yellow', '#ffff00'],
+  ['yellowgreen', '#9acd32'],
+]);
+
 (window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () {
   const cm = this;
   const CSS_PREFIX = 'colorpicker-';
@@ -57,6 +210,8 @@
     hide,
     setColor,
     getColor,
+    stringToColor,
+    colorToString,
     options,
   };
   return PUBLIC_API;
@@ -554,11 +709,16 @@
 
   function onMouseUp(event) {
     releaseMouse(event, ['saturation', 'hue', 'opacity']);
+    if (onMouseDown.outsideClick) {
+      if (!prevFocusedElement) hide();
+    }
   }
 
   function onMouseDown(event) {
-    if (event.button === 0 && !event.target.closest('.colorpicker-popup')) {
-      hide();
+    onMouseDown.outsideClick = !event.button && !event.target.closest('.colorpicker-popup');
+    if (onMouseDown.outsideClick) {
+      prevFocusedElement = null;
+      captureMouse(event);
     }
   }
 
@@ -607,6 +767,10 @@
   function onCloseRequest(event) {
     if (event.detail !== PUBLIC_API) {
       hide();
+    } else if (!prevFocusedElement) {
+      // we're between mousedown and mouseup and colorview wants to re-open us in this cm
+      // so we'll prevent onMouseUp from hiding us to avoid flicker
+      prevFocusedElement = cm.display.input;
     }
   }
 
@@ -689,7 +853,7 @@
   function unregisterEvents() {
     window.removeEventListener('keydown', onKeyDown, true);
     window.removeEventListener('mousedown', onMouseDown, true);
-    window.removeEventListener('close-colorpicker-popup', hide, true);
+    window.removeEventListener('close-colorpicker-popup', onCloseRequest, true);
     $root.removeEventListener('mouseleave', snooze);
     $root.removeEventListener('mouseenter', stopSnoozing);
     $root.removeEventListener('input', setFromInputs);
@@ -709,9 +873,13 @@
   //endregion
   //region Color conversion utilities
 
-  function colorToString({r, g, b, h, s, l, a}, type = currentFormat) {
-    a = alphaToString(a);
+  function colorToString(color, type = currentFormat) {
+    const a = alphaToString(color.a);
     const hasA = Boolean(a);
+    if (type === 'rgb' && color.type === 'hsl') {
+      color = HSVtoRGB(HSLtoHSV(color));
+    }
+    const {r, g, b, h, s, l} = color;
     switch (type) {
       case 'hex': {
         const rgbStr = (0x1000000 + (r << 16) + (g << 8) + (b | 0)).toString(16).slice(1);
@@ -721,8 +889,8 @@
       }
       case 'rgb':
         return hasA ?
-          `rgba(${r}, ${g}, ${b}, ${a})` :
-          `rgb(${r}, ${g}, ${b})`;
+          `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${a})` :
+          `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
       case 'hsl':
         return hasA ?
           `hsla(${h}, ${s}%, ${l}%, ${a})` :
@@ -731,25 +899,47 @@
   }
 
   function stringToColor(str) {
-    if (typeof str !== 'string') {
-      return;
-    }
+    if (typeof str !== 'string') return;
     str = str.trim();
-    if (str.startsWith('rgb')) {
-      const [r, g, b, a = 1] = str.replace(/rgba?\(|\)/g, '').split(',').map(parseFloat);
-      return {type: 'rgb', r, g, b, a};
+    if (!str) return;
+
+    if (str[0] !== '#' && !str.includes('(')) {
+      str = NAMED_COLORS.get(str);
+      if (!str) return;
     }
-    if (str.startsWith('hsl')) {
-      const [h, s, l, a = 1] = str.replace(/hsla?\(|\)/g, '').split(',').map(parseFloat);
-      return {type: 'hsl', h, s, l, a};
-    }
-    if (str.startsWith('#')) {
+
+    if (str[0] === '#') {
       str = str.slice(1);
       const [r, g, b, a = 255] = str.length <= 4 ?
         str.match(/(.)/g).map(c => parseInt(c + c, 16)) :
         str.match(/(..)/g).map(c => parseInt(c, 16));
       return {type: 'hex', r, g, b, a: a === 255 ? undefined : a / 255};
     }
+
+    const [, type, value] = str.match(/^(rgb|hsl)a?\((.*?)\)|$/i);
+    if (!type) return;
+
+    const comma = value.includes(',') && !value.includes('/');
+    const num = value.split(comma ? /\s*,\s*/ : /\s+(?!\/)|\s*\/\s*/);
+    if (num.length < 3 || num.length > 4) return;
+
+    let a = !num[3] ? 1 : parseFloat(num[3]) / (num[3].endsWith('%') ? 100 : 1);
+    if (isNaN(a)) a = 1;
+
+    const first = num[0];
+    if (/rgb/i.test(type)) {
+      const k = first.endsWith('%') ? 2.55 : 1;
+      const [r, g, b] = num.map(s => parseFloat(s) * k);
+      return {type: 'rgb', r, g, b, a};
+    } else {
+      let h = parseFloat(first);
+      if (first.endsWith('grad')) h *= 360 / 400;
+      else if (first.endsWith('rad')) h *= 180 / Math.PI;
+      else if (first.endsWith('turn')) h *= 360;
+      const s = parseFloat(num[1]);
+      const l = parseFloat(num[2]);
+      return {type: 'hsl', h, s, l, a};
+    }
   }
 
   function constrainHue(h) {
diff --git a/vendor-overwrites/colorpicker/colorview.js b/vendor-overwrites/colorpicker/colorview.js
index 59aa3577..48fe3bb0 100644
--- a/vendor-overwrites/colorpicker/colorview.js
+++ b/vendor-overwrites/colorpicker/colorview.js
@@ -1,527 +1,676 @@
-/* global CodeMirror */
+/* global CodeMirror NAMED_COLORS */
 'use strict';
 
 (() => {
-  const OWN_TOKEN_NAME = 'colorview';
-  const OWN_TOKEN_CLASS = 'cm-' + OWN_TOKEN_NAME;
-  const OWN_BACKGROUND_CLASS = 'codemirror-colorview-background';
+  //region Constants
 
-  const DISABLED_TOKEN_NAME = 'colorview-disabled';
-  const DISABLED_NEXT_TOKEN_NAME = 'colorview-next-disabled';
-  const DISABLED_TOKEN_CLASS = 'cm-' + DISABLED_TOKEN_NAME;
-  const DISABLED_NEXT_TOKEN_CLASS = 'cm-' + DISABLED_NEXT_TOKEN_NAME;
+  const COLORVIEW_CLASS = 'colorview';
+  const COLORVIEW_SWATCH_CLASS = COLORVIEW_CLASS + '-swatch';
+  const COLORVIEW_SWATCH_CSS = `--${COLORVIEW_SWATCH_CLASS}:`;
 
-  const HOOKED_TOKEN = new Map([
-    ['atom', colorizeAtom],
-    ['keyword', colorizeKeyword],
-  ].map(([name, fn]) => [name, {override: name + ' ' + OWN_TOKEN_NAME, process: fn}]));
+  const CLOSE_POPUP_EVENT = 'close-colorpicker-popup';
 
-  const NAMED_COLORS = getNamedColorsMap();
-  const TRANSPARENT = {
-    color: 'transparent',
-    colorValue: 'rgba(0, 0, 0, 0)', // as per the CSS spec
-  };
+  const RXS_NUM = /\s*(\d+\.?\d*|\d*\.\d+)(?:e\d+)?/.source;
   const RX_COLOR = {
-    hex: /#(?:[a-f\d]{3,4}|[a-f\d]{6}|[a-f\d]{8})\b/yi,
-    rgb: /rgb\((?:\s*\d{1,3}\s*,\s*){2}\d{1,3}\s*\)/yi,
-    rgba: /rgba\((?:\s*\d{1,3}\s*,\s*){3}\d*\.?\d+\s*\)/yi,
-    hsl: /hsl\(\s*(?:-?\d+|-?\d*\.\d+)\s*(?:,\s*(?:-?\d+|-?\d*\.\d+)%\s*){2}\)/yi,
-    hsla: /hsla\(\s*(?:-?\d+|-?\d*\.\d+)\s*(?:,\s*(?:-?\d+|-?\d*\.\d+)%\s*){2},\s*(?:-?\d+|-?\d*\.\d+)\s*\)/yi,
-  };
+    hex: /#(?:[a-f\d]{3}(?:[a-f\d](?:[a-f\d]{2}){0,2})?)\b/iy,
 
-  const CodeMirrorEvents = {
-    update(cm) {
-      if (cm.state.colorpicker.cache.size) {
-        renderVisibleTokens(cm);
-      }
+    rgb: new RegExp([
+      // num, num, num [ , num_or_pct]?
+      // pct, pct, pct [ , num_or_pct]?
+      `^((${RXS_NUM}\\s*(,|$)){3}|(${RXS_NUM}%\\s*(,|$)){3})(${RXS_NUM}%?)?\\s*$`,
+      // num num num [ / num_or_pct]?
+      // pct pct pct [ / num_or_pct]?
+      `^((${RXS_NUM}\\s*(\\s|$)){3}|(${RXS_NUM}%\\s*(\\s|$)){3})(/${RXS_NUM}%?)?\\s*$`,
+    ].join('|'), 'iy'),
+
+    hsl: new RegExp([
+      // num_or_angle, pct, pct [ , num_or_pct]?
+      `^(${RXS_NUM}(|deg|g?rad|turn)\\s*),(${RXS_NUM}%\\s*(,|$)){2}(${RXS_NUM}%?)?\\s*$`,
+      // num_or_angle pct pct [ / num_or_pct]?
+      `^(${RXS_NUM}(|deg|g?rad|turn)\\s*)\\s(${RXS_NUM}%\\s*(\\s|$)){2}(/${RXS_NUM}%?)?\\s*$`,
+    ].join('|'), 'iy'),
+
+    unsupported: new RegExp([
+      !CSS.supports('color', '#abcd') && /#(.{4}){1,2}$/,
+      !CSS.supports('color', 'rgb(1e2,0,0)') && /\de/,
+      !CSS.supports('color', 'rgb(1.5,0,0)') && /^rgba?\((([^,]+,){0,2}[^,]*\.|(\s*\S+\s+){0,2}\S*\.)/,
+      !CSS.supports('color', 'rgb(1,2,3,.5)') && /[^a]\(([^,]+,){3}/,
+      !CSS.supports('color', 'rgb(1,2,3,50%)') && /\((([^,]+,){3}|(\s*\S+[\s/]+){3}).*?%/,
+      !CSS.supports('color', 'rgb(1 2 3 / 1)') && /^[^,]+$/,
+      !CSS.supports('color', 'hsl(1turn, 2%, 3%)') && /deg|g?rad|turn/,
+    ].filter(Boolean).map(rx => rx.source).join('|') || '^$', 'i'),
+  };
+  const RX_DETECT = new RegExp('(^|[\\s():,/])(' + RX_COLOR.hex.source +
+    '|(?:rgb|hsl)a?(?=\\()|(?:' + [...NAMED_COLORS.keys()].join('|') + ')(?=[\\s;()/]|$))', 'gi');
+  const RX_DETECT_FUNC = /(rgb|hsl)a?\(/iy;
+
+  const RX_COMMENT = /\/\*(?:.(?!\*\/))*(?:.?\*\/|$)/g;
+  const SPACE1K = ' '.repeat(1000);
+
+  // milliseconds to work on invisible colors per one run
+  const TIME_BUDGET = 50;
+
+  // on initial paint the view doesn't have a size yet
+  // so we process the maximum number of lines that can fit in the window
+  let maxRenderChunkSize = Math.ceil(window.innerHeight / 14);
+
+  //endregion
+  //region CodeMirror Events
+
+  const CM_EVENTS = {
+    changes(cm, info) {
+      colorizeChanges(cm.state.colorpicker, info);
     },
-    keyup(cm) {
-      const popup = cm.state.colorpicker.popup;
-      if (popup && popup.options.isShortCut === false) {
-        popup.hide();
-      }
+    update(cm) {
+      const textHeight = cm.display.cachedTextHeight;
+      const height = cm.display.lastWrapHeight;
+      if (!height || !textHeight) return;
+      const numLines = Math.ceil(height / textHeight);
+      if (numLines >= maxRenderChunkSize) return;
+      maxRenderChunkSize = numLines;
+      cm.off('update', CM_EVENTS.update);
     },
     mousedown(cm, event) {
-      const self = cm.state.colorpicker;
-      const isMarker = event.button === 0 && event.target.classList.contains(OWN_BACKGROUND_CLASS);
-      window.dispatchEvent(new CustomEvent('close-colorpicker-popup', {detail: isMarker && self.popup}));
-      if (isMarker) {
+      const state = cm.state.colorpicker;
+      const swatch = hitTest(event);
+      dispatchEvent(new CustomEvent(CLOSE_POPUP_EVENT, {
+        detail: swatch && state.popup,
+      }));
+      if (swatch) {
         event.preventDefault();
-        self.openPopupForToken(event.target.parentNode);
+        openPopupForSwatch(state, swatch);
       }
     },
   };
 
-  function registerEvents(cm) {
-    Object.keys(CodeMirrorEvents).forEach(name => cm.on(name, CodeMirrorEvents[name]));
-  }
+  //endregion
+  //region ColorSwatch
 
-  function unregisterEvents(cm) {
-    Object.keys(CodeMirrorEvents).forEach(name => cm.off(name, CodeMirrorEvents[name]));
-  }
+  const cache = new Set();
 
-  function registerHooks() {
-    const mx = CodeMirror.modeExtensions.css;
-    if (!mx || mx.token !== colorizeToken) {
-      CodeMirror.extendMode('css', {token: colorizeToken});
-      CodeMirror.extendMode('stylus', {token: colorizeToken});
-      CodeMirror.modeExtensions.css.registerColorviewHooks = registerHooks;
-      CodeMirror.modeExtensions.css.unregisterColorviewHooks = unregisterHooks;
-    }
-  }
-
-  function unregisterHooks() {
-    for (const name in CodeMirror.modeExtensions) {
-      const mx = CodeMirror.modeExtensions[name];
-      if (mx && mx.token === colorizeToken) {
-        delete mx.token;
-      }
-    }
-  }
-
-  function resetMode(cm) {
-    cm.setOption('mode', cm.getMode().name);
-  }
-
-  function colorizeToken(stream, state) {
-    const token = this._token.apply(this, arguments);
-    const hookedToken = token && HOOKED_TOKEN.get(token);
-    if (!token || !hookedToken) {
-      return token;
-    }
-    const data = state.colorpicker = (state.colorpicker || {});
-    const cache = data.cache = (data.cache || stream.lineOracle.doc.cm.state.colorpicker.cache);
-    const string = stream.string;
-    const sameString = string === data.lastString;
-
-    data.lastString = string;
-
-    let lineCache = data.lineCache = (sameString ? data.lineCache : cache.get(string));
-    if (lineCache && lineCache.get(stream.start)) {
-      return hookedToken.override;
-    }
-
-    const color = hookedToken.process(stream);
-    if (color) {
-      if (!lineCache) {
-        lineCache = data.lineCache = new Map();
-        cache.set(string, lineCache);
-      }
-      lineCache.set(stream.start, color);
-      lineCache.set('lastAccessTime', performance.now());
-      return hookedToken.override;
-    }
-
-    return token;
-  }
-
-  function colorizeAtom(stream) {
-    const {start, pos, string} = stream;
-    const c1 = string.charAt(start);
-    if ((c1 === 't' || c1 === 'T') && string.slice(start, pos).toLowerCase() === 'transparent') {
-      return TRANSPARENT;
-    }
-    const maybeHex = c1 === '#';
-    const s = !maybeHex && string.charAt(pos) === '(' && string.slice(start, pos).toLowerCase();
-    if (maybeHex || (s === 'rgb' || s === 'rgba' || s === 'hsl' || s === 'hsla')) {
-      const rx = maybeHex ? RX_COLOR.hex : RX_COLOR[s];
-      rx.lastIndex = start;
-      const match = rx.exec(string);
-      return match && {color: match[0]};
-    }
-  }
-
-  function colorizeKeyword(stream) {
-    const {start, pos, string} = stream;
-    if (string.charAt(start) !== '!') {
-      const color = string.slice(start, pos);
-      const colorValue = NAMED_COLORS.get(color.toLowerCase());
-      return colorValue ? {color, colorValue} : colorizeAtom(stream);
-    }
-  }
-
-  function renderVisibleTokens(cm) {
-    const {cache, options} = cm.state.colorpicker;
-    let line = cm.display.viewFrom - 1;
-    for (const {line: lineHandle, text} of cm.display.renderedView) {
-      if (!lineHandle.parent) {
-        continue;
-      }
-      line++;
-      const styles = lineHandle.styles;
-      if (!styles) {
-        continue;
-      }
-      const lineCache = cache.get(lineHandle.text);
-      if (!lineCache) {
-        continue;
-      }
-      let lineCacheAlive = false;
-      let elementIndex = 0;
-      let elements, el, token;
-
-      for (let i = 1; i < styles.length; i += 2) {
-        if (token && token.includes(DISABLED_NEXT_TOKEN_NAME)) {
-          token = styles[i + 1];
-          elementIndex++;
-          i += 2;
-          continue;
-        }
-        token = styles[i + 1];
-        if (!token || !token.includes(OWN_TOKEN_NAME)) {
-          continue;
-        }
-        if (token.includes(DISABLED_TOKEN_NAME)) {
-          elementIndex++;
-          continue;
-        }
-
-        const start = styles[i - 2] || 0;
-        const data = lineCache.get(start);
-        if (!data) {
-          continue;
-        }
-        lineCacheAlive = true;
-
-        if (!elements) elements = text.getElementsByClassName(OWN_TOKEN_CLASS);
-        do {
-          el = elements[elementIndex];
-          elementIndex += el && el.classList.contains(DISABLED_NEXT_TOKEN_CLASS) ? 2 : 1;
-        } while (el && el.classList.contains(DISABLED_TOKEN_CLASS));
-
-        if (!el || (el.colorpickerData || {}).color === data.color) {
-          continue;
-        }
-
-        //////// yay we finally found something to render
-        el.colorpickerData = Object.assign({line, ch: start}, data);
-        let bg = el.firstElementChild;
-        if (!bg) {
-          bg = document.createElement('div');
-          bg.className = OWN_BACKGROUND_CLASS;
-          bg.title = options.tooltip;
-        }
-        bg.style.setProperty('background-color', data.color, 'important');
-        if (!bg.parentNode) el.appendChild(bg);
-      }
-
-      if (lineCacheAlive) {
-        lineCache.set('lastAccessTime', performance.now());
-      }
-    }
-    trimCache(cm);
-  }
-
-  function trimCache(cm, debounced) {
-    if (!debounced) {
-      clearTimeout(trimCache.timer);
-      trimCache.timer = setTimeout(trimCache, 20e3, cm, true);
-      return;
-    }
-    const cutoff = performance.now() - 60e3;
-    const {cache} = cm.state.colorpicker;
-    const textToKeep = new Set();
-    cm.doc.iter(({text}) => textToKeep.add(text));
-    for (const [text, lineCache] of cache.entries()) {
-      if (lineCache.get('lastAccessTime') < cutoff && !textToKeep.has(text)) {
-        cache.delete(text);
-      }
-    }
-  }
-
-  function parseColorAtCursor(lineText, lineTextLC = lineText.toLowerCase(), ch) {
-    const iHex = lineTextLC.lastIndexOf('#', ch);
-    const iParen = (
-      lineTextLC.lastIndexOf('(', ch) + 1 ||
-      lineTextLC.indexOf('(', ch) + 1
-    ) - 1;
-    let start = Math.max(iHex, iParen);
-    let match, end, color, colorValue;
-    if (start >= 0) {
-      if (start === iHex) {
-        match = RX_COLOR.hex;
-      } else {
-        const tokenLen = lineTextLC.charAt(start - 1) === 'a' ? 4 : 3;
-        start -= tokenLen;
-        match = RX_COLOR[lineTextLC.substr(start, tokenLen)];
-      }
-      if (match) {
-        match.lastIndex = start;
-        ([color] = match.exec(lineText) || []);
-      }
-    } else {
-      const isLetterAt = (i, code = lineTextLC.charCodeAt(i)) => code >= 97 && code <= 122;
-      for (start = ch; isLetterAt(start); start--) {} // eslint-disable-line no-empty
-      for (end = ch; isLetterAt(end); end++) {} // eslint-disable-line no-empty
-      start++;
-      (color = lineTextLC.slice(start, end));
-      colorValue = NAMED_COLORS.get(color);
-      if (!colorValue) {
-        start = ch;
-        color = '';
-      }
-    }
-    return color && {ch: start, color, colorValue};
-  }
-
-  function getNamedColorsMap() {
-    return new Map([
-      ['aliceblue', '#f0f8ff'],
-      ['antiquewhite', '#faebd7'],
-      ['aqua', '#00ffff'],
-      ['aquamarine', '#7fffd4'],
-      ['azure', '#f0ffff'],
-      ['beige', '#f5f5dc'],
-      ['bisque', '#ffe4c4'],
-      ['black', '#000000'],
-      ['blanchedalmond', '#ffebcd'],
-      ['blue', '#0000ff'],
-      ['blueviolet', '#8a2be2'],
-      ['brown', '#a52a2a'],
-      ['burlywood', '#deb887'],
-      ['cadetblue', '#5f9ea0'],
-      ['chartreuse', '#7fff00'],
-      ['chocolate', '#d2691e'],
-      ['coral', '#ff7f50'],
-      ['cornflowerblue', '#6495ed'],
-      ['cornsilk', '#fff8dc'],
-      ['crimson', '#dc143c'],
-      ['cyan', '#00ffff'],
-      ['darkblue', '#00008b'],
-      ['darkcyan', '#008b8b'],
-      ['darkgoldenrod', '#b8860b'],
-      ['darkgray', '#a9a9a9'],
-      ['darkgreen', '#006400'],
-      ['darkgrey', '#a9a9a9'],
-      ['darkkhaki', '#bdb76b'],
-      ['darkmagenta', '#8b008b'],
-      ['darkolivegreen', '#556b2f'],
-      ['darkorange', '#ff8c00'],
-      ['darkorchid', '#9932cc'],
-      ['darkred', '#8b0000'],
-      ['darksalmon', '#e9967a'],
-      ['darkseagreen', '#8fbc8f'],
-      ['darkslateblue', '#483d8b'],
-      ['darkslategray', '#2f4f4f'],
-      ['darkslategrey', '#2f4f4f'],
-      ['darkturquoise', '#00ced1'],
-      ['darkviolet', '#9400d3'],
-      ['deeppink', '#ff1493'],
-      ['deepskyblue', '#00bfff'],
-      ['dimgray', '#696969'],
-      ['dimgrey', '#696969'],
-      ['dodgerblue', '#1e90ff'],
-      ['firebrick', '#b22222'],
-      ['floralwhite', '#fffaf0'],
-      ['forestgreen', '#228b22'],
-      ['fuchsia', '#ff00ff'],
-      ['gainsboro', '#dcdcdc'],
-      ['ghostwhite', '#f8f8ff'],
-      ['gold', '#ffd700'],
-      ['goldenrod', '#daa520'],
-      ['gray', '#808080'],
-      ['green', '#008000'],
-      ['greenyellow', '#adff2f'],
-      ['grey', '#808080'],
-      ['honeydew', '#f0fff0'],
-      ['hotpink', '#ff69b4'],
-      ['indianred', '#cd5c5c'],
-      ['indigo', '#4b0082'],
-      ['ivory', '#fffff0'],
-      ['khaki', '#f0e68c'],
-      ['lavender', '#e6e6fa'],
-      ['lavenderblush', '#fff0f5'],
-      ['lawngreen', '#7cfc00'],
-      ['lemonchiffon', '#fffacd'],
-      ['lightblue', '#add8e6'],
-      ['lightcoral', '#f08080'],
-      ['lightcyan', '#e0ffff'],
-      ['lightgoldenrodyellow', '#fafad2'],
-      ['lightgray', '#d3d3d3'],
-      ['lightgreen', '#90ee90'],
-      ['lightgrey', '#d3d3d3'],
-      ['lightpink', '#ffb6c1'],
-      ['lightsalmon', '#ffa07a'],
-      ['lightseagreen', '#20b2aa'],
-      ['lightskyblue', '#87cefa'],
-      ['lightslategray', '#778899'],
-      ['lightslategrey', '#778899'],
-      ['lightsteelblue', '#b0c4de'],
-      ['lightyellow', '#ffffe0'],
-      ['lime', '#00ff00'],
-      ['limegreen', '#32cd32'],
-      ['linen', '#faf0e6'],
-      ['magenta', '#ff00ff'],
-      ['maroon', '#800000'],
-      ['mediumaquamarine', '#66cdaa'],
-      ['mediumblue', '#0000cd'],
-      ['mediumorchid', '#ba55d3'],
-      ['mediumpurple', '#9370db'],
-      ['mediumseagreen', '#3cb371'],
-      ['mediumslateblue', '#7b68ee'],
-      ['mediumspringgreen', '#00fa9a'],
-      ['mediumturquoise', '#48d1cc'],
-      ['mediumvioletred', '#c71585'],
-      ['midnightblue', '#191970'],
-      ['mintcream', '#f5fffa'],
-      ['mistyrose', '#ffe4e1'],
-      ['moccasin', '#ffe4b5'],
-      ['navajowhite', '#ffdead'],
-      ['navy', '#000080'],
-      ['oldlace', '#fdf5e6'],
-      ['olive', '#808000'],
-      ['olivedrab', '#6b8e23'],
-      ['orange', '#ffa500'],
-      ['orangered', '#ff4500'],
-      ['orchid', '#da70d6'],
-      ['palegoldenrod', '#eee8aa'],
-      ['palegreen', '#98fb98'],
-      ['paleturquoise', '#afeeee'],
-      ['palevioletred', '#db7093'],
-      ['papayawhip', '#ffefd5'],
-      ['peachpuff', '#ffdab9'],
-      ['peru', '#cd853f'],
-      ['pink', '#ffc0cb'],
-      ['plum', '#dda0dd'],
-      ['powderblue', '#b0e0e6'],
-      ['purple', '#800080'],
-      ['rebeccapurple', '#663399'],
-      ['red', '#ff0000'],
-      ['rosybrown', '#bc8f8f'],
-      ['royalblue', '#4169e1'],
-      ['saddlebrown', '#8b4513'],
-      ['salmon', '#fa8072'],
-      ['sandybrown', '#f4a460'],
-      ['seagreen', '#2e8b57'],
-      ['seashell', '#fff5ee'],
-      ['sienna', '#a0522d'],
-      ['silver', '#c0c0c0'],
-      ['skyblue', '#87ceeb'],
-      ['slateblue', '#6a5acd'],
-      ['slategray', '#708090'],
-      ['slategrey', '#708090'],
-      ['snow', '#fffafa'],
-      ['springgreen', '#00ff7f'],
-      ['steelblue', '#4682b4'],
-      ['tan', '#d2b48c'],
-      ['teal', '#008080'],
-      ['thistle', '#d8bfd8'],
-      ['tomato', '#ff6347'],
-      ['turquoise', '#40e0d0'],
-      ['violet', '#ee82ee'],
-      ['wheat', '#f5deb3'],
-      ['white', '#ffffff'],
-      ['whitesmoke', '#f5f5f5'],
-      ['yellow', '#ffff00'],
-      ['yellowgreen', '#9acd32'],
-    ]);
-  }
-
-  class ColorMarker {
-    constructor(cm, {
-      tooltip = 'Open color picker',
-      popupOptions = {},
-      colorpicker,
-      forceUpdate,
-    } = {}) {
+  class ColorSwatch {
+    constructor(cm, options) {
       this.cm = cm;
-      this.options = {
-        tooltip,
-        popup: Object.assign({
-          hideDelay: 2000,
-          hexUppercase: false,
-          tooltipForSwitcher: 'Switch formats: HEX -> RGB -> HSL',
-        }, popupOptions),
-      };
-      this.popup = cm.colorpicker ? cm.colorpicker() : colorpicker;
-      this.cache = new Map();
-      registerHooks(cm);
-      registerEvents(cm);
-      if (forceUpdate) {
-        resetMode(cm);
+      this.options = options;
+      this.popup = cm.colorpicker();
+      this.markersToRemove = [];
+      this.markersToRepaint = [];
+      this.colorize();
+      this.registerEvents();
+    }
+
+    colorize() {
+      colorizeAll(this);
+    }
+
+    openPopup(color) {
+      if (this.popup) openPopupForCursor(this, color);
+    }
+
+    registerEvents() {
+      for (const name in CM_EVENTS) {
+        this.cm.on(name, CM_EVENTS[name]);
+      }
+    }
+
+    unregisterEvents() {
+      for (const name in CM_EVENTS) {
+        this.cm.off(name, CM_EVENTS[name]);
       }
     }
 
     destroy() {
-      unregisterHooks(this.cm);
-      unregisterEvents(this.cm);
-      resetMode(this.cm);
-      this.cm.state.colorpicker = null;
-    }
-
-    openPopup(color) {
-      // eslint-disable-next-line prefer-const
-      let {line, ch} = this.cm.getCursor();
-      const lineText = this.cm.getLine(line);
-      const lineTextLC = lineText.toLowerCase();
-      const atImportant = lineTextLC.lastIndexOf('!important', ch);
-      if (atImportant >= Math.max(0, ch - '!important'.length)) {
-        ch -= Math.max(0, ch - atImportant);
-      }
-      const data = {line, ch, colorValue: color, isShortCut: true};
-      const lineCache = this.cm.state.colorpicker.cache.get(lineText);
-      if (lineCache) {
-        for (const [start, {color, colorValue = color}] of lineCache.entries()) {
-          // one entry is for lastAccessTime
-          if (lineCache.size === 2 || start <= ch && ch <= start + color.length) {
-            Object.assign(data, {ch: start, color, colorValue});
-            this.openPopupForToken({colorpickerData: data});
-            return;
-          }
-        }
-      }
-      Object.assign(data, parseColorAtCursor(lineText, lineTextLC, ch));
-      this.openPopupForToken({colorpickerData: data});
-    }
-
-    openPopupForToken({colorpickerData: data}) {
-      if (this.popup) {
-        const {left, bottom: top} = this.cm.charCoords(data, 'window');
-        this.popup.show(Object.assign(this.options.popup, data, {
-          top,
-          left,
-          cm: this.cm,
-          color: data.colorValue || data.color,
-          prevColor: data.color || '',
-          isShortCut: false,
-          callback: ColorMarker.popupOnChange,
-        }));
-      }
-    }
-
-    closePopup() {
-      if (this.popup) {
-        this.popup.hide();
-      }
-    }
-
-    static popupOnChange(newColor) {
-      if (!newColor) {
-        return;
-      }
-      const {cm, line, ch, embedderCallback} = this;
-      const to = {line, ch: ch + this.prevColor.length};
-      if (cm.getRange(this, to) !== newColor) {
-        cm.replaceRange(newColor, this, to, '*colorpicker');
-        this.prevColor = newColor;
-      }
-      if (typeof embedderCallback === 'function') {
-        embedderCallback(this);
-      }
+      this.unregisterEvents();
+      const {cm} = this;
+      const {curOp} = cm;
+      if (!curOp) cm.startOperation();
+      cm.getAllMarks().forEach(m => m.className === COLORVIEW_CLASS && m.clear());
+      if (!curOp) cm.endOperation();
+      cm.state.colorpicker = null;
     }
   }
 
+  //endregion
+  //region CodeMirror registration
+
   CodeMirror.defineOption('colorpicker', false, (cm, value, oldValue) => {
     if (oldValue && oldValue !== CodeMirror.Init && cm.state.colorpicker) {
       cm.state.colorpicker.destroy();
     }
     if (value) {
-      registerHooks();
-      cm.state.colorpicker = new ColorMarker(cm, value);
+      cm.state.colorpicker = new ColorSwatch(cm, value);
     }
   });
 
-  // initial runMode is performed by CodeMirror before setting our option
-  // so we register the hooks right away (the cost of always loading colorview is ~1ms for >200ms)
-  registerHooks();
+  CodeMirror.prototype.getStyleAtPos = getStyleAtPos;
+
+  return;
+
+  //endregion
+  //region Colorizing
+
+  function colorizeAll(state) {
+    const {cm} = state;
+    const {curOp} = cm;
+    if (!curOp) cm.startOperation();
+
+    const viewFrom = cm.display.viewFrom;
+    const viewTo = (cm.display.viewTo || maxRenderChunkSize - 1) + 1;
+
+    state.line = viewFrom;
+    state.inComment = null;
+    state.stopAt = state.stopped = null;
+
+    cm.doc.iter(viewFrom, viewTo, lineHandle => colorizeLine(state, lineHandle));
+
+    updateMarkers(state);
+    if (!curOp) cm.endOperation();
+
+    if (viewFrom > 0 || viewTo < cm.doc.size) {
+      clearTimeout(state.colorizeTimer);
+      state.colorizeTimer = setTimeout(colorizeInvisible, 100, state, viewFrom, viewTo, 0);
+    }
+  }
+
+
+  function colorizeInvisible(state, viewFrom, viewTo, line) {
+    const {cm} = state;
+    const {curOp} = cm;
+    if (!curOp) cm.startOperation();
+
+    state.stopAt = performance.now() + TIME_BUDGET;
+    state.stopped = null;
+
+    // before the visible range
+    if (viewFrom) {
+      state.line = line;
+      cm.doc.iter(line, viewFrom, lineHandle => colorizeLine(state, lineHandle));
+    }
+
+    // after the visible range
+    if (!state.stopped && viewTo < cm.doc.size) {
+      state.line = viewTo;
+      cm.doc.iter(viewTo, cm.doc.size, lineHandle => colorizeLine(state, lineHandle));
+    }
+
+    updateMarkers(state);
+    if (!curOp) cm.endOperation();
+
+    if (state.stopped) {
+      state.colorizeTimer = setTimeout(colorizeInvisible, 0, state, viewFrom, viewFrom, state.line);
+    }
+  }
+
+
+  function colorizeChanges(state, changes) {
+    const queue = [];
+    const postponed = [];
+    const viewFrom = state.cm.display.viewFrom || 0;
+    const viewTo = state.cm.display.viewTo || viewFrom + maxRenderChunkSize;
+
+    for (let change of changes) {
+      const {from} = change;
+      const to = CodeMirror.changeEnd(change);
+      const offscreen = from.line > viewTo || to.line < viewFrom;
+      if (offscreen) {
+        postponed.push(change);
+        continue;
+      }
+      if (from.line < viewFrom) {
+        postponed.push(Object.assign({}, change, {to: {line: viewFrom - 1}}));
+        change = Object.assign({}, change, {from: {line: viewFrom}});
+      }
+      if (to.line > viewTo) {
+        postponed.push(Object.assign({}, change, {from: {line: viewTo + 1}}));
+        change = Object.assign({}, change, {to: {line: viewTo}});
+      }
+      queue.push(change);
+    }
+
+    if (queue.length) colorizeChangesNow(state, queue);
+    if (postponed.length) setTimeout(colorizeChangesNow, 0, state, postponed, true);
+  }
+
+
+  function colorizeChangesNow(state, changes, canPostpone) {
+    const {cm} = state;
+    const {curOp} = cm;
+    if (!curOp) cm.startOperation();
+
+    const stopAt = canPostpone && performance.now() + TIME_BUDGET;
+    let stopped = null;
+
+    let change, changeFromLine;
+    let changeToLine = -1;
+    let queueIndex = -1;
+
+    changes = changes.sort((a, b) => a.from.line - b.from.line || a.from.ch - b.from.ch);
+    const first = changes[0].from.line;
+    const last = CodeMirror.changeEnd(changes[changes.length - 1]).line;
+    let line = state.line = first;
+
+    cm.doc.iter(first, last + 1, lineHandle => {
+      if (line > changeToLine) {
+        change = changes[++queueIndex];
+        if (!change) return true;
+        changeFromLine = change.from.line;
+        changeToLine = CodeMirror.changeEnd(change).line;
+      }
+      if (changeFromLine <= line && line <= changeToLine) {
+        state.line = line;
+        if (!lineHandle.styles) state.cm.getTokenTypeAt({line, ch: 0});
+        colorizeLineViaStyles(state, lineHandle);
+      }
+      if (canPostpone && performance.now() > stopAt) {
+        stopped = true;
+        return true;
+      }
+      line++;
+    });
+
+    updateMarkers(state);
+    if (!curOp) cm.endOperation();
+
+    if (stopped) {
+      const stoppedInChange = line >= changeFromLine && line < changeToLine;
+      if (stoppedInChange) {
+        changes.splice(0, queueIndex);
+        changes[0] = Object.assign({}, changes[0], {from: {line}});
+      } else {
+        changes.splice(0, queueIndex + 1);
+      }
+      state.colorizeTimer = setTimeout(colorizeChangesNow, 0, state, changes, true);
+    }
+  }
+
+
+  function colorizeLine(state, lineHandle) {
+    if (state.stopAt && performance.now() > state.stopAt) {
+      state.stopped = true;
+      return true;
+    }
+    const {text, styles} = lineHandle;
+    const {cm} = state;
+
+    if (state.inComment === null && !styles) {
+      cm.getTokenTypeAt({line: state.line, ch: 0});
+      colorizeLineViaStyles(state, lineHandle);
+      return;
+    }
+
+    if (styles) {
+      colorizeLineViaStyles(state, lineHandle);
+      return;
+    }
+
+    let cmtStart = 0;
+    let cmtEnd = 0;
+    do {
+      if (state.inComment) {
+        cmtEnd = text.indexOf('*/', cmtStart);
+        if (cmtEnd < 0) break;
+        state.inComment = false;
+        cmtEnd += 2;
+      }
+      cmtStart = (text.indexOf('/*', cmtEnd) + 1 || text.length + 1) - 1;
+      const chunk = !cmtEnd && cmtStart === text.length ? text : text.slice(cmtEnd, cmtStart);
+
+      RX_DETECT.lastIndex = 0;
+      const m = RX_DETECT.exec(chunk);
+      if (m) {
+        cmtEnd += m.index + m[1].length;
+        cm.getTokenTypeAt({line: state.line, ch: 0});
+        const {index} = getStyleAtPos({styles: lineHandle.styles, pos: cmtEnd}) || {};
+        colorizeLineViaStyles(state, lineHandle, Math.max(1, index || 0));
+        return;
+      }
+      state.inComment = cmtStart < text.length;
+    } while (state.inComment);
+    state.line++;
+  }
+
+
+  function colorizeLineViaStyles(state, lineHandle, styleIndex = 1) {
+    const {styles} = lineHandle;
+    let {text} = lineHandle;
+    let spanIndex = 0;
+    let uncommented = false;
+    let span, style, start, end, len, isHex, isFunc, color;
+
+    let {markedSpans} = lineHandle;
+    let spansSorted = false;
+
+    for (let i = styleIndex; i + 1 < styles.length; i += 2) {
+      style = styles[i + 1];
+      const styleSupported = style && (style.includes('atom') || style.includes('keyword'));
+      if (!styleSupported) continue;
+
+      start = i > 2 ? styles[i - 2] : 0;
+      end = styles[i];
+      len = end - start;
+      isHex = text[start] === '#';
+      isFunc = text[end] === '(';
+
+      if (isFunc && (len < 3 || len > 4 || !testAt(RX_DETECT_FUNC, start, text))) continue;
+      if (isFunc && !uncommented) {
+        text = blankOutComments(text, start);
+        uncommented = true;
+      }
+
+      color = text.slice(start, isFunc ? text.indexOf(')', end) + 1 : end);
+      const spanState = markedSpans && checkSpan();
+      if (spanState === 'same') continue;
+      if (checkColor()) {
+        (spanState ? redeem : mark)(getSafeColorValue());
+      }
+    }
+
+    removeDeadSpans();
+
+    state.inComment = style && style.includes('comment') && !text.endsWith('*/');
+    state.line++;
+    return;
+
+    function checkColor() {
+      if (isHex) return testAt(RX_COLOR.hex, 0, color);
+      if (!isFunc) return NAMED_COLORS.has(color.toLowerCase());
+
+      const colorLower = color.toLowerCase();
+      if (cache.has(colorLower)) return true;
+
+      const type = color.substr(0, 3);
+      const value = color.slice(len + 1, -1);
+      if (!testAt(RX_COLOR[type], 0, value)) return false;
+
+      cache.add(colorLower);
+      return true;
+    }
+
+    function mark(colorValue) {
+      const {line} = state;
+      state.cm.markText({line, ch: start}, {line, ch: end}, {
+        className: COLORVIEW_CLASS,
+        startStyle: COLORVIEW_SWATCH_CLASS,
+        css: COLORVIEW_SWATCH_CSS + colorValue,
+        color,
+      });
+    }
+
+    function getSafeColorValue() {
+      if (isHex && color.length !== 5 && color.length !== 9) return color;
+      if (!isFunc || !RX_COLOR.unsupported.test(color)) return color;
+      const value = state.popup.stringToColor(color);
+      return state.popup.colorToString(value, 'rgb');
+    }
+
+    // update or skip or delete existing swatches
+    function checkSpan() {
+      if (!spansSorted) {
+        markedSpans = markedSpans.sort((a, b) => a.from - b.from);
+        spansSorted = true;
+      }
+      while (spanIndex < markedSpans.length) {
+        span = markedSpans[spanIndex];
+        if (span.from <= start) {
+          spanIndex++;
+        } else {
+          break;
+        }
+        if (span.from === start && span.marker.className === COLORVIEW_CLASS) {
+          const same = color === span.marker.color &&
+            (isFunc || /\W|^$/i.test(text.substr(start + color.length, 1)));
+          if (same) return 'same';
+          state.markersToRemove.push(span.marker);
+          return 'redeem';
+        }
+      }
+    }
+
+    function redeem(colorValue) {
+      state.markersToRemove.pop();
+      state.markersToRepaint.push(span);
+      span.to = end;
+      span.line = state.line;
+      span.index = spanIndex - 1;
+      span.marker.color = color;
+      span.marker.css = COLORVIEW_SWATCH_CSS + colorValue;
+    }
+
+    function removeDeadSpans() {
+      while (markedSpans && spanIndex < markedSpans.length) {
+        span = markedSpans[spanIndex++];
+        if (span.marker.className === COLORVIEW_CLASS) {
+          state.markersToRemove.push(span.marker);
+        }
+      }
+    }
+  }
+
+  //endregion
+  //region Popup
+
+  function openPopupForCursor(state, defaultColor) {
+    const {line, ch} = state.cm.getCursor();
+    const lineHandle = state.cm.getLineHandle(line);
+    const data = {
+      line, ch,
+      color: defaultColor,
+      isShortCut: true,
+    };
+
+    let found;
+    for (const {from, marker} of lineHandle.markedSpans || []) {
+      if (marker.className === COLORVIEW_CLASS &&
+          from <= ch && ch < from + marker.color.length) {
+        found = {color: marker.color, ch: from};
+        break;
+      }
+    }
+    found = found || findNearestColor(lineHandle, ch);
+    doOpenPopup(state, Object.assign(data, found));
+    if (found) highlightColor(state, data);
+  }
+
+
+  function openPopupForSwatch(state, swatch) {
+    const cm = state.cm;
+    const lineDiv = swatch.closest('div');
+    const {line: {markedSpans} = {}} = cm.display.renderedView.find(v => v.node === lineDiv) || {};
+    if (!markedSpans) return;
+
+    let swatchIndex = [...lineDiv.getElementsByClassName(COLORVIEW_SWATCH_CLASS)].indexOf(swatch);
+    for (const {marker} of markedSpans.sort((a, b) => a.from - b.from)) {
+      if (marker.className === COLORVIEW_CLASS && swatchIndex-- === 0) {
+        const data = Object.assign({color: marker.color}, marker.find().from);
+        highlightColor(state, data);
+        doOpenPopup(state, data);
+        break;
+      }
+    }
+  }
+
+
+  function doOpenPopup(state, data) {
+    const {left, bottom: top} = state.cm.charCoords(data, 'window');
+    state.popup.show(Object.assign(state.options.popup, data, {
+      top,
+      left,
+      cm: state.cm,
+      color: data.color,
+      prevColor: data.color || '',
+      isShortCut: false,
+      callback: popupOnChange,
+    }));
+  }
+
+
+  function popupOnChange(newColor) {
+    if (!newColor) {
+      return;
+    }
+    const {cm, line, ch, embedderCallback} = this;
+    const to = {line, ch: ch + this.prevColor.length};
+    if (cm.getRange(this, to) !== newColor) {
+      cm.replaceRange(newColor, this, to, '*colorpicker');
+      this.prevColor = newColor;
+    }
+    if (typeof embedderCallback === 'function') {
+      embedderCallback(this);
+    }
+  }
+
+  //endregion
+  //region Utility
+
+  function updateMarkers(state) {
+    state.markersToRemove.forEach(m => m.clear());
+    state.markersToRemove.length = 0;
+
+    const {cm: {display: {viewFrom, viewTo, view}}} = state;
+    let viewIndex = 0;
+    let lineView = view[0];
+    let lineViewLine = viewFrom;
+    for (const {line, index, marker} of state.markersToRepaint) {
+      if (line < viewFrom || line >= viewTo) continue;
+      while (lineViewLine < line && lineView) {
+        lineViewLine += lineView.size;
+        lineView = view[++viewIndex];
+      }
+      if (!lineView) break;
+      const el = lineView.text.getElementsByClassName(COLORVIEW_SWATCH_CLASS)[index];
+      if (el) el.style = marker.css;
+    }
+    state.markersToRepaint.length = 0;
+  }
+
+
+  function findNearestColor({styles, text}, pos) {
+    const ALLOWED_STYLES = ['atom', 'keyword', 'comment', 'string'];
+    let start, color, prevStart, prevColor, m;
+    RX_DETECT.lastIndex = Math.max(0, pos - 1000);
+
+    while ((m = RX_DETECT.exec(text))) {
+      start = m.index + m[1].length;
+      color = getColor(m[2].toLowerCase());
+      if (!color) continue;
+      if (start >= pos) break;
+      prevStart = start;
+      prevColor = color;
+    }
+
+    if (prevColor && pos - (prevStart + prevColor.length) < start - pos) {
+      return {color: prevColor, ch: prevStart};
+    } else if (color) {
+      return {color, ch: start};
+    }
+
+    function getColor(token) {
+      const {style} = getStyleAtPos({styles, pos: start + 1}) || {};
+      const allowed = ALLOWED_STYLES.includes(style);
+      if (!allowed) return;
+
+      if (text[start + token.length] === '(') {
+        const tail = blankOutComments(text.slice(start), 0);
+        const color = tail.slice(0, tail.indexOf(')') + 1);
+        const type = color.slice(0, 3);
+        const value = color.slice(token.length + 1, -1);
+        return testAt(RX_COLOR[type], 0, value) && color;
+      }
+      return (token[0] === '#' || NAMED_COLORS.has(token)) && token;
+    }
+  }
+
+
+  function highlightColor(state, data) {
+    const {line} = data;
+    const {cm} = state;
+    const {viewFrom, viewTo} = cm.display;
+    if (line < viewFrom || line > viewTo) {
+      return;
+    }
+    const first = cm.charCoords(data, 'window');
+    const colorEnd = data.ch + data.color.length - 1;
+    let last = cm.charCoords({line, ch: colorEnd}, 'window');
+    if (last.top !== first.top) {
+      const funcEnd = data.ch + data.color.indexOf('(') - 1;
+      last = cm.charCoords({line, ch: funcEnd}, 'window');
+    }
+    const el = document.createElement('div');
+    const DURATION_SEC = 1;
+    el.style = `
+      position: fixed;
+      display: block;
+      top: ${first.top}px;
+      left: ${first.left}px;
+      width: ${last.right - first.left}px;
+      height: ${last.bottom - first.top}px;
+      animation: highlight ${DURATION_SEC}s;
+    `;
+    document.body.appendChild(el);
+    setTimeout(() => el.remove(), DURATION_SEC * 1000);
+  }
+
+
+  function testAt(rx, index, text) {
+    if (!rx) return false;
+    rx.lastIndex = index;
+    return rx.test(text);
+  }
+
+
+  function getStyleAtPos({
+    line,
+    styles = this.getLineHandle(line).styles,
+    pos,
+  }) {
+    if (pos < 0) return;
+    const len = styles.length;
+    const end = styles[len - 2];
+    if (pos > end) return;
+    const mid = (pos / end * (len - 1) & ~1) + 1;
+    let a = mid;
+    let b;
+    while (a > 1 && styles[a] > pos) {
+      b = a;
+      a = (a / 2 & ~1) + 1;
+    }
+    if (!b) b = mid;
+    while (b < len && styles[b] < pos) b = ((len + b) / 2 & ~1) + 1;
+    while (a < b - 3) {
+      const c = ((a + b) / 2 & ~1) + 1;
+      if (styles[c] > pos) b = c; else a = c;
+    }
+    while (a < len && styles[a] < pos) a += 2;
+    return {
+      style: styles[a + 1],
+      index: a,
+    };
+  }
+
+
+  function blankOutComments(text, start) {
+    const cmtStart = text.indexOf('/*', start);
+    return cmtStart < 0 ? text : (
+      text.slice(0, cmtStart) +
+      text.slice(cmtStart)
+        .replace(RX_COMMENT, s =>
+          SPACE1K.repeat(s.length / 1000 | 0) + SPACE1K.slice(0, s.length % 1000))
+    );
+  }
+
+  function hitTest({button, target, offsetX, offsetY}) {
+    if (button) return;
+    const swatch = target.closest('.' + COLORVIEW_CLASS);
+    if (!swatch) return;
+    const {left, width, height} = getComputedStyle(swatch, '::after');
+    const bounds = swatch.getBoundingClientRect();
+    const swatchClicked =
+      offsetX >= parseFloat(left) - 1 &&
+      offsetX <= parseFloat(left) + parseFloat(width) + 1 &&
+      offsetY >= parseFloat(height) / 2 - bounds.height / 2 - 1 &&
+      offsetY <= parseFloat(height) / 2 + bounds.height / 2 + 1;
+    return swatchClicked && swatch;
+  }
+
+  //endregion
 })();