forked from lxm_front/Mergely
267 lines
6.8 KiB
JavaScript
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;
|