Include styleRuns in text edit undo

styleRuns were updated on the graph during editing (via syncText) but
commitTextEdit only captured text changes. Now both text and styleRuns
are snapshotted at startTextEditing and restored on undo.
This commit is contained in:
Danila Poyarkov 2026-04-06 15:59:57 +03:00
parent f77c42d798
commit 3be01dfdeb
2 changed files with 71 additions and 4 deletions

View file

@ -1,7 +1,9 @@
import type { SceneNode } from '../scene-graph'
import type { EditorContext } from './types'
export function createTextActions(ctx: EditorContext) {
let textBeforeEdit: string | null = null
let styleRunsBeforeEdit: SceneNode['styleRuns'] | null = null
function startTextEditing(nodeId: string) {
const te = ctx.getTextEditor()
@ -9,6 +11,7 @@ export function createTextActions(ctx: EditorContext) {
const node = ctx.graph.getNode(nodeId)
if (!node) return
textBeforeEdit = node.text
styleRunsBeforeEdit = structuredClone(node.styleRuns)
ctx.state.editingTextId = nodeId
if (te) {
te.setRenderer(ctx.getRenderer())
@ -22,28 +25,37 @@ export function createTextActions(ctx: EditorContext) {
if (!te?.isActive) {
ctx.state.editingTextId = null
textBeforeEdit = null
styleRunsBeforeEdit = null
return
}
const result = te.stop()
if (!result) {
ctx.state.editingTextId = null
textBeforeEdit = null
styleRunsBeforeEdit = null
ctx.requestRender()
return
}
const prevText = textBeforeEdit ?? ''
const prevRuns = styleRunsBeforeEdit ?? []
const newText = result.text
ctx.graph.updateNode(result.nodeId, { text: newText })
const node = ctx.graph.getNode(result.nodeId)
const newRuns = node ? structuredClone(node.styleRuns) : []
ctx.graph.updateNode(result.nodeId, { text: newText, styleRuns: newRuns })
ctx.state.editingTextId = null
textBeforeEdit = null
if (prevText !== newText) {
styleRunsBeforeEdit = null
const textChanged = prevText !== newText
const runsChanged = JSON.stringify(prevRuns) !== JSON.stringify(newRuns)
if (textChanged || runsChanged) {
ctx.undo.push({
label: 'Edit text',
forward: () => {
ctx.graph.updateNode(result.nodeId, { text: newText })
ctx.graph.updateNode(result.nodeId, { text: newText, styleRuns: newRuns })
},
inverse: () => {
ctx.graph.updateNode(result.nodeId, { text: prevText })
ctx.graph.updateNode(result.nodeId, { text: prevText, styleRuns: prevRuns })
}
})
}

View file

@ -3,6 +3,7 @@ import { describe, test, expect } from 'bun:test'
import { SceneGraph, TextEditor, UndoManager } from '@open-pencil/core'
import { createTextActions } from '@open-pencil/core/editor'
import type { StyleRun } from '@open-pencil/core'
import type { EditorContext, EditorState } from '@open-pencil/core/editor'
function setup() {
@ -117,4 +118,58 @@ describe('text edit undo', () => {
undo.undo()
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
})
test('undo restores styleRuns when they changed during editing', () => {
const { graph, undo, textEditor, textNode, actions } = setup()
const boldRun: StyleRun = { start: 0, length: 5, style: { fontWeight: 700 } }
graph.updateNode(textNode.id, { styleRuns: [boldRun] })
actions.startTextEditing(textNode.id)
textEditor.insert(' World', textNode)
const newRuns: StyleRun[] = [
{ start: 0, length: 5, style: { fontWeight: 700 } },
{ start: 5, length: 6, style: { fontWeight: 400 } },
]
graph.updateNode(textNode.id, { text: textEditor.state!.text, styleRuns: newRuns })
actions.commitTextEdit()
expect(graph.getNode(textNode.id)!.styleRuns).toEqual(newRuns)
expect(undo.canUndo).toBe(true)
undo.undo()
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
expect(graph.getNode(textNode.id)!.styleRuns).toEqual([boldRun])
undo.redo()
expect(graph.getNode(textNode.id)!.text).toBe('Hello World')
expect(graph.getNode(textNode.id)!.styleRuns).toEqual(newRuns)
})
test('undo entry is pushed when only styleRuns changed', () => {
const { graph, undo, textNode, actions } = setup()
actions.startTextEditing(textNode.id)
const newRuns: StyleRun[] = [{ start: 0, length: 5, style: { fontWeight: 700 } }]
graph.updateNode(textNode.id, { styleRuns: newRuns })
actions.commitTextEdit()
expect(undo.canUndo).toBe(true)
undo.undo()
expect(graph.getNode(textNode.id)!.styleRuns).toEqual([])
})
test('no undo entry when neither text nor styleRuns changed', () => {
const { undo, actions, textNode } = setup()
actions.startTextEditing(textNode.id)
actions.commitTextEdit()
expect(undo.canUndo).toBe(false)
})
})