Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix undoing node removal #9561

Merged
merged 6 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions app/gui2/e2e/undoRedo.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import test, { type Locator, type Page } from 'playwright/test'
import * as actions from './actions'
import { expect } from './customExpect'
import { mockMethodCallInfo } from './expressionUpdates'
import * as locate from './locate'

test('Adding new node', async ({ page }) => {
await actions.goToGraph(page)

const nodesCount = await locate.graphNode(page).count()
await locate.addNewNodeButton(page).click()
await expect(locate.componentBrowserInput(page)).toBeVisible()
await page.keyboard.insertText('foo')
await page.keyboard.press('Control+Enter')
await expect(locate.graphNode(page)).toHaveCount(nodesCount + 1)
await expect(locate.graphNode(page).last().locator('.WidgetToken')).toHaveText(['foo'])
const newNodeBBox = await locate.graphNode(page).last().boundingBox()

await page.keyboard.press('Control+Z')
await expect(locate.graphNode(page)).toHaveCount(nodesCount)
await expect(
locate.graphNode(page).locator('.WidgetToken').filter({ hasText: 'foo' }),
).toHaveCount(0)

await page.keyboard.press('Control+Shift+Z')
await expect(locate.graphNode(page)).toHaveCount(nodesCount + 1)
await expect(locate.graphNode(page).last().locator('.WidgetToken')).toHaveText(['foo'])
const restoredBox = await locate.graphNode(page).last().boundingBox()
await expect(restoredBox).toEqual(newNodeBBox)
})

test('Removing node', async ({ page }) => {
await actions.goToGraph(page)

const nodesCount = await locate.graphNode(page).count()
const deletedNode = locate.graphNodeByBinding(page, 'final')
const deletedNodeBBox = await deletedNode.boundingBox()
await deletedNode.click()
await page.keyboard.press('Delete')
await expect(locate.graphNode(page)).toHaveCount(nodesCount - 1)

await page.keyboard.press('Control+Z')
await expect(locate.graphNode(page)).toHaveCount(nodesCount)
await expect(deletedNode.locator('.WidgetToken')).toHaveText(['Main', '.', 'func1', 'prod'])
await expect(deletedNode.locator('.GraphNodeComment')).toHaveText('This node can be entered')
const restoredBBox = await deletedNode.boundingBox()
await expect(restoredBBox).toEqual(deletedNodeBBox)

await page.keyboard.press('Control+Shift+Z')
await expect(locate.graphNode(page)).toHaveCount(nodesCount - 1)
await expect(deletedNode).not.toBeVisible()
})
5 changes: 3 additions & 2 deletions app/gui2/shared/ast/mutableModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,6 @@ class UpdateBuilder {

addNode(id: AstId) {
this.nodesAdded.add(id)
this.updateAllFields(id)
}

updateAllFields(id: AstId) {
Expand Down Expand Up @@ -404,7 +403,9 @@ class UpdateBuilder {
}

finish(): ModuleUpdate {
const updateRoots = subtreeRoots(this.module, new Set(this.nodesUpdated.keys()))
const dirtyNodes = new Set(this.nodesUpdated)
this.nodesAdded.forEach((node) => dirtyNodes.add(node))
const updateRoots = subtreeRoots(this.module, dirtyNodes)
return { ...this, updateRoots }
}
}
15 changes: 8 additions & 7 deletions app/gui2/shared/yjsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,18 @@ export class DistributedModule {
}
}

