alex/auto-undo-redo: auto undo-redo

alex/no-batches
alex 2024-04-09 13:43:49 +01:00
rodzic b4f26556bf
commit 6d2018c897
29 zmienionych plików z 231 dodań i 124 usunięć

Wyświetl plik

@ -57,6 +57,7 @@ import {
sortByIndex,
structuredClone,
} from '@tldraw/utils'
import { setTimeout } from 'core-js'
import { EventEmitter } from 'eventemitter3'
import { flushSync } from 'react-dom'
import { createRoot } from 'react-dom/client'
@ -552,9 +553,9 @@ export class Editor extends EventEmitter<TLEventMap> {
])
}
},
beforeDelete: (record) => {
afterDelete: (record) => {
// page was deleted, need to check whether it's the current page and select another one if so
if (this.getInstanceState().currentPageId === record.id) {
if (this.getInstanceState()?.currentPageId === record.id) {
const backupPageId = this.getPages().find((p) => p.id !== record.id)?.id
if (backupPageId) {
this.store.put([{ ...this.getInstanceState(), currentPageId: backupPageId }])
@ -570,7 +571,7 @@ export class Editor extends EventEmitter<TLEventMap> {
},
},
instance: {
beforeChange: (prev, next) => {
afterChange: (prev, next) => {
// instance should never be updated to a page that no longer exists (this can
// happen when undoing a change that involves switching to a page that has since
// been deleted by another user)
@ -579,14 +580,15 @@ export class Editor extends EventEmitter<TLEventMap> {
? prev.currentPageId
: this.getPages()[0]?.id
if (backupPageId) {
return { ...next, currentPageId: backupPageId }
this.store.update(next.id, (instance) => ({
...instance,
currentPageId: backupPageId,
}))
} else {
// if there are no pages, bail out to `ensureStoreIsUsable`
this.store.ensureStoreIsUsable()
return this.getInstanceState()
}
}
return next
},
},
instance_page_state: {
@ -651,17 +653,16 @@ export class Editor extends EventEmitter<TLEventMap> {
)
this.disposables.add(this.history.dispose)
this.store.ensureStoreIsUsable()
this.history.ephemeral(() => {
this.store.ensureStoreIsUsable()
// clear ephemeral state
this._updateCurrentPageState(
{
// clear ephemeral state
this._updateCurrentPageState({
editingShapeId: null,
hoveredShapeId: null,
erasingShapeIds: [],
},
{ history: 'ephemeral' }
)
})
})
if (initialState && this.root.children[initialState] === undefined) {
throw Error(`No state found for initialState "${initialState}".`)
@ -1240,7 +1241,7 @@ export class Editor extends EventEmitter<TLEventMap> {
partial: Partial<Omit<TLInstance, 'currentPageId'>>,
historyOptions?: TLHistoryBatchOptions
): this {
this._updateInstanceState(partial, historyOptions)
this._updateInstanceState(partial, { history: 'ephemeral', ...historyOptions })
if (partial.isChangingStyle !== undefined) {
clearTimeout(this._isChangingStyleTimeout)
@ -2895,7 +2896,7 @@ export class Editor extends EventEmitter<TLEventMap> {
transact(() => {
this.stopFollowingUser()
this.updateInstanceState({ followingUserId: userId }, { history: 'ephemeral' })
this.updateInstanceState({ followingUserId: userId })
})
const cancel = () => {
@ -3000,7 +3001,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
stopFollowingUser(): this {
this.updateInstanceState({ followingUserId: null }, { history: 'ephemeral' })
this.updateInstanceState({ followingUserId: null })
this.emit('stop-following')
return this
}
@ -6298,7 +6299,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// find last parent id
const currentPageShapesSorted = this.getCurrentPageShapesSorted()
let partials = shapes.map((partial) => {
const partials = shapes.map((partial) => {
if (!partial.id) {
partial = { id: createShapeId(), ...partial }
}
@ -6310,7 +6311,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// children of the creating shape's type, or else the page itself.
if (
!partial.parentId ||
!(this.store.has(partial.parentId) || partials.some((p) => p.id === partial.parentId))
!(this.store.has(partial.parentId) || shapes.some((p) => p.id === partial.parentId))
) {
let parentId: TLParentId = this.getFocusedGroupId()
@ -8058,12 +8059,7 @@ export class Editor extends EventEmitter<TLEventMap> {
if (this.inputs.isPanning) {
this.inputs.isPanning = false
this.updateInstanceState({
cursor: {
type: this._prevCursor,
rotation: 0,
},
})
this.setCursor({ type: this._prevCursor, rotation: 0 })
}
}

Wyświetl plik

@ -152,14 +152,15 @@ export class HistoryManager<
// start by collecting the pending diff (everything since the last mark).
// we'll accumulate the diff to undo in this variable so we can apply it atomically.
const pendingDiff = this.pendingDiff.clear()
const isPendingDiffEmpty = isRecordsDiffEmpty(pendingDiff)
const diffToUndo = reverseRecordsDiff(pendingDiff)
if (pushToRedoStack) {
if (pushToRedoStack && !isPendingDiffEmpty) {
redos = redos.push({ type: 'diff', diff: pendingDiff })
}
let didFindMark = false
if (isRecordsDiffEmpty(diffToUndo)) {
if (isPendingDiffEmpty) {
// if nothing has happened since the last mark, pop any intermediate marks off the stack
while (undos.head?.type === 'stop') {
const mark = undos.head
@ -287,6 +288,17 @@ export class HistoryManager<
this.stacks.set({ undos: stack(), redos: stack() })
this.pendingDiff.clear()
}
/** @internal */
debug() {
const { undos, redos } = this.stacks.get()
return {
undos: undos.toArray(),
redos: redos.toArray(),
pendingDiff: this.pendingDiff.debug(),
state: this.state,
}
}
}
const modeToState = {
@ -314,4 +326,8 @@ class PendingDiff<R extends UnknownRecord> {
squashRecordDiffsMutable(this.diff, [diff])
this.isEmptyAtom.set(isRecordsDiffEmpty(this.diff))
}
debug() {
return { diff: this.diff, isEmpty: this.isEmpty() }
}
}

Wyświetl plik

@ -38,6 +38,9 @@ export type ComputedCache<Data, R extends UnknownRecord> = {
get(id: IdOf<R>): Data | undefined;
};
// @internal (undocumented)
export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R>;
// @public
export function createRecordType<R extends UnknownRecord>(typeName: R['typeName'], config: {
migrations?: Migrations;
@ -229,13 +232,13 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
schema: StoreSchema<R, Props>;
props: Props;
});
// @internal
accumulatingChanges(accumulator: RecordsDiff<R>, fn: () => void): void;
// @internal (undocumented)
addHistoryInterceptor(fn: (entry: HistoryEntry<R>, source: ChangeSource) => void): () => void;
allRecords: () => R[];
// (undocumented)
applyDiff(diff: RecordsDiff<R>, runCallbacks?: boolean): void;
// @internal (undocumented)
atomic<T>(fn: () => T, runCallbacks?: boolean): T;
clear: () => void;
createComputedCache: <T, V extends R = R>(name: string, derive: (record: V) => T | undefined, isEqual?: ((a: V, b: V) => boolean) | undefined) => ComputedCache<T, V>;
createSelectedComputedCache: <T, J, V extends R = R>(name: string, selector: (record: V) => T | undefined, derive: (input: T) => J | undefined) => ComputedCache<J, V>;

Wyświetl plik

@ -2,6 +2,7 @@ export type { BaseRecord, IdOf, RecordId, UnknownRecord } from './lib/BaseRecord
export { IncrementalSetConstructor } from './lib/IncrementalSetConstructor'
export { RecordType, assertIdType, createRecordType } from './lib/RecordType'
export {
createEmptyRecordsDiff,
isRecordsDiffEmpty,
reverseRecordsDiff,
squashRecordDiffs,

Wyświetl plik

@ -12,6 +12,11 @@ export type RecordsDiff<R extends UnknownRecord> = {
removed: Record<IdOf<R>, R>
}
/** @internal */
export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R> {
return { added: {}, updated: {}, removed: {} } as RecordsDiff<R>
}
/** @public */
export function reverseRecordsDiff(diff: RecordsDiff<any>) {
const result: RecordsDiff<any> = { added: diff.removed, removed: diff.added, updated: {} }

Wyświetl plik

@ -12,7 +12,7 @@ import { nanoid } from 'nanoid'
import { IdOf, RecordId, UnknownRecord } from './BaseRecord'
import { Cache } from './Cache'
import { RecordScope } from './RecordType'
import { RecordsDiff, squashRecordDiffs, squashRecordDiffsMutable } from './RecordsDiff'
import { RecordsDiff, squashRecordDiffs } from './RecordsDiff'
import { StoreQueries } from './StoreQueries'
import { SerializedSchema, StoreSchema } from './StoreSchema'
import { devFreeze } from './devFreeze'
@ -350,7 +350,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* @public
*/
put = (records: R[], phaseOverride?: 'initialize'): void => {
this.transactWithAfterEvents(() => {
this.atomic(() => {
const updates: Record<IdOf<UnknownRecord>, [from: R, to: R]> = {}
const additions: Record<IdOf<UnknownRecord>, R> = {}
@ -447,7 +447,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* @public
*/
remove = (ids: IdOf<R>[]): void => {
this.transactWithAfterEvents(() => {
this.atomic(() => {
const cancelled = [] as IdOf<R>[]
const source = this.isMergingRemoteChanges ? 'remote' : 'user'
@ -586,7 +586,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
throw new Error(`Failed to migrate snapshot: ${migrationResult.reason}`)
}
transact(() => {
this.atomic(() => {
this.clear()
this.put(Object.values(migrationResult.value))
this.ensureStoreIsUsable()
@ -708,24 +708,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
}
}
/**
* Like {@link extractingChanges}, but accumulates the changes into the provided accumulator
* diff instead of returning a fresh diff.
* @internal
*/
accumulatingChanges(accumulator: RecordsDiff<R>, fn: () => void) {
const changes: Array<RecordsDiff<R>> = []
const dispose = this.historyAccumulator.addInterceptor((entry) => changes.push(entry.changes))
try {
transact(fn)
squashRecordDiffsMutable(accumulator, changes)
} finally {
dispose()
}
}
applyDiff(diff: RecordsDiff<R>, runCallbacks = true) {
this.transactWithAfterEvents(() => {
this.atomic(() => {
const toPut = objectMapValues(diff.added).concat(
objectMapValues(diff.updated).map(([_from, to]) => to)
)
@ -847,7 +831,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
this.pendingAfterEvents.set(id, { before, after, source })
}
}
private transactWithAfterEvents<T>(fn: () => T, runCallbacks = true): T {
/** @internal */
atomic<T>(fn: () => T, runCallbacks = true): T {
return transact(() => {
if (this.pendingAfterEvents) return fn()

Wyświetl plik

@ -224,9 +224,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
isPrecise: boolean;
}>;
point: ObjectValidator< {
type: "point";
x: number;
y: number;
type: "point";
}>;
}, never>;
end: UnionValidator<"type", {
@ -238,9 +238,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
isPrecise: boolean;
}>;
point: ObjectValidator< {
type: "point";
x: number;
y: number;
type: "point";
}>;
}, never>;
bend: Validator<number>;
@ -1061,9 +1061,9 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
spline: EnumStyleProp<"cubic" | "line">;
points: DictValidator<string, {
id: string;
x: number;
y: number;
id: string;
index: IndexKey;
}>;
};

Wyświetl plik

@ -1645,7 +1645,7 @@
},
{
"kind": "Content",
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
@ -1690,7 +1690,7 @@
},
{
"kind": "Content",
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",
@ -12394,7 +12394,7 @@
},
{
"kind": "Content",
"text": "<string, {\n x: number;\n y: number;\n id: string;\n index: import(\"@tldraw/editor\")."
"text": "<string, {\n id: string;\n x: number;\n y: number;\n index: import(\"@tldraw/editor\")."
},
{
"kind": "Reference",

Wyświetl plik

@ -11,7 +11,7 @@ afterEach(() => {
})
describe(NoteShapeTool, () => {
it('Creates note shapes on click-and-drag, supports undo and redo', () => {
it.only('Creates note shapes on click-and-drag, supports undo and redo', () => {
expect(editor.getCurrentPageShapes().length).toBe(0)
editor.setCurrentTool('note')
@ -25,7 +25,6 @@ describe(NoteShapeTool, () => {
editor.cancel() // leave edit mode
editor.undo() // undoes the selection change
editor.undo()
expect(editor.getCurrentPageShapes().length).toBe(0)

Wyświetl plik

@ -64,12 +64,7 @@ export class Cropping extends StateNode {
if (!selectedShape) return
const cursorType = CursorTypeMap[this.info.handle!]
this.editor.updateInstanceState({
cursor: {
type: cursorType,
rotation: this.editor.getSelectionRotation(),
},
})
this.editor.setCursor({ type: cursorType, rotation: this.editor.getSelectionRotation() })
}
private getDefaultCrop = (): TLImageShapeCrop => ({

Wyświetl plik

@ -19,12 +19,7 @@ export class PointingCropHandle extends StateNode {
if (!selectedShape) return
const cursorType = CursorTypeMap[this.info.handle!]
this.editor.updateInstanceState({
cursor: {
type: cursorType,
rotation: this.editor.getSelectionRotation(),
},
})
this.editor.setCursor({ type: cursorType, rotation: this.editor.getSelectionRotation() })
this.editor.setCroppingShape(selectedShape.id)
}

Wyświetl plik

@ -34,11 +34,9 @@ export class PointingResizeHandle extends StateNode {
private updateCursor() {
const selected = this.editor.getSelectedShapes()
const cursorType = CursorTypeMap[this.info.handle!]
this.editor.updateInstanceState({
cursor: {
type: cursorType,
rotation: selected.length === 1 ? this.editor.getSelectionRotation() : 0,
},
this.editor.setCursor({
type: cursorType,
rotation: selected.length === 1 ? this.editor.getSelectionRotation() : 0,
})
}

Wyświetl plik

@ -11,11 +11,9 @@ export class PointingRotateHandle extends StateNode {
private info = {} as PointingRotateHandleInfo
private updateCursor() {
this.editor.updateInstanceState({
cursor: {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: this.editor.getSelectionRotation(),
},
this.editor.setCursor({
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: this.editor.getSelectionRotation(),
})
}

Wyświetl plik

@ -50,11 +50,9 @@ export class Rotating extends StateNode {
})
// Update cursor
this.editor.updateInstanceState({
cursor: {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
},
this.editor.setCursor({
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
})
}
@ -104,11 +102,9 @@ export class Rotating extends StateNode {
})
// Update cursor
this.editor.updateInstanceState({
cursor: {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
},
this.editor.setCursor({
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
})
}

Wyświetl plik

@ -19,10 +19,7 @@ export class ZoomTool extends StateNode {
override onExit = () => {
this.parent.setCurrentToolIdMask(undefined)
this.editor.updateInstanceState(
{ zoomBrush: null, cursor: { type: 'default', rotation: 0 } },
{ history: 'ephemeral' }
)
this.editor.updateInstanceState({ zoomBrush: null, cursor: { type: 'default', rotation: 0 } })
this.parent.setCurrentToolIdMask(undefined)
}

Wyświetl plik

@ -37,7 +37,7 @@ export function MobileStylePanel() {
const handleStylesOpenChange = useCallback(
(isOpen: boolean) => {
if (!isOpen) {
editor.updateInstanceState({ isChangingStyle: false }, { history: 'ephemeral' })
editor.updateInstanceState({ isChangingStyle: false })
}
},
[editor]

Wyświetl plik

@ -21,7 +21,7 @@ export const DefaultStylePanel = memo(function DefaultStylePanel({
const handlePointerOut = useCallback(() => {
if (!isMobile) {
editor.updateInstanceState({ isChangingStyle: false }, { history: 'ephemeral' })
editor.updateInstanceState({ isChangingStyle: false })
}
}, [editor, isMobile])

Wyświetl plik

@ -83,7 +83,7 @@ function useStyleChangeCallback() {
editor.setStyleForSelectedShapes(style, value)
}
editor.setStyleForNextShapes(style, value)
editor.updateInstanceState({ isChangingStyle: true }, { history: 'ephemeral' })
editor.updateInstanceState({ isChangingStyle: true })
})
trackEvent('set-style', { source: 'style-panel', id: style.id, value: value as string })
@ -332,7 +332,7 @@ export function OpacitySlider() {
editor.setOpacityForSelectedShapes(item)
}
editor.setOpacityForNextShapes(item)
editor.updateInstanceState({ isChangingStyle: true }, { history: 'ephemeral' })
editor.updateInstanceState({ isChangingStyle: true })
})
trackEvent('set-style', { source: 'style-panel', id: 'opacity', value })

Wyświetl plik

@ -1129,10 +1129,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
readonlyOk: true,
onSelect(source) {
trackEvent('toggle-transparent', { source })
editor.updateInstanceState(
{ exportBackground: !editor.getInstanceState().exportBackground },
{ history: 'ephemeral' }
)
editor.updateInstanceState({
exportBackground: !editor.getInstanceState().exportBackground,
})
},
checkbox: true,
},
@ -1292,7 +1291,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
editor.setStyleForSelectedShapes(style, 'white')
}
editor.setStyleForNextShapes(style, 'white')
editor.updateInstanceState({ isChangingStyle: true }, { history: 'ephemeral' })
editor.updateInstanceState({ isChangingStyle: true })
})
trackEvent('set-style', { source, id: style.id, value: 'white' })
},

Wyświetl plik

@ -24,9 +24,9 @@ export function pasteTldrawContent(editor: Editor, clipboard: TLContent, point?:
seletionBoundsBefore?.collides(selectedBoundsAfter)
) {
// Creates a 'puff' to show a paste has happened.
editor.updateInstanceState({ isChangingStyle: true }, { history: 'ephemeral' })
editor.updateInstanceState({ isChangingStyle: true })
setTimeout(() => {
editor.updateInstanceState({ isChangingStyle: false }, { history: 'ephemeral' })
editor.updateInstanceState({ isChangingStyle: false })
}, 150)
}
}

Wyświetl plik

@ -183,10 +183,7 @@ describe('<TldrawEditor />', () => {
expect(editor).toBeTruthy()
await act(async () => {
editor.updateInstanceState(
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
{ history: 'ephemeral' }
)
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } })
})
const id = createShapeId()
@ -304,10 +301,7 @@ describe('Custom shapes', () => {
expect(editor).toBeTruthy()
await act(async () => {
editor.updateInstanceState(
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
{ history: 'ephemeral' }
)
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } })
})
expect(editor.shapeUtils.card).toBeTruthy()

Wyświetl plik

@ -23,7 +23,7 @@ describe('when less than two shapes are selected', () => {
editor.setSelectedShapes([ids.boxB])
const fn = jest.fn()
editor.on('update', fn)
editor.store.listen(fn)
editor.alignShapes(editor.getSelectedShapeIds(), 'top')
jest.advanceTimersByTime(1000)
expect(fn).not.toHaveBeenCalled()

Wyświetl plik

@ -46,7 +46,7 @@ describe('distributeShapes command', () => {
it('does nothing', () => {
editor.setSelectedShapes([ids.boxA, ids.boxB])
const fn = jest.fn()
editor.on('change-history', fn)
editor.store.listen(fn)
editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
jest.advanceTimersByTime(1000)
expect(fn).not.toHaveBeenCalled()

Wyświetl plik

@ -60,8 +60,9 @@ describe('Editor.moveShapesToPage', () => {
it('Adds undo items', () => {
editor.history.clear()
expect(editor.history.getNumUndos()).toBe(0)
editor.moveShapesToPage([ids.box1], ids.page2)
expect(editor.history.getNumUndos()).toBeGreaterThan(1)
expect(editor.history.getNumUndos()).toBe(1)
})
it('Does nothing on an empty ids array', () => {

Wyświetl plik

@ -52,7 +52,7 @@ describe('distributeShapes command', () => {
it('does nothing', () => {
editor.setSelectedShapes([ids.boxA, ids.boxB])
const fn = jest.fn()
editor.on('change-history', fn)
editor.store.listen(fn)
editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal', 0)
jest.advanceTimersByTime(1000)
expect(fn).not.toHaveBeenCalled()

Wyświetl plik

@ -27,7 +27,7 @@ describe('when less than two shapes are selected', () => {
it('does nothing', () => {
editor.setSelectedShapes([ids.boxB])
const fn = jest.fn()
editor.on('change-history', fn)
editor.store.listen(fn)
editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal')
jest.advanceTimersByTime(1000)

Wyświetl plik

@ -0,0 +1,129 @@
/* eslint-disable no-console */
import { Editor, RecordsDiff } from '@tldraw/editor'
// eslint-disable-next-line import/no-extraneous-dependencies
import { DiffOptions, diff as jestDiff } from 'jest-diff'
import { inspect } from 'util'
class Printer {
private _output = ''
private _indent = 0
appendLines(str: string) {
const indent = ' '.repeat(this._indent)
this._output +=
str
.split('\n')
.map((line) => indent + line)
.join('\n') + '\n'
}
indent() {
this._indent++
}
dedent() {
this._indent--
}
log(...args: any[]) {
this.appendLines(args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg))).join(' '))
}
print() {
console.log(this._output)
}
get() {
return this._output
}
}
export function prettyPrintDiff(diff: RecordsDiff<any>, opts?: DiffOptions) {
const before = {} as Record<string, any>
const after = {} as Record<string, any>
for (const added of Object.values(diff.added)) {
after[added.id] = added
}
for (const [from, to] of Object.values(diff.updated)) {
before[from.id] = from
after[to.id] = to
}
for (const removed of Object.values(diff.removed)) {
before[removed.id] = removed
}
const prettyDiff = jestDiff(after, before, {
aAnnotation: 'After',
bAnnotation: 'Before',
aIndicator: '+',
bIndicator: '-',
...opts,
})
if (prettyDiff?.includes('Compared values have no visual difference.')) {
const p = new Printer()
p.log('Before & after have no visual difference.')
p.log('Diff:')
p.indent()
p.log(diff)
return p.get()
}
return prettyDiff
}
export function logHistory(editor: Editor) {
const { undos, redos, pendingDiff } = editor.history.debug()
const p = new Printer()
p.log('=== History ===')
p.indent()
p.log('Pending diff:')
p.indent()
if (pendingDiff.isEmpty) {
p.log('(empty)')
} else {
p.log(prettyPrintDiff(pendingDiff.diff))
}
p.log('')
p.dedent()
p.log('Undos:')
p.indent()
if (undos.length === 0) {
p.log('(empty)\n')
}
for (const undo of undos) {
if (!undo) continue
if (undo.type === 'stop') {
p.log('- Stop', undo.id)
} else {
p.log('- Diff')
p.indent()
p.log(prettyPrintDiff(undo.diff))
p.dedent()
}
p.log('')
}
p.dedent()
p.log('Redos:')
p.indent()
if (redos.length === 0) {
p.log('(empty)\n')
}
for (const redo of redos) {
if (!redo) continue
if (redo.type === 'stop') {
p.log('> Stop', redo.id)
} else {
p.log('- Diff')
p.indent()
p.log(prettyPrintDiff(redo.diff))
p.dedent()
}
p.log('')
}
p.print()
}

Wyświetl plik

@ -47,9 +47,9 @@ export const arrowShapeProps: {
isPrecise: boolean;
} & {}>;
point: T.ObjectValidator<{
type: "point";
x: number;
y: number;
type: "point";
} & {}>;
}, never>;
end: T.UnionValidator<"type", {
@ -61,9 +61,9 @@ export const arrowShapeProps: {
isPrecise: boolean;
} & {}>;
point: T.ObjectValidator<{
type: "point";
x: number;
y: number;
type: "point";
} & {}>;
}, never>;
bend: T.Validator<number>;
@ -684,9 +684,9 @@ export const lineShapeProps: {
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
spline: EnumStyleProp<"cubic" | "line">;
points: T.DictValidator<string, {
id: string;
x: number;
y: number;
id: string;
index: IndexKey;
} & {}>;
};

Wyświetl plik

@ -364,7 +364,7 @@
},
{
"kind": "Content",
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n } & {}>;\n }, never>;\n end: "
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n end: "
},
{
"kind": "Reference",
@ -409,7 +409,7 @@
},
{
"kind": "Content",
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n } & {}>;\n }, never>;\n bend: "
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n bend: "
},
{
"kind": "Reference",
@ -2818,7 +2818,7 @@
},
{
"kind": "Content",
"text": "<string, {\n x: number;\n y: number;\n id: string;\n index: "
"text": "<string, {\n id: string;\n x: number;\n y: number;\n index: "
},
{
"kind": "Reference",