Import Cobalt 25.master.0.1033734
diff --git a/third_party/skia/demos.skia.org/demos/textedit/index.html b/third_party/skia/demos.skia.org/demos/textedit/index.html
new file mode 100644
index 0000000..d6852c8
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/textedit/index.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<title>TextEdit demo in CanvasKit</title>
+<meta charset="utf-8" />
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<script type="text/javascript" src="https://particles.skia.org/dist/canvaskit.js"></script>
+<script type="text/javascript" src="textapi_utils.js"></script>
+<script type="text/javascript" src="spiralshader.js"></script>
+
+<style>
+canvas {
+  border: 1px dashed grey;
+}
+</style>
+
+<body>
+  <h1>TextEdit in CanvasKit</h1>
+
+  <canvas id=para2 width=600 height=600 tabindex='-1'></canvas>
+</body>
+
+<script type="text/javascript" charset="utf-8">
+    let CanvasKit;
+    onload = async () => {
+      CanvasKit = await CanvasKitInit({ locateFile: (file) => 'https://particles.skia.org/dist/'+file });
+      ParagraphAPI2();
+    };
+
+    function ParagraphAPI2() {
+      const surface = CanvasKit.MakeCanvasSurface('para2');
+      if (!surface) {
+        console.error('Could not make surface');
+        return;
+      }
+
+      const mouse = MakeMouse();
+      const cursor = MakeCursor(CanvasKit);
+      const canvas = surface.getCanvas();
+      const spiralEffect = MakeSpiralShaderEffect(CanvasKit);
+
+      const text0 = "In a hole in the ground there lived a hobbit. Not a nasty, dirty, " +
+                    "wet hole full of worms and oozy smells. This was a hobbit-hole and " +
+                    "that means good food, a warm hearth, and all the comforts of home.";
+      const LOC_X = 20,
+            LOC_Y = 20;
+
+      const bgPaint = new CanvasKit.Paint();
+      bgPaint.setColor([0.965, 0.965, 0.965, 1]);
+
+      const editor = MakeEditor(text0, {typeface:null, size:30}, cursor, 540);
+
+      editor.applyStyleToRange({size:130}, 0, 1);
+      editor.applyStyleToRange({italic:true}, 38, 38+6);
+      editor.applyStyleToRange({color:[1,0,0,1]}, 5, 5+4);
+
+      editor.setXY(LOC_X, LOC_Y);
+
+      function drawFrame(canvas) {
+        const lines = editor.getLines();
+
+        canvas.clear(CanvasKit.WHITE);
+
+        if (mouse.isActive()) {
+            const pos = mouse.getPos(-LOC_X, -LOC_Y);
+            const a = lines_pos_to_index(lines, pos[0], pos[1]);
+            const b = lines_pos_to_index(lines, pos[2], pos[3]);
+            if (a === b) {
+                editor.setIndex(a);
+            } else {
+                editor.setIndices(a, b);
+            }
+        }
+
+        canvas.drawRect(editor.bounds(), bgPaint);
+
+        {
+            // update our animated shaders
+            const rad_scale = Math.sin(Date.now() / 5000) / 2;
+            const shader0 = spiralEffect.makeShader([
+                rad_scale,
+                editor.width()/2, editor.width()/2,
+                1,0,0,1,                            // color0
+                0,0,1,1                             // color1
+             ]);
+            editor.draw(canvas, [shader0]);
+            shader0.delete();
+        }
+
+        surface.requestAnimationFrame(drawFrame);
+      }
+      surface.requestAnimationFrame(drawFrame);
+
+      function interact(e) {
+        const type = e.type;
+        if (type === 'pointerup') {
+            mouse.setUp(e.offsetX, e.offsetY);
+        } else if (type === 'pointermove') {
+            mouse.setMove(e.offsetX, e.offsetY);
+        } else if (type === 'pointerdown') {
+            mouse.setDown(e.offsetX, e.offsetY);
+        }
+      };
+
+      function keyhandler(e) {
+          switch (e.key) {
+              case 'ArrowLeft':  editor.moveDX(-1); return;
+              case 'ArrowRight': editor.moveDX(1); return;
+              case 'ArrowUp':
+                e.preventDefault();
+                editor.moveDY(-1);
+                return;
+              case 'ArrowDown':
+                e.preventDefault();
+                editor.moveDY(1);
+                return;
+            case 'Backspace':
+                editor.deleteSelection(-1);
+                return;
+            case 'Delete':
+                editor.deleteSelection(1);
+                return;
+              case 'Shift':
+                return;
+              case 'Tab':   // todo: figure out how to handle...
+                e.preventDefault();
+                return;
+            }
+            if (e.ctrlKey) {
+                e.preventDefault();
+                e.stopImmediatePropagation();
+                switch (e.key) {
+                    case 'r': editor.applyStyleToSelection({color:[1,0,0,1]}); return;
+                    case 'g': editor.applyStyleToSelection({color:[0,0.6,0,1]}); return;
+                    case 'u': editor.applyStyleToSelection({color:[0,0,1,1]}); return;
+                    case 'k': editor.applyStyleToSelection({color:[0,0,0,1]}); return;
+
+                    case 's': editor.applyStyleToSelection({shaderIndex:0}); return;
+
+                    case 'i': editor.applyStyleToSelection({italic:'toggle'}); return;
+                    case 'b': editor.applyStyleToSelection({bold:'toggle'}); return;
+                    case 'w': editor.applyStyleToSelection({wavy:'toggle'}); return;
+
+                    case ']': editor.applyStyleToSelection({size_add:1});  return;
+                    case '[': editor.applyStyleToSelection({size_add:-1}); return;
+                    case '}': editor.applyStyleToSelection({size_add:10});  return;
+                    case '{': editor.applyStyleToSelection({size_add:-10}); return;
+                }
+            }
+            if (!e.ctrlKey && !e.metaKey) {
+                if (e.key.length == 1) {  // avoid keys like "Escape" for now
+                    e.preventDefault();
+                    e.stopImmediatePropagation();
+                    editor.insert(e.key);
+                }
+            }
+      }
+
+      document.getElementById('para2').addEventListener('pointermove', interact);
+      document.getElementById('para2').addEventListener('pointerdown', interact);
+      document.getElementById('para2').addEventListener('pointerup', interact);
+      document.getElementById('para2').addEventListener('keydown', keyhandler);
+      return surface;
+    }
+
+</script>
diff --git a/third_party/skia/demos.skia.org/demos/textedit/spiralshader.js b/third_party/skia/demos.skia.org/demos/textedit/spiralshader.js
new file mode 100644
index 0000000..3ad0c37
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/textedit/spiralshader.js
@@ -0,0 +1,28 @@
+
+/*
+ *  When calling makeShader, pass
+ *      radius_scale    0.5
+ *      center          x, y
+ *      color0          r, g, b, a
+ *      color1          r, g, b, a
+ */
+function MakeSpiralShaderEffect(CanvasKit) {
+    const _spiralSkSL = `
+    uniform float rad_scale;
+    uniform float2 in_center;
+    uniform float4 in_colors0;
+    uniform float4 in_colors1;
+
+    half4 main(float2 p) {
+        float2 pp = p - in_center;
+        float radius = sqrt(dot(pp, pp));
+        radius = sqrt(radius);
+        float angle = atan(pp.y / pp.x);
+        float t = (angle + 3.1415926/2) / (3.1415926);
+        t += radius * rad_scale;
+        t = fract(t);
+        return half4(mix(in_colors0, in_colors1, t));
+    }`;
+
+    return CanvasKit.RuntimeEffect.Make(_spiralSkSL);
+}
diff --git a/third_party/skia/demos.skia.org/demos/textedit/textapi_utils.js b/third_party/skia/demos.skia.org/demos/textedit/textapi_utils.js
new file mode 100644
index 0000000..9e6df24
--- /dev/null
+++ b/third_party/skia/demos.skia.org/demos/textedit/textapi_utils.js
@@ -0,0 +1,638 @@
+
+function ASSERT(pred) {
+    console.assert(pred, 'assert failed');
+}
+
+function LOG(...args) {
+    // comment out for non-debugging
+//    console.log(args);
+}
+
+function MakeCursor(CanvasKit) {
+    const linePaint = new CanvasKit.Paint();
+    linePaint.setColor([0,0,1,1]);
+    linePaint.setStyle(CanvasKit.PaintStyle.Stroke);
+    linePaint.setStrokeWidth(2);
+    linePaint.setAntiAlias(true);
+
+    const pathPaint = new CanvasKit.Paint();
+    pathPaint.setColor([0,0,1,0.25]);
+    linePaint.setAntiAlias(true);
+
+    return {
+        _line_paint: linePaint,    // wrap in weak-ref so we can delete it?
+        _path_paint: pathPaint,
+        _x: 0,
+        _top: 0,
+        _bottom: 0,
+        _path: null,            // only use x,top,bottom if path is null
+        _draws_per_sec: 2,
+
+        // pass 0 for no-draw, pass inf. for always on
+        setBlinkRate: function(blinks_per_sec) {
+            this._draws_per_sec = blinks_per_sec;
+        },
+        place: function(x, top, bottom) {
+            this._x = x;
+            this._top = top;
+            this._bottom = bottom;
+
+            this.setPath(null);
+        },
+        setPath: function(path) {
+            if (this._path) {
+                this._path.delete();
+            }
+            this._path = path;
+        },
+        draw_before: function(canvas) {
+            if (this._path) {
+                canvas.drawPath(this._path, this._path_paint);
+            }
+        },
+        draw_after: function(canvas) {
+            if (this._path) {
+                return;
+            }
+            if (Math.floor(Date.now() * this._draws_per_sec / 1000) & 1) {
+                canvas.drawLine(this._x, this._top, this._x, this._bottom, this._line_paint);
+            }
+        },
+    };
+}
+
+function MakeMouse() {
+    return {
+        _start_x: 0, _start_y: 0,
+        _curr_x:  0,  _curr_y: 0,
+        _active: false,
+
+        isActive: function() {
+            return this._active;
+        },
+        setDown: function(x, y) {
+            this._start_x = this._curr_x = x;
+            this._start_y = this._curr_y = y;
+            this._active = true;
+        },
+        setMove: function(x, y) {
+            this._curr_x = x;
+            this._curr_y = y;
+        },
+        setUp: function(x, y) {
+            this._curr_x = x;
+            this._curr_y = y;
+            this._active = false;
+        },
+        getPos: function(dx, dy) {
+            return [ this._start_x + dx, this._start_y + dy, this._curr_x + dx, this._curr_y + dy ];
+        },
+    };
+}
+
+function runs_x_to_index(runs, x) {
+    for (const r of runs) {
+        for (let i = 1; i < r.offsets.length; i += 1) {
+            if (x < r.positions[i*2]) {
+                const mid = (r.positions[i*2-2] + r.positions[i*2]) * 0.5;
+                if (x <= mid) {
+                    return r.offsets[i-1];
+                } else {
+                    return r.offsets[i];
+                }
+            }
+        }
+    }
+    const r = runs[runs.length-1];
+    return r.offsets[r.offsets.length-1];
+}
+
+function lines_pos_to_index(lines, x, y) {
+    if (y < lines[0].top) {
+        return 0;
+    }
+    const l = lines.find((l) => y <= l.bottom);
+    return l ? runs_x_to_index(l.runs, x)
+             : lines[lines.length - 1].textRange.last;
+}
+
+function runs_index_to_run(runs, index) {
+    const r = runs.find((r) => index <= r.offsets[r.offsets.length-1]);
+    // return last if no run is found
+    return r ? r : runs[runs.length-1];
+}
+
+function runs_index_to_x(runs, index) {
+    const r = runs_index_to_run(runs, index);
+    const i = r.offsets.findIndex((offset) => index === offset);
+    return i >= 0 ? r.positions[i*2]
+                  : r.positions[r.positions.length-2]; // last x
+}
+
+function lines_index_to_line_index(lines, index) {
+    const l = lines.findIndex((l) => index <= l.textRange.last);
+    return l >= 0 ? l : lines.length-1;
+}
+
+function lines_index_to_line(lines, index) {
+  return lines[lines_index_to_line_index(lines, index)];
+}
+
+function lines_indices_to_path(lines, a, b, width) {
+    if (a == b) {
+        return null;
+    }
+    if (a > b) { [a, b] = [b, a]; }
+
+    const path = new CanvasKit.Path();
+    const la = lines_index_to_line(lines, a);
+    const lb = lines_index_to_line(lines, b);
+    const ax = runs_index_to_x(la.runs, a);
+    const bx = runs_index_to_x(lb.runs, b);
+    if (la == lb) {
+        path.addRect([ax, la.top, bx, la.bottom]);
+    } else {
+        path.addRect([ax, la.top, width, la.bottom]);
+        path.addRect([0, lb.top, bx, lb.bottom]);
+        if (la.bottom < lb.top) {
+            path.addRect([0, la.bottom, width, lb.top]);   // extra lines inbetween
+        }
+    }
+    return path;
+}
+
+function string_del(str, start, end) {
+    return str.slice(0, start) + str.slice(end, str.length);
+}
+
+function make_default_paint() {
+    const p = new CanvasKit.Paint();
+    p.setAntiAlias(true);
+    return p;
+}
+
+function make_default_font(tf) {
+    const font = new CanvasKit.Font(tf);
+    font.setSubpixel(true);
+    return font;
+}
+
+function MakeStyle(length) {
+    return {
+        _length: length,
+        typeface: null,
+        size: null,
+        color: null,
+        bold: null,
+        italic: null,
+
+        _check_toggle: function(src, dst) {
+            if (src == 'toggle') {
+                return !dst;
+            } else {
+                return src;
+            }
+        },
+
+        // returns true if we changed something affecting layout
+        mergeFrom: function(src) {
+            let layoutChanged = false;
+
+            if (src.typeface && this.typeface !== src.typeface) {
+                this.typeface = src.typeface;
+                layoutChanged = true;
+            }
+            if (src.size && this.size !== src.size) {
+                this.size = src.size;
+                layoutChanged = true;
+            }
+            if (src.color) {
+                this.color = src.color;
+                delete this.shaderIndex;    // we implicitly delete shader if there is a color
+            }
+
+            if (src.bold) {
+                this.bold = this._check_toggle(src.bold, this.bold);
+            }
+            if (src.italic) {
+                this.italic = this._check_toggle(src.italic, this.italic);
+            }
+            if (src.wavy) {
+                this.wavy = this._check_toggle(src.wavy, this.wavy);
+            }
+
+            if (src.size_add) {
+                this.size += src.size_add;
+                layoutChanged = true;
+            }
+
+            if ('shaderIndex' in src) {
+                if (src.shaderIndex >= 0) {
+                    this.shaderIndex = src.shaderIndex;
+                } else {
+                    delete this.shaderIndex;
+                }
+            }
+            return layoutChanged;
+        }
+    };
+}
+
+function MakeEditor(text, style, cursor, width) {
+    const ed = {
+        _text: text,
+        _lines: null,
+        _cursor: cursor,
+        _width: width,
+        _index: { start: 0, end: 0 },
+        _styles: null,
+        // drawing
+        _X: 0,
+        _Y: 0,
+        _paint: make_default_paint(),
+        _font: make_default_font(style.typeface),
+
+        getLines: function() { return this._lines; },
+
+        width: function() {
+            return this._width;
+        },
+        height: function() {
+            return this._lines[this._lines.length-1].bottom;
+        },
+        bounds: function() {
+            return [this._X, this._Y, this._X + this.width(), this._Y + this.height()];
+        },
+        setXY: function(x, y) {
+            this._X = x;
+            this._Y = y;
+        },
+
+        _rebuild_selection: function() {
+            const a = this._index.start;
+            const b = this._index.end;
+            ASSERT(a >= 0 && a <= b && b <= this._text.length);
+            if (a === b) {
+                const l = lines_index_to_line(this._lines, a);
+                const x = runs_index_to_x(l.runs, a);
+                this._cursor.place(x, l.top, l.bottom);
+            } else {
+                this._cursor.setPath(lines_indices_to_path(this._lines, a, b, this._width));
+            }
+        },
+        setIndex: function(i) {
+            this._index.start = this._index.end = i;
+            this._rebuild_selection();
+        },
+        setIndices: function(a, b) {
+            if (a > b) { [a, b] = [b, a]; }
+            this._index.start = a;
+            this._index.end = b;
+            this._rebuild_selection();
+        },
+        moveDX: function(dx) {
+            let index;
+            if (this._index.start == this._index.end) {
+                // just adjust and pin
+                index = Math.max(Math.min(this._index.start + dx, this._text.length), 0);
+            } else {
+                // 'deselect' the region, and turn it into just a single index
+                index = dx < 0 ? this._index.start : this._index.end;
+            }
+            this.setIndex(index);
+        },
+        moveDY: function(dy) {
+            let index = (dy < 0) ? this._index.start : this._index.end;
+            const i = lines_index_to_line_index(this._lines, index);
+            if (dy < 0 && i === 0) {
+                index = 0;
+            } else if (dy > 0 && i == this._lines.length - 1) {
+                index = this._text.length;
+            } else {
+                const x = runs_index_to_x(this._lines[i].runs, index);
+                // todo: statefully track "original" x when an up/down sequence started,
+                //       so we can avoid drift.
+                index = runs_x_to_index(this._lines[i+dy].runs, x);
+            }
+            this.setIndex(index);
+        },
+
+        _validateStyles: function() {
+            const len = this._styles.reduce((sum, style) => sum + style._length, 0);
+            ASSERT(len === this._text.length);
+        },
+        _validateBlocks: function(blocks) {
+            const len = blocks.reduce((sum, block) => sum + block.length, 0);
+            ASSERT(len === this._text.length);
+        },
+
+        _buildLines: function() {
+            this._validateStyles();
+
+            const blocks = [];
+            let block = null;
+            for (const s of this._styles) {
+                if (!block || (block.typeface === s.typeface && block.size === s.size)) {
+                    if (!block) {
+                        block = { length: 0, typeface: s.typeface, size: s.size };
+                    }
+                    block.length += s._length;
+                } else {
+                    blocks.push(block);
+                    block = { length: s._length, typeface: s.typeface, size: s.size };
+                }
+            }
+            blocks.push(block);
+            this._validateBlocks(blocks);
+
+            this._lines = CanvasKit.ParagraphBuilder.ShapeText(this._text, blocks, this._width);
+            this._rebuild_selection();
+
+            // add textRange to each run, to aid in drawing
+            this._runs = [];
+            for (const l of this._lines) {
+                for (const r of l.runs) {
+                    r.textRange = { start: r.offsets[0], end: r.offsets[r.offsets.length-1] };
+                    this._runs.push(r);
+                }
+            }
+        },
+
+        // note: this does not rebuild lines/runs, or update the cursor,
+        //       but it does edit the text and styles
+        // returns true if it deleted anything
+        _deleteRange: function(start, end) {
+            ASSERT(start >= 0 && end <= this._text.length);
+            ASSERT(start <= end);
+            if (start === end) {
+                return false;
+            }
+
+            this._delete_style_range(start, end);
+            // Do this after shrink styles (we use text.length in an assert)
+            this._text = string_del(this._text, start, end);
+        },
+        deleteSelection: function(direction) {
+            let start = this._index.start;
+            if (start == this._index.end) {
+                if (direction < 0) {
+                    if (start == 0) {
+                        return;     // nothing to do
+                    }
+                    this._deleteRange(start - 1, start);
+                    start -= 1;
+                } else {
+                    if (start >= this._text.length) {
+                        return;     // nothing to do
+                    }
+                    this._deleteRange(start, start + 1);
+                }
+            } else {
+                this._deleteRange(start, this._index.end);
+            }
+            this._index.start = this._index.end = start;
+            this._buildLines();
+        },
+        insert: function(charcode) {
+            const len = charcode.length;
+            if (this._index.start != this._index.end) {
+                this.deleteSelection();
+            }
+            const index = this._index.start;
+
+            // do this before edit the text (we use text.length in an assert)
+            const [i, prev_len] = this.find_style_index_and_prev_length(index);
+            this._styles[i]._length += len;
+
+            // now grow the text
+            this._text = this._text.slice(0, index) + charcode + this._text.slice(index);
+
+            this._index.start = this._index.end = index + len;
+            this._buildLines();
+        },
+
+        draw: function(canvas, shaders) {
+            canvas.save();
+            canvas.translate(this._X, this._Y);
+
+            this._cursor.draw_before(canvas);
+
+            const runs = this._runs;
+            const styles = this._styles;
+            const f = this._font;
+            const p = this._paint;
+
+            let s = styles[0];
+            let sindex = 0;
+            let s_start = 0;
+            let s_end = s._length;
+
+            let r = runs[0];
+            let rindex = 0;
+
+            let start = 0;
+            let end = 0;
+            while (start < this._text.length) {
+                while (r.textRange.end <= start) {
+                    r = runs[++rindex];
+                    if (!r) {
+                        // ran out of runs, so the remaining text must just be WS
+                        break;
+                    }
+                }
+                if (!r) break;
+                while (s_end <= start) {
+                    s = styles[++sindex];
+                    s_start = s_end;
+                    s_end += s._length;
+                }
+                end = Math.min(r.textRange.end, s_end);
+
+                LOG('New range: ', start, end,
+                    'from run', r.textRange.start, r.textRange.end,
+                    'style', s_start, s_end);
+
+                // check that we have anything to draw
+                if (r.textRange.start >= end) {
+                    start = end;
+                    continue;  // could be a span of WS with no glyphs
+                }
+
+//              f.setTypeface(r.typeface); // r.typeface is always null (for now)
+                f.setSize(r.size);
+                f.setEmbolden(s.bold);
+                f.setSkewX(s.italic ? -0.2 : 0);
+                p.setColor(s.color ? s.color : [0,0,0,1]);
+                p.setShader(s.shaderIndex >= 0 ? shaders[s.shaderIndex] : null);
+
+                let gly = r.glyphs;
+                let pos = r.positions;
+                if (start > r.textRange.start || end < r.textRange.end) {
+                    // search for the subset of glyphs to draw
+                    let glyph_start, glyph_end;
+                    for (let i = 0; i < r.offsets.length; ++i) {
+                        if (r.offsets[i] >= start) {
+                            glyph_start = i;
+                            break;
+                        }
+                    }
+                    for (let i = glyph_start+1; i < r.offsets.length; ++i) {
+                        if (r.offsets[i] >= end) {
+                            glyph_end = i;
+                            break;
+                        }
+                    }
+                    LOG('    glyph subrange', glyph_start, glyph_end);
+                    gly = gly.slice(glyph_start, glyph_end);
+                    pos = pos.slice(glyph_start*2, glyph_end*2);
+                } else {
+                    LOG('    use entire glyph run');
+                }
+
+                let working_pos = pos;
+                if (s.wavy) {
+                    const xscale = 0.05;
+                    const yscale = r.size * 0.125;
+                    let wavy = [];
+                    for (let i = 0; i < pos.length; i += 2) {
+                        const x = pos[i + 0];
+                        wavy.push(x);
+                        wavy.push(pos[i + 1] + Math.sin(x * xscale) * yscale);
+                    }
+                    working_pos = new Float32Array(wavy);
+                }
+                canvas.drawGlyphs(gly, working_pos, 0, 0, f, p);
+
+                p.setShader(null);  // in case our caller deletes their shader(s)
+
+                start = end;
+            }
+
+            this._cursor.draw_after(canvas);
+            canvas.restore();
+        },
+
+        // Styling
+
+        // returns [index, prev total length before this style]
+        find_style_index_and_prev_length: function(index) {
+            let len = 0;
+            for (let i = 0; i < this._styles.length; ++i) {
+                const l = this._styles[i]._length;
+                len += l;
+                // < favors the latter style if index is between two styles
+                if (index < len) {
+                    return [i, len - l];
+                }
+            }
+            ASSERT(len === this._text.length);
+            return [this._styles.length-1, len];
+        },
+        _delete_style_range: function(start, end) {
+            // shrink/remove styles
+            //
+            // [.....][....][....][.....]  styles
+            //    [..................]     start...end
+            //
+            // - trim the first style
+            // - remove the middle styles
+            // - trim the last style
+
+            let N = end - start;
+            let [i, prev_len] = this.find_style_index_and_prev_length(start);
+            let s = this._styles[i];
+            if (start > prev_len) {
+                // we overlap the first style (but not entirely
+                const skip = start - prev_len;
+                ASSERT(skip < s._length);
+                const shrink = Math.min(N, s._length - skip);
+                ASSERT(shrink > 0);
+                s._length -= shrink;
+                N -= shrink;
+                if (N === 0) {
+                    return;
+                }
+                i += 1;
+                ASSERT(i < this._styles.length);
+            }
+            while (N > 0) {
+                s = this._styles[i];
+                if (N >= s._length) {
+                    N -= s._length;
+                    this._styles.splice(i, 1);
+                } else {
+                    s._length -= N;
+                    break;
+                }
+            }
+        },
+
+        applyStyleToRange: function(style, start, end) {
+            if (start > end) { [start, end] = [end, start]; }
+            ASSERT(start >= 0 && end <= this._text.length);
+            if (start === end) {
+                return;
+            }
+
+            LOG('trying to apply', style, start, end);
+            let i;
+            for (i = 0; i < this._styles.length; ++i) {
+                if (start <= this._styles[i]._length) {
+                    break;
+                }
+                start -= this._styles[i]._length;
+                end -= this._styles[i]._length;
+            }
+
+            let s = this._styles[i];
+            // do we need to fission off a clean subset for the head of s?
+            if (start > 0) {
+                const ns = Object.assign({}, s);
+                s._length = start;
+                ns._length -= start;
+                LOG('initial splice', i, start, s._length, ns._length);
+                i += 1;
+                this._styles.splice(i, 0, ns);
+                end -= start;
+                // we don't use start any more
+            }
+            // merge into any/all whole styles we overlap
+            let layoutChanged = false;
+            while (end >= this._styles[i]._length) {
+                LOG('whole run merging for style index', i)
+                layoutChanged |= this._styles[i].mergeFrom(style);
+                end -= this._styles[i]._length;
+                i += 1;
+                if (end == 0) {
+                    break;
+                }
+            }
+            // do we partially cover the last run
+            if (end > 0) {
+                s = this._styles[i];
+                const ns = Object.assign({}, s);    // the new first half
+                ns._length = end;
+                s._length -= end;                   // trim the (unchanged) tail
+                LOG('merging tail', i, ns._length, s._length);
+                layoutChanged |= ns.mergeFrom(style);
+                this._styles.splice(i, 0, ns);
+            }
+
+            this._validateStyles();
+            LOG('after applying styles', this._styles);
+
+            if (layoutChanged) {
+                this._buildLines();
+            }
+        },
+        applyStyleToSelection: function(style) {
+            this.applyStyleToRange(style, this._index.start, this._index.end);
+        },
+    };
+
+    const s = MakeStyle(ed._text.length);
+    s.mergeFrom(style);
+    ed._styles = [ s ];
+    ed._buildLines();
+    return ed;
+}