export const localOrigins = ['local', 'local:CodeEditor'] as const
export type LocalOrigin = (typeof localOrigins)[number]
export type Origin = LocalOrigin | 'remote'
export const localUserActionOrigins = ['local:userAction', 'local:userAction:CodeEditor'] as const
export type LocalUserActionOrigin = (typeof localUserActionOrigins)[number]
export type Origin = LocalUserActionOrigin | 'remote' | 'local:autoLayout'
/** Locally-originated changes not otherwise specified. */
export const defaultLocalOrigin: LocalOrigin = 'local'
export function isLocalOrigin(origin: string): origin is LocalOrigin {
const localOriginNames: readonly string[] = localOrigins
export const defaultLocalOrigin: LocalUserActionOrigin = 'local:userAction'
export function isLocalUserActionOrigin(origin: string): origin is LocalUserActionOrigin {
const localOriginNames: readonly string[] = localUserActionOrigins
return localOriginNames.includes(origin)
}
export function tryAsOrigin(origin: string): Origin | undefined {
if (isLocalOrigin(origin)) return origin
if (isLocalUserActionOrigin(origin)) return origin
if (origin === 'local:autoLayout') return origin
if (origin === 'remote') return origin
}

Expand Down
4 changes: 2 additions & 2 deletions app/gui2/src/components/CodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ function commitPendingChanges() {
if (!pendingChanges || !currentModule) return
try {
currentModule.applyTextEdits(changeSetToTextEdits(pendingChanges), graphStore.viewModule)
graphStore.commitEdit(currentModule, undefined, 'local:CodeEditor')
graphStore.commitEdit(currentModule, undefined, 'local:userAction:CodeEditor')
} catch (error) {
console.error(`Code Editor failed to modify module`, error)
resetView()
Expand Down Expand Up @@ -264,7 +264,7 @@ function observeSourceChange(textEdits: SourceRangeEdit[], origin: Origin | unde
return
}
// When we aren't in the `needResync` state, we can ignore updates that originated in the Code Editor.
if (origin === 'local:CodeEditor') return
if (origin === 'local:userAction:CodeEditor') return
if (pendingChanges) {
console.info(`Deferring update (editor dirty).`)
needResync = true
Expand Down
18 changes: 5 additions & 13 deletions app/gui2/src/stores/graph/graphDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,19 +348,11 @@ export class GraphDb {
const node = this.nodeIdToNode.get(nodeId)
currentNodeIds.add(nodeId)
if (node == null) {
let metadataFields: NodeDataFromMetadata = {
position: new Vec2(0, 0),
vis: undefined,
}
// We are notified of new or changed metadata by `updateMetadata`, so we only need to read existing metadata
// when we switch to a different function.
if (functionChanged) {
const nodeMeta = newNode.rootExpr.nodeMetadata
const pos = nodeMeta.get('position') ?? { x: 0, y: 0 }
metadataFields = {
position: new Vec2(pos.x, pos.y),
vis: nodeMeta.get('visualization'),
}
const nodeMeta = newNode.rootExpr.nodeMetadata
const pos = nodeMeta.get('position') ?? { x: 0, y: 0 }
const metadataFields = {
position: new Vec2(pos.x, pos.y),
vis: nodeMeta.get('visualization'),
}
this.nodeIdToNode.set(nodeId, { ...newNode, ...metadataFields, zIndex: this.highestZIndex })
} else {
Expand Down
24 changes: 17 additions & 7 deletions app/gui2/src/stores/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import { iteratorFilter } from 'lib0/iterator'
import { defineStore } from 'pinia'
import { SourceDocument } from 'shared/ast/sourceDocument'
import type { ExpressionUpdate, StackItem } from 'shared/languageServerTypes'
import type { LocalOrigin, SourceRangeKey, VisualizationMetadata } from 'shared/yjsModel'
import type {
LocalUserActionOrigin,
Origin,
SourceRangeKey,
VisualizationMetadata,
} from 'shared/yjsModel'
import { defaultLocalOrigin, sourceRangeKey, visMetadataEquals } from 'shared/yjsModel'
import {
computed,
Expand Down Expand Up @@ -134,8 +139,13 @@ export const useGraphStore = defineStore('graph', () => {
id: AstId
changes: NodeMetadata
}[]
const dirtyNodeSet = new Set(update.nodesUpdated)
if (moduleChanged || dirtyNodeSet.size !== 0) {
const dirtyNodeSet = new Set(
(function* () {
yield* update.nodesUpdated
yield* update.nodesAdded
})(),
)
if (moduleChanged || dirtyNodeSet.size !== 0 || update.nodesDeleted.size !== 0) {
db.updateExternalIds(root)
toRaw = new Map()
visitRecursive(Ast.parseEnso(moduleSource.text), (node) => {
Expand Down Expand Up @@ -457,7 +467,7 @@ export const useGraphStore = defineStore('graph', () => {
)
nodeRects.set(nodeId, new Rect(position, rect.size))
}
})
}, 'local:autoLayout')
})

function updateVizRect(id: NodeId, rect: Rect | undefined) {
Expand Down Expand Up @@ -540,7 +550,7 @@ export const useGraphStore = defineStore('graph', () => {
function commitEdit(
edit: MutableModule,
skipTreeRepair?: boolean,
origin: LocalOrigin = defaultLocalOrigin,
origin: LocalUserActionOrigin = defaultLocalOrigin,
) {
const root = edit.root()
if (!(root instanceof Ast.BodyBlock)) {
Expand Down Expand Up @@ -576,9 +586,9 @@ export const useGraphStore = defineStore('graph', () => {
return result!
}

function batchEdits(f: () => void) {
function batchEdits(f: () => void, origin: Origin = defaultLocalOrigin) {
assert(syncModule.value != null)
syncModule.value.transact(f, 'local')
syncModule.value.transact(f, origin)
}

function editNodeMetadata(ast: Ast.Ast, f: (metadata: Ast.MutableNodeMetadata) => void) {
Expand Down
9 changes: 7 additions & 2 deletions app/gui2/src/stores/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ import type {
StackItem,
VisualizationConfiguration,
} from 'shared/languageServerTypes'
import { DistributedProject, localOrigins, type ExternalId, type Uuid } from 'shared/yjsModel'
import {
DistributedProject,
localUserActionOrigins,
type ExternalId,
type Uuid,
} from 'shared/yjsModel'
import {
computed,
markRaw,
Expand Down Expand Up @@ -539,7 +544,7 @@ export const useProjectStore = defineStore('project', () => {
const moduleName = projectModel.findModuleByDocId(guid)
if (moduleName == null) return null
const mod = await projectModel.openModule(moduleName)
for (const origin of localOrigins) mod?.undoManager.addTrackedOrigin(origin)
for (const origin of localUserActionOrigins) mod?.undoManager.addTrackedOrigin(origin)
return mod
})

Expand Down
2 changes: 1 addition & 1 deletion app/gui2/ydoc-server/edits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function applyDocumentUpdates(
synced: EnsoFileParts,
update: ModuleUpdate,
): AppliedUpdates {
const codeChanged = update.nodesUpdated.size !== 0
const codeChanged = update.nodesUpdated.size && update.nodesAdded.size && update.nodesDeleted.size
let idsChanged = false
let metadataChanged = false
for (const { changes } of update.metadataUpdated) {
Expand Down
Loading