-
Notifications
You must be signed in to change notification settings - Fork 334
Delete-and-connect-around operation #14403
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
base: develop
Are you sure you want to change the base?
Changes from 3 commits
73e6bbf
47f23da
59e047c
b81682d
57a84bb
ca643b6
82d38eb
4a96194
bad0490
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import { GraphDb } from '$/providers/openedProjects/graph/graphDatabase' | ||
| import { assert } from '@/util/assert' | ||
| import { Ast } from '@/util/ast' | ||
| import * as iter from 'enso-common/src/utilities/data/iter' | ||
| import { expect, test } from 'vitest' | ||
| import { watchEffect } from 'vue' | ||
| import { analyzeConnectAround } from '../detaching' | ||
|
|
||
| function fixture(code: string) { | ||
| const graphDb = GraphDb.Mock() | ||
| const { root, getSpan } = Ast.parseUpdatingIdMap(code) | ||
| const func = iter.first(root.statements()) | ||
| assert(func instanceof Ast.MutableFunctionDef) | ||
| graphDb.updateExternalIds(root) | ||
| graphDb.updateNodes(func, { watchEffect }) | ||
| graphDb.updateBindings(func, { text: code, getSpan }) | ||
| return { graphDb, func } | ||
| } | ||
|
|
||
| interface TestCase { | ||
| description: string | ||
| funcParameters?: string[] | ||
| initialNodes: string[] | ||
| selectedNodesRange: { start: number; end: number } | ||
| changedNodes: [number, string][] | ||
| } | ||
|
|
||
| const cases: TestCase[] = [ | ||
| { | ||
| description: 'Single node', | ||
| initialNodes: ['a = data', 'b = a.operation', 'c = 3 + b'], | ||
| selectedNodesRange: { start: 1, end: 2 }, | ||
| changedNodes: [[2, 'c = 3 + a']], | ||
| }, | ||
| { | ||
| description: 'Several input connections node', | ||
| initialNodes: ['a = data', 'b = data2', 'c = a.operation b', 'd = 3 + c'], | ||
| selectedNodesRange: { start: 2, end: 3 }, | ||
| changedNodes: [[3, 'd = 3 + a']], | ||
| }, | ||
| { | ||
| description: 'Multiple nodes', | ||
| initialNodes: [ | ||
| 'a = data', | ||
| 'b = data2', | ||
| 'c = data3', | ||
| 'd = b.operation a', | ||
| 'e = d.operation c', | ||
| 'f = 2 + e', | ||
| ], | ||
| selectedNodesRange: { start: 3, end: 5 }, | ||
| changedNodes: [[5, 'f = 2 + b']], | ||
| }, | ||
| { | ||
| description: 'Multiple flows', | ||
| initialNodes: [ | ||
| 'a = data', | ||
| 'b = data2', | ||
| 'c = data3', | ||
| 'd = b.operation a', | ||
| 'e = d.operation', | ||
| 'f = c.operation b', | ||
| 'g = 2 + e + f', | ||
| 'h = f.write', | ||
| ], | ||
| selectedNodesRange: { start: 3, end: 6 }, | ||
| changedNodes: [ | ||
| [6, 'g = 2 + b + c'], | ||
| [7, 'h = c.write'], | ||
| ], | ||
| }, | ||
| { | ||
| description: 'Detaching unavailable - no input', | ||
| initialNodes: [ | ||
| 'a = data', | ||
| 'b = data2', | ||
| 'c = data3', | ||
| 'd = b.operation a', | ||
| 'e = Main.collapsed', | ||
| 'f = 2 + d + e', | ||
| 'g = 3 + e', | ||
| ], | ||
| selectedNodesRange: { start: 3, end: 5 }, | ||
| changedNodes: [], | ||
| }, | ||
| { | ||
| description: 'Detaching unavailable - no output', | ||
| initialNodes: [ | ||
| 'a = data', | ||
| 'b = data2', | ||
| 'c = data3', | ||
| 'd = b.operation a', | ||
| 'e = d.operation c', | ||
| 'f = 2 + c', | ||
| ], | ||
| selectedNodesRange: { start: 3, end: 5 }, | ||
| changedNodes: [], | ||
| }, | ||
| { | ||
| description: 'Reconnecting Input Node', | ||
| funcParameters: ['x'], | ||
| initialNodes: ['a = x.operation', 'b = a.write'], | ||
| selectedNodesRange: { start: 0, end: 1 }, | ||
| changedNodes: [[1, 'b = x.write']], | ||
| }, | ||
| ] | ||
|
|
||
| test.each(cases)( | ||
| 'Connecting around nodes: $description', | ||
| ({ initialNodes, funcParameters, selectedNodesRange, changedNodes }) => { | ||
| const code = `main ${funcParameters?.join(' ') ?? ''} =\n ${initialNodes.join('\n ')}` | ||
| const { graphDb, func } = fixture(code) | ||
| const nodeIds = [...graphDb.nodeIdToNode.entries()] | ||
| .filter(([, node]) => node.type === 'component') | ||
| .map(([id]) => id) | ||
| const selected = new Set(nodeIds.slice(selectedNodesRange.start, selectedNodesRange.end)) | ||
| for (const { port, ident } of analyzeConnectAround(selected, graphDb)) { | ||
| func.module.replace(port, Ast.Ident.new(func.module, ident)) | ||
| } | ||
| const changedNodesMap = new Map(changedNodes) | ||
| nodeIds.forEach((id, index) => { | ||
| const node = graphDb.nodeIdToNode.get(id) | ||
| if (changedNodesMap.has(index)) { | ||
| expect(node?.outerAst.code()).toBe(changedNodesMap.get(index)) | ||
| } else if (!selected.has(id)) { | ||
| expect(node?.outerAst.code()).toBe(initialNodes[index]) | ||
| } | ||
| }) | ||
| }, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import type { GraphDb, NodeId } from '$/providers/openedProjects/graph/graphDatabase' | ||
| import { set } from 'lib0' | ||
| import { isIdentifier, type AstId, type Identifier } from 'ydoc-shared/ast' | ||
|
|
||
| const none: unique symbol = Symbol() | ||
| type None = typeof none | ||
|
|
||
| /** | ||
| * Return all changes to port values for "connecting around" operation of given selected nodes. | ||
| * | ||
| * The connections going out of the `selected` group will be reconnected to their self-port | ||
| * input connection. If any connection could not be subsituted this way (e.g. no connection to self-port), | ||
| * an empty array will be returned. | ||
|
||
| */ | ||
| export function analyzeConnectAround(selected: Set<NodeId>, graphDb: GraphDb) { | ||
| const knownMainSourceIdentifier = new Map<NodeId, Identifier | None>() | ||
| const result: { port: AstId; ident: Identifier }[] = [] | ||
|
|
||
| const findMainSourceOf = (selectedNode: NodeId) => { | ||
| const findResult = (): Identifier | None => { | ||
| const alreadyKnown = knownMainSourceIdentifier.get(selectedNode) | ||
| if (alreadyKnown) return alreadyKnown | ||
| const selfPort = graphDb.nodeIdToNode.get(selectedNode)?.primaryApplication.selfArgument | ||
| if (!selfPort) return none | ||
| const mainSourceId = set.first(graphDb.connections.reverseLookup(selfPort)) | ||
| if (!mainSourceId) return none | ||
| const mainSourceNode = graphDb.getPatternExpressionNodeId(mainSourceId) | ||
| if (mainSourceNode && selected.has(mainSourceNode)) { | ||
| return findMainSourceOf(mainSourceNode) | ||
| } else { | ||
| const ident = graphDb.getOutputPortIdentifier(mainSourceId) | ||
| return ident != null && isIdentifier(ident) ? ident : none | ||
| } | ||
| } | ||
| const result = findResult() | ||
| knownMainSourceIdentifier.set(selectedNode, result) | ||
| return result | ||
| } | ||
|
||
|
|
||
| for (const [source, targets] of graphDb.connections.allForward()) { | ||
| for (const target of targets) { | ||
| // TODO[ao]: copied from collapsing.ts. Worth merging? | ||
|
||
| const targetNode = graphDb.getExpressionNodeId(target) | ||
| if (targetNode == null) continue | ||
| const sourceNode = graphDb.getPatternExpressionNodeId(source) | ||
| if (sourceNode == null) continue | ||
| // Sometimes the connection source is in expression, not pattern; for example, when its | ||
| // lambda. | ||
| const nodeWithSource = sourceNode ?? graphDb.getExpressionNodeId(source) | ||
| // If source is not in pattern nor expression of any node, it's a function argument. | ||
| const startsInside = nodeWithSource != null && selected.has(nodeWithSource) | ||
| const endsInside = selected.has(targetNode) | ||
| if (startsInside && !endsInside) { | ||
| const mainSource = findMainSourceOf(sourceNode) | ||
| if (mainSource == none) { | ||
| // Do not allow the action if any port would miss its source. | ||
| return [] | ||
| } | ||
| result.push({ port: target, ident: mainSource }) | ||
| } | ||
| } | ||
| } | ||
| return result | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should expect this case to be reachable, as we have
awaited during this edit and arbitrary graph edits may have taken place since thedetachInfoanalysis was performed. As such, we should carefully consider error recovery. I think maybe the best we could do is: Notify the user (e.g. with a toast) that an error occurred during the operation, and they may Undo if the result is wrong.I think I've said this before, but I really don't like the complexity that results from allowing the
editoperations to beasync. It makes it so much harder to avoid bugs and crashes in hard-to-predict-or-test cases where multiple operations interact.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a toast. AFAIK this async edit was needed in signature editor. I will make a task for sorting this out, as I agree async edits are very troublesome.