From 4f9cf3aa7e5dce646a18894e8f3146becfb38119 Mon Sep 17 00:00:00 2001 From: Jamie Peabody Date: Sat, 15 Jan 2022 14:18:05 +0000 Subject: [PATCH] refactor: moved cm markup to virtual doc for good performance gain --- examples/app.js | 5 +- src/diff-view.js | 258 ++++++++++++++++------------------------------- src/vdoc.js | 171 ++++++++++++++++++++++++++----- 3 files changed, 234 insertions(+), 200 deletions(-) diff --git a/examples/app.js b/examples/app.js index 2c6187b..dec9b12 100644 --- a/examples/app.js +++ b/examples/app.js @@ -22,7 +22,7 @@ document.onreadystatechange = function () { const doc = new Mergely('#mergely', { license: 'lgpl', ignorews: true, - wrap_lines: true, + wrap_lines: false, // change_timeout: 0, viewport: true, cmsettings: { @@ -36,8 +36,7 @@ document.onreadystatechange = function () { .replace(/love/g, 'hate') .replace(/heart/g, 'head')); }, - _debug: 'api,event,scroll,change,diff', - // _debug: 'draw,event,diff,change'//'event,scroll,diff,draw' + _debug: 'draw,change,event' }); // On init, scroll to first diff diff --git a/src/diff-view.js b/src/diff-view.js index ea721e1..aee8d3e 100644 --- a/src/diff-view.js +++ b/src/diff-view.js @@ -41,11 +41,11 @@ Fixed issue where unmarkup did not emit an updated event. Fixed issue where init triggered an updated event when autoupdate is disabled. Fixed documentation issue where `merge` incorrectly stated: from the specified `side` to the opposite side Fixed performance issue scrolling (find #) +Fixed performance issue with large sections of deleted/added text Fixed issue where initial render scrolled to first change, showing it at the bottom, as opposed to middle TODO: For some reason ignore-whitespace will mark the "red" differently -When wrap_lines is false, the CM editor grows, screwing up the layout Introduce an async render pipeline as it's currently blocking UI Fix issue where characters like `{}[].?` are not detected by LCS Fix the popup @@ -115,7 +115,7 @@ CodeMirrorDiffView.prototype.init = function(el, options = {}) { this.el = el; this.lhs_cmsettings = { - viewportMargin: Infinity, + // viewportMargin: Infinity, ...this.settings.cmsettings, ...this.settings.lhs_cmsettings, // these override any user-defined CodeMirror settings @@ -215,7 +215,7 @@ CodeMirrorDiffView.prototype.scrollToDiff = function(direction) { this._current_diff = Math.max(--this._current_diff, 0); } } - if (this.settings._debug.includes('change')) { + if (this.settings._debug.includes('debug')) { trace('change', Timer.stop(), 'current-diff', this._current_diff); } this._scroll_to_change(this.changes[this._current_diff]); @@ -581,22 +581,25 @@ CodeMirrorDiffView.prototype.bind = function(el) { this.editor.lhs.on('change', (instance, ev) => { if (this.settings._debug.includes('event')) { - trace('event', Timer.stop(), 'change lhs', ev); + trace('event#lhs-change'); } if (!this.settings.autoupdate) { return; } this._changing(); + if (this.settings._debug.includes('event')) { + trace('event#lhs-change [emitted]'); + } }); this.editor.lhs.on('scroll', () => { if (this.settings._debug.includes('event')) { - trace('event', Timer.stop(), 'scroll lhs'); + trace('event#lhs-scroll'); } this._scrolling({ side: 'lhs', id: this.lhsId }); }); this.editor.rhs.on('change', (instance, ev) => { if (this.settings._debug.includes('event')) { - trace('event', Timer.stop(), 'change rhs', ev); + trace('event#lhs-change'); } if (!this.settings.autoupdate) { return; @@ -605,7 +608,7 @@ CodeMirrorDiffView.prototype.bind = function(el) { }); this.editor.rhs.on('scroll', () => { if (this.settings._debug.includes('event')) { - trace('event', Timer.stop(), 'scroll rhs'); + trace('event#rhs-scroll'); } this._scrolling({ side: 'rhs', id: this.rhsId }); }); @@ -614,13 +617,16 @@ CodeMirrorDiffView.prototype.bind = function(el) { let resizeTimeout; const resize = () => { if (this.settings._debug.includes('event')) { - traceTimeStart('resize'); + traceTimeStart('event#resize'); + trace('event#resize [start]'); } this.resize(); - this.editor.lhs.refresh(); - this.editor.rhs.refresh(); + console.log('wtf refresh? >'); + // this.editor.lhs.refresh(); + // this.editor.rhs.refresh(); + console.log('wtf refresh? <'); if (this.settings._debug.includes('event')) { - traceTimeEnd('resize'); + traceTimeEnd('event#resize'); } }; this._handleResize = () => { @@ -630,9 +636,6 @@ CodeMirrorDiffView.prototype.bind = function(el) { resizeTimeout = setTimeout(resize, this.settings.resize_timeout); }; window.addEventListener('resize', () => { - if (this.settings._debug.includes('event')) { - trace('event', Timer.stop(), 'resize'); - } this._handleResize(); }); resize(); @@ -743,7 +746,7 @@ CodeMirrorDiffView.prototype._get_colors = function() { bg: cStyle.backgroundColor }; cColor.remove(); - if (this.settings._debug.includes('draw')) { + if (this.settings._debug.includes('debug')) { trace('draw', Timer.stop(), '_get_colors', this._colors); } } @@ -873,12 +876,14 @@ CodeMirrorDiffView.prototype._scrolling = function({ side, id }) { CodeMirrorDiffView.prototype._changing = function({ force } = { force: false }) { if (this.settings._debug.includes('change')) { - traceTimeStart('_changing'); + traceTimeStart('change#_changing'); + trace('change#_changing [start]'); } const handleChange = () => { this._changedTimeout = null; if (!force && !this.settings.autoupdate) { - if (this.settings._debug.includes('change')) { + if (this.settings._debug.includes('debug') + && this.settings._debug.includes('change')) { trace('change', Timer.stop(), '_changing', 'ignore change', force, this.settings.autoupdate); } return; @@ -886,7 +891,8 @@ CodeMirrorDiffView.prototype._changing = function({ force } = { force: false }) this._changed(); }; if (this.settings.change_timeout > 0) { - if (this.settings._debug.includes('change')) { + if (this.settings._debug.includes('debug') + && this.settings._debug.includes('change')) { trace('change', Timer.stop(), 'setting timeout', this.settings.change_timeout) } if (this._changedTimeout != null) { @@ -897,19 +903,21 @@ CodeMirrorDiffView.prototype._changing = function({ force } = { force: false }) handleChange(); } if (this.settings._debug.includes('change')) { - traceTimeEnd('_changing'); + traceTimeEnd('change#_changing'); } }; CodeMirrorDiffView.prototype._changed = function() { if (this.settings._debug.includes('change')) { - trace('change', Timer.stop(), '_changed', 'start'); - traceTimeStart('_changed'); + traceTimeStart('change#_changed'); + trace('change#_changed [start]'); } // NOTE: clear is handled by the beforeChange event this._diff(); + this._renderChanges(); + this.el.dispatchEvent(new Event('updated')); if (this.settings._debug.includes('change')) { - traceTimeEnd('_changed'); + traceTimeEnd('change#_changed'); } }; @@ -918,31 +926,30 @@ CodeMirrorDiffView.prototype._changed = function() { */ CodeMirrorDiffView.prototype._clear = function() { if (this.settings._debug.includes('draw')) { - traceTimeStart('_clear'); - trace('draw', Timer.stop(), '_clear', 'start'); + traceTimeStart('draw#_clear'); } this.changes = []; this._clearMarkup(); this._clearCanvases(); if (this.settings._debug.includes('draw')) { - traceTimeEnd('_clear'); + traceTimeEnd('draw#_clear'); } }; CodeMirrorDiffView.prototype._clearMarkup = function () { if (this.settings._debug.includes('draw')) { - traceTimeStart('_clearMarkup'); + traceTimeStart('draw#_clearMarkup'); } this._vdoc.clear(); this._vdoc = new VDoc(); if (this.settings._debug.includes('draw')) { - traceTimeEnd('_clearMarkup'); + traceTimeEnd('draw#_clearMarkup'); } } CodeMirrorDiffView.prototype._clearCanvases = function() { if (this.settings._debug.includes('draw')) { - traceTimeStart('_clearCanvases'); + traceTimeStart(' draw#_clearCanvases'); } const ex = this._draw_info(); @@ -968,37 +975,34 @@ CodeMirrorDiffView.prototype._clearCanvases = function() { ctx.clearRect(0, 0, this.draw_mid_width, ex.visible_page_height); if (this.settings._debug.includes('draw')) { - traceTimeEnd('_clearCanvases'); + traceTimeEnd(' draw#_clearCanvases'); } }; CodeMirrorDiffView.prototype._diff = function() { + if (this.settings._debug.includes('change')) { + traceTimeStart(' change#_diff'); + } const lhs = this.editor.lhs.getValue(); const rhs = this.editor.rhs.getValue(); - if (this.settings._debug.includes('diff')) { - trace('draw', Timer.stop(), '_diff', 'start'); - traceTimeStart('_diff'); - } const comparison = new diff(lhs, rhs, this.settings); this.changes = DiffParser(comparison.normal_form()); - if (this.settings._debug.includes('diff')) { - traceTimeEnd('_diff'); + if (this.settings._debug.includes('change')) { + traceTimeEnd(' change#_diff'); } - this._renderChanges(); - this.el.dispatchEvent(new Event('updated')); }; CodeMirrorDiffView.prototype._renderChanges = function() { if (this.settings._debug.includes('draw')) { - trace('draw', Timer.stop(), '_renderChanges', 'start'); - traceTimeStart('_renderChanges'); + traceTimeStart('draw#_renderChanges'); + trace('draw#_renderChanges [start]'); } this._clearCanvases(); this._calculateOffsets(this.changes); this._markupLineChanges(this.changes); this._renderDiff(this.changes); if (this.settings._debug.includes('draw')) { - traceTimeEnd('_renderChanges'); + traceTimeEnd('draw#_renderChanges'); } } @@ -1039,7 +1043,7 @@ CodeMirrorDiffView.prototype._set_top_offset = function (side) { CodeMirrorDiffView.prototype._calculateOffsets = function (changes) { if (this.settings._debug.includes('draw')) { - traceTimeStart('_calculateOffsets'); + traceTimeStart(' draw#_calculateOffsets'); } const { lhs: led, @@ -1136,13 +1140,13 @@ CodeMirrorDiffView.prototype._calculateOffsets = function (changes) { change['rhs-y-end'] += 0.5; } if (this.settings._debug.includes('draw')) { - traceTimeEnd('_calculateOffsets'); + traceTimeEnd(' draw#_calculateOffsets'); } } CodeMirrorDiffView.prototype._markupLineChanges = function (changes) { if (this.settings._debug.includes('draw')) { - traceTimeStart('_markupLineChanges'); + traceTimeStart(' draw#_markupLineChanges'); } const { lhs: led, @@ -1153,160 +1157,67 @@ CodeMirrorDiffView.prototype._markupLineChanges = function (changes) { const rhsvp = this._get_viewport_side('rhs'); const { _vdoc: vdoc } = this; - led.operation(() => { - for (let i = 0; i < changes.length; ++i) { - const change = changes[i]; - if (!this._isChangeInView('lhs', lhsvp, change)) { - // if the change is outside the viewport, skip - continue; - } - if (vdoc.hasRendered('lhs', i)) { - // already rendered - continue; - } + // use the virtual doc to markup all the changes + for (let i = 0; i < changes.length; ++i) { + const change = changes[i]; + const isCurrent = current_diff === i; + const lineDiff = this.settings.lcs !== false; + const lhsInView = this._isChangeInView('lhs', lhsvp, change); + const rhsInView = this._isChangeInView('rhs', rhsvp, change); + + // lhs changes + if (lhsInView) { + // the change is inside the viewport vdoc.addRender('lhs', change, i, { - isCurrent: current_diff === i, + isCurrent, + lineDiff, + // TODO: move out of loop getMergeHandler: (change, side, oside) => { return () => this._merge_change(change, side, oside); }, mergeButton: red.getOption('readOnly') ? null : this.merge_rhs_button.cloneNode(true) }); - vdoc.update('lhs', i, led); } - }); - red.operation(() => { - for (let i = 0; i < changes.length; ++i) { - const change = changes[i]; - if (!this._isChangeInView('rhs', rhsvp, change)) { - // if the change is outside the viewport, skip - continue; - } - if (vdoc.hasRendered('rhs', i)) { - // already rendered - continue; - } + // rhs changes + if (rhsInView) { vdoc.addRender('rhs', change, i, { - isCurrent: current_diff === i, + isCurrent, + lineDiff, + // TODO: move out of loop getMergeHandler: (change, side, oside) => { return () => this._merge_change(change, side, oside); }, mergeButton: led.getOption('readOnly') ? null : this.merge_lhs_button.cloneNode(true) }); - vdoc.update('rhs', i, red); - } - }); - - // mark text deleted, LCS changes - - function getMarkupFunc({ marktext, editor, lineNum, op }) { - return (from, to) => { - marktext.push([ - editor, - { line: lineNum, ch: from }, - { line: lineNum, ch: to }, - { className: `mergely ch ${op}` } - ]); - } - } - const marktext = []; - for (let i = 0; this.settings.lcs && i < changes.length; ++i) { - const change = changes[i]; - const llf = change['lhs-line-from'] >= 0 ? change['lhs-line-from'] : 0; - const llt = change['lhs-line-to'] >= 0 ? change['lhs-line-to'] : 0; - const rlf = change['rhs-line-from'] >= 0 ? change['rhs-line-from'] : 0; - const rlt = change['rhs-line-to'] >= 0 ? change['rhs-line-to'] : 0; - - if (!this._isChangeInView('lhs', lhsvp, change) - && !this._isChangeInView('lhs', rhsvp, change)) { - continue; } - if (change.op == 'd') { - // apply delete to cross-out (left-hand side only) - const from = llf; - const to = llt; - - // the change is within the viewport - const to_ln = led.lineInfo(to); - if (to_ln) { - marktext.push([led, {line:from, ch:0}, {line:to, ch:to_ln.text.length}, {className: 'mergely ch d lhs'}]); - } - } - else if (change.op == 'c') { - // apply LCS changes to each line - for (let j = llf, k = rlf; - ((j >= 0) && (j <= llt)) || ((k >= 0) && (k <= rlt)); - ++j, ++k) { - let lhs_line; - let rhs_line; - if (k > rlt) { - // lhs continues past rhs, mark lhs as deleted - lhs_line = led.getLine( j ); - if (!lhs_line) { - continue; + if (lineDiff && (lhsInView || rhsInView)) { + vdoc.addInlineDiff(change, { + getText: (side, lineNum) => { + if (side === 'lhs') { + return led.getLine(lineNum); + } else { + return red.getLine(lineNum); } - marktext.push([led, {line:j, ch:0}, {line:j, ch:lhs_line.length}, {className: 'mergely ch d lhs'}]); - continue; } - if (j > llt) { - // rhs continues past lhs, mark rhs as added - rhs_line = red.getLine( k ); - if (!rhs_line) { - continue; - } - marktext.push([red, {line:k, ch:0}, {line:k, ch:rhs_line.length}, {className: 'mergely ch a rhs'}]); - continue; - } - lhs_line = led.getLine( j ); - rhs_line = red.getLine( k ); - - if (this.settings._debug.includes('diff')) { - traceTimeStart(`diff lcs change: ${i}`); - } - - const lcs = new LCS(lhs_line, rhs_line, { - ignoreaccents: !!this.settings.ignoreaccents, - ignorews: !!this.settings.ignorews - }); - - lcs.diff( - getMarkupFunc({ marktext, editor: red, lineNum: k, op: 'a rhs' }), - getMarkupFunc({ marktext, editor: led, lineNum: j, op: 'd lhs' }) - ); - - if (this.settings._debug.includes('diff')) { - traceTimeEnd(`diff lcs change: ${i}`); - } - } + }); } } - - if (this.settings._debug.includes('draw')) { - trace('draw', Timer.stop(), '_markupLineChanges applying', marktext.length, 'markups'); - } - led.operation(() => { - // apply lhs markup - for (let i = 0; i < marktext.length; ++i) { - const m = marktext[i]; - if (m[0].doc.id != led.getDoc().id) continue; - this.chfns.lhs.push(m[0].markText(m[1], m[2], m[3])); + for (let i = 0; i < changes.length; ++i) { + vdoc.update('lhs', i, led, lhsvp); } }); red.operation(() => { - // apply lhs markup - for (let i = 0; i < marktext.length; ++i) { - const m = marktext[i]; - if (m[0].doc.id != red.getDoc().id) continue; - this.chfns.rhs.push(m[0].markText(m[1], m[2], m[3])); + for (let i = 0; i < changes.length; ++i) { + vdoc.update('rhs', i, red, rhsvp); } }); - if (this.settings._debug.includes('draw')) { - traceTimeEnd('_markupLineChanges'); + traceTimeEnd(' draw#_markupLineChanges'); } }; @@ -1389,6 +1300,9 @@ CodeMirrorDiffView.prototype._draw_info = function() { }; CodeMirrorDiffView.prototype._renderDiff = function(changes) { + if (this.settings._debug.includes('draw')) { + traceTimeStart(' draw#_renderDiff'); + } const ex = this._draw_info(); const mcanvas_lhs = ex.lhs_margin; const mcanvas_rhs = ex.rhs_margin; @@ -1396,7 +1310,7 @@ CodeMirrorDiffView.prototype._renderDiff = function(changes) { const ctx_lhs = mcanvas_lhs.getContext('2d'); const ctx_rhs = mcanvas_rhs.getContext('2d'); - if (this.settings._debug.includes('draw')) { + if (this.settings._debug.includes('debug')) { trace('draw', Timer.stop(), '_renderDiff', 'visible page height', ex.visible_page_height); trace('draw', Timer.stop(), '_renderDiff', 'scroller-top lhs', ex.lhs_scroller.scrollTop); trace('draw', Timer.stop(), '_renderDiff', 'scroller-top rhs', ex.rhs_scroller.scrollTop); @@ -1431,7 +1345,7 @@ CodeMirrorDiffView.prototype._renderDiff = function(changes) { } // draw margin indicators - if (this.settings._debug.includes('draw1')) { + if (this.settings._debug.includes('debug')) { trace('draw', Timer.stop(), '_renderDiff', 'draw1', 'marker', lhs_y_start, lhs_y_end, rhs_y_start, rhs_y_end); } @@ -1548,6 +1462,10 @@ CodeMirrorDiffView.prototype._renderDiff = function(changes) { }; ex.lhs_margin.addEventListener('click', this._handleLhsMarginClick); ex.rhs_margin.addEventListener('click', this._handleRhsMarginClick); + + if (this.settings._debug.includes('draw')) { + traceTimeEnd(' draw#_renderDiff'); + } }; CodeMirrorDiffView.prototype.trace = function(name) { diff --git a/src/vdoc.js b/src/vdoc.js index bbf2b3d..01d7829 100644 --- a/src/vdoc.js +++ b/src/vdoc.js @@ -1,3 +1,5 @@ +const LCS = require('./lcs'); + class VDoc { constructor() { this._lines = { @@ -10,54 +12,127 @@ class VDoc { }; } - hasRendered(side, changeId) { + hasRenderedChange(side, changeId) { return !!this._rendered[side][changeId]; } - addRender(side, change, changeId, { isCurrent, mergeButton, getMergeHandler }) { - this.setRendered(side, changeId); + addRender(side, change, changeId, options) { + const { + isCurrent, + lineDiff, + mergeButton, + getMergeHandler + } = options; + + if (this.hasRenderedChange(side, change)) { + return; + } const oside = (side === 'lhs') ? 'rhs' : 'lhs'; const bgClass = [ 'mergely', side, change.op, `cid-${changeId}` ]; - const lf = change[`${side}-line-from`] >= 0 ? change[`${side}-line-from`] : 0; - const lt = change[`${side}-line-to`] >= 0 ? change[`${side}-line-to`] : 0; - const olf = change[`${oside}-line-from`] >= 0 ? change[`${oside}-line-from`] : 0; + const { lf, lt, olf, olt } = getExtents(side, change); if (change[lf] < 0) { bgClass.push('empty'); } - this.getLine(side, lf).addLineClass('background', 'start'); - this.getLine(side, lt).addLineClass('background', 'end'); + this._getLine(side, lf).addLineClass('background', 'start'); + this._getLine(side, lt).addLineClass('background', 'end'); if (isCurrent) { if (lf != lt) { - this.getLine(side, lf).addLineClass('background', 'current'); + this._getLine(side, lf).addLineClass('background', 'current'); } - this.getLine(side, lt).addLineClass('background', 'current'); + this._getLine(side, lt).addLineClass('background', 'current'); for (let j = lf; j <= lt; ++j) { - this.getLine(side, j).addLineClass('gutter', 'mergely current'); + this._getLine(side, j).addLineClass('gutter', 'mergely current'); } } + if (lf == 0 && lt == 0 && olf == 0) { - this.getLine(side, lf).addLineClass('background', bgClass.join(' ')); - this.getLine(side, lf).addLineClass('background', 'first'); + // test if this change is the first line of the side + this._getLine(side, lf).addLineClass('background', bgClass.join(' ')); + this._getLine(side, lf).addLineClass('background', 'first'); + // FIXME: need lineDiff here? } else { // apply change for each line in-between the changed lines for (let j = lf; j <= lt; ++j) { - this.getLine(side, j).addLineClass('background', bgClass.join(' ')); + this._getLine(side, j).addLineClass('background', bgClass.join(' ')); + + if (!lineDiff) { + // inner line diffs are disabled, skip the rest + continue; + } + + if (side === 'lhs' + && (change.op === 'd' + || (change.op === 'c' && j > olt) + )) { + // mark entire line text with deleted (strikeout) if the + // change is a delete, or if it is changed text and the + // line goes past the end of the other side. + this._getLine(side, j).markText(0, undefined, 'mergely ch d lhs'); + } else if (side == 'rhs' + && (change.op === 'a' + || (change.op === 'c' && j > olt) + )) { + // mark entire line text with added if the change is an + // add, or if it is changed text and the line goes past the + // end of the other side. + this._getLine(side, j).markText(0, undefined, 'mergely ch a rhs'); + } } } + if (mergeButton) { mergeButton.className = `merge-button merge-${oside}-button`; const handler = getMergeHandler(change, side, oside); - this.getLine(side, lf).addMergeButton('merge', mergeButton, handler); + this._getLine(side, lf).addMergeButton('merge', mergeButton, handler); + } + this._setRenderedChange(side, changeId); + } + + addInlineDiff(change, { getText }) { + const { lf, lt, olf, olt } = getExtents('lhs', change); + const vdoc = this; + + for (let j = lf, k = olf; + ((j >= 0) && (j <= lt)) || ((k >= 0) && (k <= olt)); + ++j, ++k) { + // if both lhs line and rhs are within the change range with + // respect to each other, do inline diff. + if (j <= lt && k <= olt) { + const lhsText = getText('lhs', j); + const rhsText = getText('rhs', k); + + // TODO: there is an LCS performance gain here if either side + // is empty. + const lcs = new LCS(lhsText, rhsText, { + // TODO + ignoreaccents: false, + ignorews: false + }); + + // TODO: there might be an LCS performance gain here to move + // function declaration out of loop, but there shouldn't be + // too many lines here. it's no more than the viewport. + lcs.diff( + function _added(from, to) { + const line = vdoc._getLine('rhs', k); + line.markText(from, to, 'mergely ch a rhs'); + }, + function _removed(from, to) { + const line = vdoc._getLine('lhs', j); + line.markText(from, to, 'mergely ch d lhs'); + } + ); + } } } - setRendered(side, changeId) { + _setRenderedChange(side, changeId) { return this._rendered[side][changeId] = true; } - getLine(side, id) { + _getLine(side, id) { let line = this._lines[side][id]; if (line) { return line; @@ -67,12 +142,19 @@ class VDoc { return line; } - update(side, changeId, editor) { - this.setRendered(side, changeId); + update(side, changeId, editor, viewport) { + this._setRenderedChange(side, changeId); const lines = Object.keys(this._lines[side]); for (let i = 0; i < lines.length; ++i) { const id = lines[i]; - const vline = this.getLine(side, id); + if (id < viewport.from || id > viewport.to) { + continue; + } + + const vline = this._getLine(side, id); + if (vline.rendered) { + continue; + } vline.update(editor); } } @@ -84,13 +166,6 @@ class VDoc { for (const lineId in this._lines.rhs) { this._lines.rhs[lineId].clear(); } - - // for (let i = 0; i < Object.keys(this._lines.lhs).length; ++i) { - // this._lines.lhs[i].clear(); - // } - // for (let i = 0; i < Object.keys(this._lines.rhs).length; ++i) { - // this._lines.rhs[i].clear(); - // } } } @@ -101,6 +176,8 @@ class VLine { this.gutter = new Set(); this.marker = null; this.editor = null; + this.markup = []; + this.rendered = false; } addLineClass(location, clazz) { @@ -111,7 +188,16 @@ class VLine { this.marker = [ name, item, handler ]; } + markText(charFrom, charTo, className) { + this.markup = [ charFrom, charTo, className ]; + } + update(editor) { + if (this.rendered) { + // FIXME: probably do not need this now + console.log('already rendered', this.id); + return; + } this.editor = editor; if (this.background.size) { const clazz = Array.from(this.background).join(' '); @@ -126,10 +212,28 @@ class VLine { item.addEventListener('click', handler); editor.setGutterMarker(this.id, name, item); } + if (this.markup) { + const [ charFrom, charTo, className ] = this.markup; + const fromPos = { line: this.id }; + const toPos = { line: this.id }; + if (charFrom >= 0) { + fromPos.ch = charFrom; + } + if (charTo >= 0) { + toPos.ch = charTo; + } + this._clearMarkup = editor.markText(fromPos, toPos, { className }); + } + this.rendered = true; } clear() { const { editor } = this; + if (!this.rendered) { + return; + } + + console.warn('clear editor', this.id); if (this.background) { editor.removeLineClass(this.id, 'background'); } @@ -143,7 +247,20 @@ class VLine { item.removeEventListener('click', handler); item.remove(); } + if (this._clearMarkup) { + this._clearMarkup.clear(); + } } } +function getExtents(side, change) { + const oside = (side === 'lhs') ? 'rhs' : 'lhs'; + return { + lf: change[`${side}-line-from`] >= 0 ? change[`${side}-line-from`] : 0, + lt: change[`${side}-line-to`] >= 0 ? change[`${side}-line-to`] : 0, + olf: change[`${oside}-line-from`] >= 0 ? change[`${oside}-line-from`] : 0, + olt: change[`${oside}-line-to`] >= 0 ? change[`${oside}-line-to`] : 0 + }; +} + module.exports = VDoc;