1
0
mirror of synced 2025-12-14 02:21:47 +08:00
Files
Mergely/src/vdoc.js

356 lines
9.2 KiB
JavaScript

const diff = require('./diff');
const trace = console.log;
const expLetters = new RegExp(
/\p{Letter}\p{Mark}*|\p{Number}\p{Mark}*|\p{Punctuation}\p{Mark}*|\p{Symbol}\p{Mark}*|\p{White_Space}/gu
);
class VDoc {
constructor(options) {
this.options = options;
this._lines = {
lhs: {},
rhs: {}
};
this._rendered = {
lhs: {},
rhs: {}
};
}
addRender(side, change, changeId, options) {
if (this.options._debug) {
trace('vdoc#addRender', side, changeId, change);
}
const {
isCurrent,
lineDiff,
mergeButton,
getMergeHandler
} = options;
const alreadyRendered = !!this._rendered[side][changeId];
if (alreadyRendered) {
if (this.options._debug) {
trace('vdoc#addRender (already rendered)', side, changeId, change);
}
return;
}
const oside = (side === 'lhs') ? 'rhs' : 'lhs';
const bgClass = [ 'mergely', side, `cid-${changeId}` ];
const { lf, lt, olf } = getExtents(side, change);
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');
}
}
const bgChangeOp = {
lhs: {
d: 'd',
a: 'd',
c: 'c'
},
rhs: {
d: 'a',
a: 'a',
c: 'c'
}
}[ side ][ change.op ];
if (lf < 0) {
// If this is the first, line it has start but no end
bgClass.push('start');
bgClass.push('no-end');
bgClass.push(bgChangeOp);
this._getLine(side, 0).addLineClass('background', bgClass.join(' '));
this._setRenderedChange(side, changeId);
return;
}
if (side === 'lhs' && change['lhs-y-start'] === change['lhs-y-end']) {
// if lhs, and start/end are the same, it has end but no-start
bgClass.push('no-start');
bgClass.push('end');
bgClass.push(bgChangeOp);
this._getLine(side, lf).addLineClass('background', bgClass.join(' '));
this._setRenderedChange(side, changeId);
return;
}
if (side === 'rhs' && change['rhs-y-start'] === change['rhs-y-end']) {
// if rhs, and start/end are the same, it has end but no-start
bgClass.push('no-start');
bgClass.push('end');
bgClass.push(bgChangeOp);
this._getLine(side, lf).addLineClass('background', bgClass.join(' '));
this._setRenderedChange(side, changeId);
return;
}
this._getLine(side, lf).addLineClass('background', 'start');
this._getLine(side, lt).addLineClass('background', 'end');
for (let j = lf, k = olf; lf !== -1 && lt !== -1 && j <= lt; ++j, ++k) {
this._getLine(side, j).addLineClass('background', bgChangeOp);
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')) {
// 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 cid-${changeId}`);
} else if (side === 'rhs' && (change.op === 'a')) {
// 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 cid-${changeId}`);
}
}
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, changeId, { getText, ignorews, ignoreaccents, ignorecase }) {
if (this.options._debug) {
trace('vdoc#addInlineDiff', changeId, change);
}
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);
const alreadyRendered
= !!this._getLine('lhs', j).markup.length
|| !!this._getLine('rhs', k).markup.length
if (alreadyRendered) {
continue;
}
const results = new diff(lhsText, rhsText, {
ignoreaccents,
ignorews,
ignorecase,
split: 'chars'
});
for (const change of results.changes()) {
const {
lhs_start,
lhs_deleted_count,
rhs_start,
rhs_inserted_count
} = change;
const lhs_to = lhs_start + lhs_deleted_count;
const rhs_to = rhs_start + rhs_inserted_count;
const lhs_line = vdoc._getLine('lhs', j);
lhs_line.markText(lhs_start, lhs_to, `mergely ch ind lhs cid-${changeId}`);
const rhs_line = vdoc._getLine('rhs', k);
rhs_line.markText(rhs_start, rhs_to, `mergely ch ina rhs cid-${changeId}`);
}
} else if (k > olt) {
// lhs has exceeded the max lines in the rhs editor, remainder are deleted
const line = vdoc._getLine('lhs', j);
line.markText(0, undefined, `mergely ch ind lhs cid-${changeId}`);
} else if (j > lt) {
// rhs has exceeded the max lines in the lhs editor, remainder are added
const line = vdoc._getLine('rhs', k);
line.markText(0, undefined, `mergely ch ina rhs cid-${changeId}`);
}
}
}
_setRenderedChange(side, changeId) {
if (this.options._debug) {
trace('vdoc#_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, editor, viewport) {
if (this.options._debug) {
trace('vdoc#update', side, editor, viewport);
}
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() {
if (this.options._debug) {
trace('vdoc#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._clearMarkup = [];
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.push([ 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;
editor.operation(() => {
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.length) {
// while Mergely diffs unicode chars (letters+mark), CM is by character,
// so diffs need to be mapped.
const mapped = mapLettersToChars(editor.getValue());
for (const markup of this.markup) {
const [ charFrom, charTo, className ] = markup;
const fromPos = { line: this.id };
const toPos = { line: this.id };
if (charFrom >= 0) {
fromPos.ch = mapped[charFrom];
}
if (charTo >= 0) {
toPos.ch = mapped[charTo];
}
this._clearMarkup.push(
editor.markText(fromPos, toPos, { className }));
}
}
});
this.rendered = true;
}
clear() {
const { editor } = this;
if (!this.rendered) {
return;
}
editor.operation(() => {
if (this.background) {
editor.removeLineClass(this.id, 'background');
}
if (this.gutter) {
editor.removeLineClass(this.id, 'gutter');
}
if (this.marker) {
const [ name, 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.length) {
for (const markup of this._clearMarkup) {
markup.clear();
}
this._clearMarkup = [];
this.markup = [];
}
});
}
}
function getExtents(side, change) {
const oside = (side === 'lhs') ? 'rhs' : 'lhs';
return {
lf: change[`${side}-line-from`],
lt: change[`${side}-line-to`],
olf: change[`${oside}-line-from`],
olt: change[`${oside}-line-to`]
};
}
function mapLettersToChars(text) {
let match;
let mapped = {};
let index = 0;
expLetters.lastIndex = 0;
while ((match = expLetters.exec(text)) !== null) {
mapped[index++] = match.index;
}
return mapped;
}
module.exports = VDoc;