Files
Mergely/src/vdoc.js

267 lines
6.8 KiB
JavaScript

const LCS = require('./lcs');
class VDoc {
constructor() {
this._lines = {
lhs: {},
rhs: {}
};
this._rendered = {
lhs: {},
rhs: {}
};
}
hasRenderedChange(side, changeId) {
return !!this._rendered[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, 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');
if (isCurrent) {
if (lf != lt) {
this._getLine(side, lf).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');
}
}
if (lf == 0 && lt == 0 && olf == 0) {
// 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(' '));
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._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');
}
);
}
}
}
_setRenderedChange(side, changeId) {
return this._rendered[side][changeId] = true;
}
_getLine(side, id) {
let line = this._lines[side][id];
if (line) {
return line;
}
line = new VLine(id);
this._lines[side][id] = line;
return line;
}
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];
if (id < viewport.from || id > viewport.to) {
continue;
}
const vline = this._getLine(side, id);
if (vline.rendered) {
continue;
}
vline.update(editor);
}
}
clear() {
for (const lineId in this._lines.lhs) {
this._lines.lhs[lineId].clear();
}
for (const lineId in this._lines.rhs) {
this._lines.rhs[lineId].clear();
}
}
}
class VLine {
constructor(id) {
this.id = id;
this.background = new Set();
this.gutter = new Set();
this.marker = null;
this.editor = null;
this.markup = [];
this.rendered = false;
}
addLineClass(location, clazz) {
this[location].add(clazz);
}
addMergeButton(name, item, handler) {
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(' ');
editor.addLineClass(this.id, 'background', clazz);
}
if (this.gutter.size) {
const clazz = Array.from(this.gutter).join(' ');
editor.addLineClass(this.id, 'gutter', clazz);
}
if (this.marker) {
const [ name, item, handler ] = this.marker;
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');
}
if (this.gutter) {
editor.removeLineClass(this.id, 'gutter');
}
if (this.marker) {
const [ , item, handler ] = this.marker;
// set with `null` to clear marker
editor.setGutterMarker(this.id, name, null);
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;