2019-08-01 12:25:04 +03:00
|
|
|
/*
|
2024-09-09 16:57:16 +03:00
|
|
|
Copyright 2024 New Vector Ltd.
|
2019-08-01 12:25:04 +03:00
|
|
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
|
|
|
2024-09-09 16:57:16 +03:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
|
|
Please see LICENSE files in the repository root for full details.
|
2019-08-01 12:25:04 +03:00
|
|
|
*/
|
|
|
|
|
2020-07-15 11:45:45 +03:00
|
|
|
import EditorModel from "./model";
|
2021-06-29 15:11:58 +03:00
|
|
|
import { IDiff } from "./diff";
|
|
|
|
import { SerializedPart } from "./parts";
|
|
|
|
import { Caret } from "./caret";
|
2020-07-15 11:45:45 +03:00
|
|
|
|
2023-03-23 16:35:55 +03:00
|
|
|
export interface IHistory {
|
2020-07-20 18:33:53 +03:00
|
|
|
parts: SerializedPart[];
|
2023-05-16 16:25:43 +03:00
|
|
|
caret?: Caret;
|
2020-07-15 11:45:45 +03:00
|
|
|
}
|
|
|
|
|
2019-08-01 17:10:21 +03:00
|
|
|
export const MAX_STEP_LENGTH = 10;
|
|
|
|
|
2019-08-01 12:25:04 +03:00
|
|
|
export default class HistoryManager {
|
2020-07-15 11:45:45 +03:00
|
|
|
private stack: IHistory[] = [];
|
|
|
|
private newlyTypedCharCount = 0;
|
|
|
|
private currentIndex = -1;
|
|
|
|
private changedSinceLastPush = false;
|
2023-05-16 16:25:43 +03:00
|
|
|
private lastCaret?: Caret;
|
2020-07-15 11:45:45 +03:00
|
|
|
private nonWordBoundarySinceLastPush = false;
|
|
|
|
private addedSinceLastPush = false;
|
|
|
|
private removedSinceLastPush = false;
|
2019-08-20 18:15:52 +03:00
|
|
|
|
2021-07-12 15:26:34 +03:00
|
|
|
public clear(): void {
|
2020-07-15 11:45:45 +03:00
|
|
|
this.stack = [];
|
|
|
|
this.newlyTypedCharCount = 0;
|
|
|
|
this.currentIndex = -1;
|
|
|
|
this.changedSinceLastPush = false;
|
2023-05-16 16:25:43 +03:00
|
|
|
this.lastCaret = undefined;
|
2020-07-15 11:45:45 +03:00
|
|
|
this.nonWordBoundarySinceLastPush = false;
|
|
|
|
this.addedSinceLastPush = false;
|
|
|
|
this.removedSinceLastPush = false;
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
|
2023-03-16 14:07:29 +03:00
|
|
|
private shouldPush(inputType?: string, diff?: IDiff): boolean {
|
2019-08-02 12:31:01 +03:00
|
|
|
// right now we can only push a step after
|
|
|
|
// the input has been applied to the model,
|
|
|
|
// so we can't push the state before something happened.
|
|
|
|
// not ideal but changing this would be harder to fit cleanly into
|
|
|
|
// the editor model.
|
|
|
|
const isNonBulkInput =
|
|
|
|
inputType === "insertText" || inputType === "deleteContentForward" || inputType === "deleteContentBackward";
|
|
|
|
if (diff && isNonBulkInput) {
|
|
|
|
if (diff.added) {
|
2020-07-15 11:45:45 +03:00
|
|
|
this.addedSinceLastPush = true;
|
2019-08-02 12:31:01 +03:00
|
|
|
}
|
2019-08-01 12:25:04 +03:00
|
|
|
if (diff.removed) {
|
2020-07-15 11:45:45 +03:00
|
|
|
this.removedSinceLastPush = true;
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
2019-08-02 12:31:01 +03:00
|
|
|
// as long as you've only been adding or removing since the last push
|
2020-07-15 11:45:45 +03:00
|
|
|
if (this.addedSinceLastPush !== this.removedSinceLastPush) {
|
2019-08-02 12:31:01 +03:00
|
|
|
// add steps by word boundary, up to MAX_STEP_LENGTH characters
|
2023-02-03 18:27:47 +03:00
|
|
|
const str = diff.added ? diff.added : diff.removed!;
|
2019-08-02 12:31:01 +03:00
|
|
|
const isWordBoundary = str === " " || str === "\t" || str === "\n";
|
2020-07-15 11:45:45 +03:00
|
|
|
if (this.nonWordBoundarySinceLastPush && isWordBoundary) {
|
2019-08-02 12:31:01 +03:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (!isWordBoundary) {
|
2020-07-15 11:45:45 +03:00
|
|
|
this.nonWordBoundarySinceLastPush = true;
|
2019-08-02 12:31:01 +03:00
|
|
|
}
|
2020-07-15 11:45:45 +03:00
|
|
|
this.newlyTypedCharCount += str.length;
|
|
|
|
return this.newlyTypedCharCount > MAX_STEP_LENGTH;
|
2019-08-02 12:31:01 +03:00
|
|
|
} else {
|
|
|
|
// if starting to remove while adding before, or the opposite, push
|
|
|
|
return true;
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
} else {
|
2019-08-02 12:31:01 +03:00
|
|
|
// bulk input (paste, ...) should be pushed every time
|
2019-08-01 12:25:04 +03:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-16 16:25:43 +03:00
|
|
|
private pushState(model: EditorModel, caret?: Caret): void {
|
2019-08-01 12:25:04 +03:00
|
|
|
// remove all steps after current step
|
2020-07-15 11:45:45 +03:00
|
|
|
while (this.currentIndex < this.stack.length - 1) {
|
|
|
|
this.stack.pop();
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
const parts = model.serializeParts();
|
2021-06-29 15:11:58 +03:00
|
|
|
this.stack.push({ parts, caret });
|
2020-07-15 11:45:45 +03:00
|
|
|
this.currentIndex = this.stack.length - 1;
|
2023-05-16 16:25:43 +03:00
|
|
|
this.lastCaret = undefined;
|
2020-07-15 11:45:45 +03:00
|
|
|
this.changedSinceLastPush = false;
|
|
|
|
this.newlyTypedCharCount = 0;
|
|
|
|
this.nonWordBoundarySinceLastPush = false;
|
|
|
|
this.addedSinceLastPush = false;
|
|
|
|
this.removedSinceLastPush = false;
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// needs to persist parts and caret position
|
2023-05-16 16:25:43 +03:00
|
|
|
public tryPush(model: EditorModel, caret?: Caret, inputType?: string, diff?: IDiff): boolean {
|
2019-08-01 12:25:04 +03:00
|
|
|
// ignore state restoration echos.
|
|
|
|
// these respect the inputType values of the input event,
|
|
|
|
// but are actually passed in from MessageEditor calling model.reset()
|
|
|
|
// in the keydown event handler.
|
|
|
|
if (inputType === "historyUndo" || inputType === "historyRedo") {
|
|
|
|
return false;
|
|
|
|
}
|
2020-07-15 11:45:45 +03:00
|
|
|
const shouldPush = this.shouldPush(inputType, diff);
|
2019-08-01 12:25:04 +03:00
|
|
|
if (shouldPush) {
|
2020-07-15 11:45:45 +03:00
|
|
|
this.pushState(model, caret);
|
2019-08-01 12:25:04 +03:00
|
|
|
} else {
|
2020-07-15 11:45:45 +03:00
|
|
|
this.lastCaret = caret;
|
|
|
|
this.changedSinceLastPush = true;
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
return shouldPush;
|
|
|
|
}
|
|
|
|
|
2021-07-12 15:26:34 +03:00
|
|
|
public ensureLastChangesPushed(model: EditorModel): void {
|
2023-03-23 16:35:55 +03:00
|
|
|
if (this.changedSinceLastPush && this.lastCaret) {
|
2020-07-15 11:45:45 +03:00
|
|
|
this.pushState(model, this.lastCaret);
|
2019-09-05 16:34:42 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-12 15:26:34 +03:00
|
|
|
public canUndo(): boolean {
|
2020-07-15 11:45:45 +03:00
|
|
|
return this.currentIndex >= 1 || this.changedSinceLastPush;
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
|
2021-07-12 15:26:34 +03:00
|
|
|
public canRedo(): boolean {
|
2020-07-15 11:45:45 +03:00
|
|
|
return this.currentIndex < this.stack.length - 1;
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// returns state that should be applied to model
|
2023-03-23 16:35:55 +03:00
|
|
|
public undo(model: EditorModel): IHistory | void {
|
2019-08-01 12:25:04 +03:00
|
|
|
if (this.canUndo()) {
|
2019-09-05 16:34:42 +03:00
|
|
|
this.ensureLastChangesPushed(model);
|
2020-07-15 11:45:45 +03:00
|
|
|
this.currentIndex -= 1;
|
|
|
|
return this.stack[this.currentIndex];
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// returns state that should be applied to model
|
2023-03-23 16:35:55 +03:00
|
|
|
public redo(): IHistory | void {
|
2019-08-01 12:25:04 +03:00
|
|
|
if (this.canRedo()) {
|
2020-07-15 11:45:45 +03:00
|
|
|
this.changedSinceLastPush = false;
|
|
|
|
this.currentIndex += 1;
|
|
|
|
return this.stack[this.currentIndex];
|
2019-08-01 12:25:04 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|