mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move client code to core
Summary: This moves all client code to core, and makes minimal fix-ups to get grist and grist-core to compile correctly. The client works in core, but I'm leaving clean-up around the build and bundles to follow-up. Test Plan: existing tests pass; server-dev bundle looks sane Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2627
This commit is contained in:
128
app/client/widgets/DiffBox.ts
Normal file
128
app/client/widgets/DiffBox.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { DataRowModel } from 'app/client/models/DataRowModel';
|
||||
import { NewAbstractWidget } from 'app/client/widgets/NewAbstractWidget';
|
||||
import { CellValue } from 'app/common/DocActions';
|
||||
import { isVersions } from 'app/common/gristTypes';
|
||||
import { BaseFormatter } from 'app/common/ValueFormatter';
|
||||
import { Diff, DIFF_DELETE, DIFF_INSERT, diff_match_patch as DiffMatchPatch, DIFF_EQUAL } from 'diff-match-patch';
|
||||
import { Computed, dom } from 'grainjs';
|
||||
|
||||
/**
|
||||
*
|
||||
* A special widget used for rendering cell-level comparisons and conflicts.
|
||||
*
|
||||
*/
|
||||
export class DiffBox extends NewAbstractWidget {
|
||||
|
||||
private _diffTool = new DiffMatchPatch();
|
||||
|
||||
public buildConfigDom() {
|
||||
return dom('div');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a cell-level diff as a series of styled spans.
|
||||
*/
|
||||
public buildDom(row: DataRowModel) {
|
||||
const formattedValue = Computed.create(null, (use) => {
|
||||
if (use(row._isAddRow) || this.isDisposed() || use(this.field.displayColModel).isDisposed()) {
|
||||
// Work around JS errors during certain changes, following code in Reference.js
|
||||
return [] as Diff[];
|
||||
}
|
||||
const value = use(row.cells[use(use(this.field.displayColModel).colId)]);
|
||||
const formatter = use(this.valueFormatter);
|
||||
return this._prepareCellDiff(value, formatter);
|
||||
});
|
||||
return dom(
|
||||
'div.field_clip',
|
||||
dom.autoDispose(formattedValue),
|
||||
dom.style('text-align', this.options.prop('alignment')),
|
||||
dom.cls('text_wrapping', (use) => Boolean(use(this.options.prop('wrap')))),
|
||||
dom.forEach(formattedValue, ([code, txt]) => {
|
||||
if (code === DIFF_DELETE) {
|
||||
return dom("span.diff-parent", txt);
|
||||
} else if (code === DIFF_INSERT) {
|
||||
return dom("span.diff-remote", txt);
|
||||
} else if (code === DIFF_LOCAL) {
|
||||
return dom("span.diff-local", txt);
|
||||
} else {
|
||||
return dom("span.diff-common", txt);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the cell value and the formatter, construct a list of fragments in
|
||||
* diff-match-patch format expressing the difference between versions.
|
||||
* The format is a list of [CODE, STRING] pairs, where the possible values of
|
||||
* CODE are:
|
||||
* -1 -- meaning DELETION of the parent value.
|
||||
* 0 -- meaning text common to all versions.
|
||||
* 1 -- meaning INSERTION of the remote value.
|
||||
* 2 -- meaning INSERTION of the local value.
|
||||
*
|
||||
* When a change is made only locally or remotely, then the list returned may
|
||||
* include common text, deletions and insertions in any order.
|
||||
*
|
||||
* When a change is made both locally and remotely, the list returned does not
|
||||
* include any common text, but just reports the parent value, then the local value,
|
||||
* then the remote value. This may be optimized in future.
|
||||
*/
|
||||
private _prepareCellDiff(value: CellValue, formatter: BaseFormatter): Diff[] {
|
||||
if (!isVersions(value)) {
|
||||
// This can happen for reference columns, where the diff widget is
|
||||
// selected on the basis of one column, but we are displaying the
|
||||
// content of another. We have more version information for the
|
||||
// reference column than for its display column.
|
||||
return [[DIFF_EQUAL, formatter.format(value)]];
|
||||
}
|
||||
const versions = value[1];
|
||||
if (!('local' in versions)) {
|
||||
// Change was made remotely only.
|
||||
return this._prepareTextDiff(formatter.format(versions.parent),
|
||||
formatter.format(versions.remote));
|
||||
} else if (!('remote' in versions)) {
|
||||
// Change was made locally only.
|
||||
return this._prepareTextDiff(formatter.format(versions.parent),
|
||||
formatter.format(versions.local))
|
||||
.map(([code, txt]) => [code === DIFF_INSERT ? DIFF_LOCAL : code, txt]);
|
||||
}
|
||||
// Change was made both locally and remotely.
|
||||
return [[DIFF_DELETE, formatter.format(versions.parent)],
|
||||
[DIFF_LOCAL, formatter.format(versions.local)],
|
||||
[DIFF_INSERT, formatter.format(versions.remote)]];
|
||||
}
|
||||
|
||||
// Run diff-match-patch on the text, do its cleanup, and then some extra
|
||||
// ad-hoc cleanup of our own. Diffs are hard to read if they are too
|
||||
// "choppy".
|
||||
private _prepareTextDiff(txt1: string, txt2: string): Diff[] {
|
||||
const diffs = this._diffTool.diff_main(txt1, txt2);
|
||||
this._diffTool.diff_cleanupSemantic(diffs);
|
||||
if (diffs.length > 2 && this._notDiffWorthy(txt1, diffs.length) &&
|
||||
this._notDiffWorthy(txt2, diffs.length)) {
|
||||
return [[DIFF_DELETE, txt1], [DIFF_INSERT, txt2]];
|
||||
}
|
||||
if (diffs.length === 1 && diffs[0][0] === DIFF_DELETE) {
|
||||
// Add an empty set symbol, since otherwise it will be ambiguous
|
||||
// whether the deletion was done locally or remotely.
|
||||
diffs.push([1, '\u2205']);
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
|
||||
// Heuristic for whether to show common parts of versions, or to treat them
|
||||
// as entirely distinct.
|
||||
private _notDiffWorthy(txt: string, parts: number) {
|
||||
return txt.length < 5 * parts || this._isMostlyNumeric(txt);
|
||||
}
|
||||
|
||||
// Check is text has a lot of numeric content.
|
||||
private _isMostlyNumeric(txt: string) {
|
||||
return [...txt].filter(c => c >= '0' && c <= '9').length > txt.length / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// A constant marking text fragments present locally but not in parent (or remote).
|
||||
// Must be distinct from DiffMatchPatch.DIFF_* constants (-1, 0, 1).
|
||||
const DIFF_LOCAL = 2;
|
||||
Reference in New Issue
Block a user