170 lines
4.8 KiB
TypeScript
170 lines
4.8 KiB
TypeScript
import wasmInit, {
|
|
CursorPosition as wasmCursorPosition,
|
|
reconcile as wasmReconcile,
|
|
TextWithCursors as wasmTextWithCursors,
|
|
SpanWithHistory as wasmSpanWithHistory,
|
|
BuiltinTokenizer,
|
|
reconcileWithHistory as wasmReconcileWithHistory,
|
|
History,
|
|
initSync,
|
|
} from 'reconcile';
|
|
|
|
import wasm from 'reconcile/reconcile_bg.wasm';
|
|
|
|
export interface TextWithCursors {
|
|
/** The document's entire content */
|
|
text: string;
|
|
/** List of cursor positions, can be null or undefined if there are no cursors */
|
|
cursors: null | undefined | CursorPosition[];
|
|
}
|
|
|
|
export interface CursorPosition {
|
|
/** Unique identifier for the cursor */
|
|
id: number;
|
|
/** Character position in the text, 0-based */
|
|
position: number;
|
|
}
|
|
|
|
export interface TextWithCursorsAndHistory {
|
|
/** The document's entire content */
|
|
text: string;
|
|
/** List of cursor positions, can be null or undefined if there are no cursors */
|
|
cursors: null | undefined | CursorPosition[];
|
|
/** List of operations leading to `text` from the 3 ancestors */
|
|
history: SpanWithHistory[];
|
|
}
|
|
|
|
export interface SpanWithHistory {
|
|
/** Span of text associated with the historical opearion */
|
|
text: string;
|
|
/** Origin of the `text` span */
|
|
history: History;
|
|
}
|
|
|
|
export type Tokenizer = 'Line' | 'Word' | 'Character';
|
|
const TOKENIZERS = ['Line', 'Word', 'Character'];
|
|
|
|
let isInitialised = false;
|
|
|
|
const UNSUPPORTED_TOKENIZER_ERROR = `Unsupported tokenizer. Only ${TOKENIZERS.join(
|
|
', '
|
|
)} are supported.`;
|
|
|
|
/**
|
|
* Merges three versions of text (original, left, right) and cursor positions.
|
|
*
|
|
* @param original - The original/base version of the text
|
|
* @param left - The left version of the text, either as string or TextWithCursors
|
|
* @param right - The right version of the text, either as string or TextWithCursors
|
|
* @param tokenizer - The tokenization strategy to use (default: "Word")
|
|
* @returns The reconciled text with merged cursor positions
|
|
*/
|
|
export function reconcile(
|
|
original: string,
|
|
left: string | TextWithCursors,
|
|
right: string | TextWithCursors,
|
|
tokenizer: BuiltinTokenizer = 'Word'
|
|
): TextWithCursors {
|
|
init();
|
|
|
|
if (!TOKENIZERS.includes(tokenizer)) {
|
|
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
|
|
}
|
|
|
|
const leftCursor = toWasmTextWithCursors(left);
|
|
const rightCursor = toWasmTextWithCursors(right);
|
|
|
|
const result = wasmReconcile(original, leftCursor, rightCursor, tokenizer);
|
|
|
|
leftCursor.free();
|
|
rightCursor.free();
|
|
|
|
const jsResult = toTextWithCursors(result);
|
|
result.free();
|
|
|
|
return jsResult;
|
|
}
|
|
|
|
/**
|
|
* Merges three versions of text and returns the result with historical information.
|
|
*
|
|
* Calculating the `history` is somewhat more expensive, otherwise this function behaves like `reconcile`.
|
|
*
|
|
* @param original - The original/base version of the text
|
|
* @param left - The left version of the text, either as string or TextWithCursors
|
|
* @param right - The right version of the text, either as string or TextWithCursors
|
|
* @param tokenizer - The tokenization strategy to use (default: "Word")
|
|
* @returns The reconciled text with cursor positions and history of changes
|
|
*/
|
|
export function reconcileWithHistory(
|
|
original: string,
|
|
left: string | TextWithCursors,
|
|
right: string | TextWithCursors,
|
|
tokenizer: BuiltinTokenizer = 'Word'
|
|
): TextWithCursorsAndHistory {
|
|
init();
|
|
|
|
if (!TOKENIZERS.includes(tokenizer)) {
|
|
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
|
|
}
|
|
|
|
const leftCursor = toWasmTextWithCursors(left);
|
|
const rightCursor = toWasmTextWithCursors(right);
|
|
|
|
const result = wasmReconcileWithHistory(original, leftCursor, rightCursor, tokenizer);
|
|
|
|
leftCursor.free();
|
|
rightCursor.free();
|
|
|
|
const jsResult = toTextWithCursors(result);
|
|
const history = result.history().map(toSpanWithHistory);
|
|
result.free();
|
|
|
|
return {
|
|
...jsResult,
|
|
history,
|
|
};
|
|
}
|
|
|
|
function init() {
|
|
if (isInitialised) {
|
|
return;
|
|
}
|
|
|
|
initSync({ module: (wasm as any).default });
|
|
|
|
isInitialised = true;
|
|
}
|
|
|
|
function toWasmTextWithCursors(text: string | TextWithCursors): wasmTextWithCursors {
|
|
const isInputString = typeof text == 'string';
|
|
const leftText = isInputString ? text : text.text;
|
|
const leftCursors = isInputString ? [] : (text.cursors ?? []);
|
|
|
|
return new wasmTextWithCursors(leftText, leftCursors.map(toWasmCursorPosition));
|
|
}
|
|
|
|
function toWasmCursorPosition({ id, position }: CursorPosition): wasmCursorPosition {
|
|
return new wasmCursorPosition(id, position);
|
|
}
|
|
|
|
function toTextWithCursors(textWithCursor: wasmTextWithCursors): TextWithCursors {
|
|
return {
|
|
text: textWithCursor.text(),
|
|
cursors: textWithCursor.cursors().map(toCursorPosition),
|
|
};
|
|
}
|
|
|
|
function toCursorPosition(cursor: wasmCursorPosition): CursorPosition {
|
|
return {
|
|
id: cursor.id(),
|
|
position: cursor.characterIndex(),
|
|
};
|
|
}
|
|
|
|
function toSpanWithHistory(textWithHistory: wasmSpanWithHistory): SpanWithHistory {
|
|
return {
|
|
text: textWithHistory.text(),
|
|
history: textWithHistory.history(),
|
|
};
|
|
}
|