mirror of
https://github.com/enso-org/enso.git
synced 2024-11-24 00:27:16 +03:00
Collapsing nodes (#8686)
Closes #8067 Also fixes `nodeRects` map, as it no longer stores invlisible nodes. https://github.com/enso-org/enso/assets/6566674/ba66c99f-df74-497b-8924-dc779cce8ef5 # Important Notes Positioning of newly created nodes is not handled yet, as it requires fixes in the Ast editing API.
This commit is contained in:
parent
1b3c9638ea
commit
58cf4e5244
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
averagePositionPlacement,
|
||||||
mouseDictatedPlacement,
|
mouseDictatedPlacement,
|
||||||
nonDictatedPlacement,
|
nonDictatedPlacement,
|
||||||
previousNodeDictatedPlacement,
|
previousNodeDictatedPlacement,
|
||||||
@ -447,6 +448,54 @@ describe('Mouse dictated placement', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Average position placement', () => {
|
||||||
|
function environment(selectedNodeRects: Rect[], nonSelectedNodeRects: Rect[]): Environment {
|
||||||
|
return {
|
||||||
|
screenBounds,
|
||||||
|
nodeRects: [...selectedNodeRects, ...nonSelectedNodeRects],
|
||||||
|
selectedNodeRects,
|
||||||
|
get mousePosition() {
|
||||||
|
return getMousePosition()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function options(): { horizontalGap: number; verticalGap: number } {
|
||||||
|
return {
|
||||||
|
get horizontalGap() {
|
||||||
|
return getHorizontalGap()
|
||||||
|
},
|
||||||
|
get verticalGap() {
|
||||||
|
return getVerticalGap()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('One selected, no other nodes', () => {
|
||||||
|
const X = 1100
|
||||||
|
const Y = 700
|
||||||
|
const selectedNodeRects = [rectAt(X, Y)]
|
||||||
|
const result = averagePositionPlacement(nodeSize, environment(selectedNodeRects, []), options())
|
||||||
|
expect(result).toEqual({ position: new Vec2(X, Y), pan: undefined })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Multiple selected, no other nodes', () => {
|
||||||
|
const selectedNodeRects = [rectAt(1000, 600), rectAt(1300, 800)]
|
||||||
|
const result = averagePositionPlacement(nodeSize, environment(selectedNodeRects, []), options())
|
||||||
|
expect(result).toEqual({ position: new Vec2(1150, 700), pan: undefined })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Average position occupied', () => {
|
||||||
|
const selectedNodeRects = [rectAt(1000, 600), rectAt(1300, 800)]
|
||||||
|
const result = averagePositionPlacement(
|
||||||
|
nodeSize,
|
||||||
|
environment(selectedNodeRects, [rectAt(1150, 700)]),
|
||||||
|
options(),
|
||||||
|
)
|
||||||
|
expect(result).toEqual({ position: new Vec2(1150, 744), pan: undefined })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// === Helpers for debugging ===
|
// === Helpers for debugging ===
|
||||||
|
|
||||||
function generateVueCodeForNonDictatedPlacement(newNode: Rect, rects: Rect[]) {
|
function generateVueCodeForNonDictatedPlacement(newNode: Rect, rects: Rect[]) {
|
||||||
|
@ -128,3 +128,54 @@ export function mouseDictatedPlacement(
|
|||||||
const nodeRadius = nodeSize.y / 2
|
const nodeRadius = nodeSize.y / 2
|
||||||
return { position: mousePosition.add(new Vec2(nodeRadius, nodeRadius)) }
|
return { position: mousePosition.add(new Vec2(nodeRadius, nodeRadius)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The new node should appear at the average position of selected nodes.
|
||||||
|
*
|
||||||
|
* If the desired place is already occupied by non-selected node, it should be moved down to the closest free space.
|
||||||
|
*
|
||||||
|
* Specifically, this code, in order:
|
||||||
|
* - calculates the average position of selected nodes
|
||||||
|
* - searches for all vertical spans below the initial position,
|
||||||
|
* that horizontally intersect the initial position (no horizontal gap is required between
|
||||||
|
* the new node and old nodes)
|
||||||
|
* - shifts the node down (if required) until there is sufficient vertical space -
|
||||||
|
* the height of the node, in addition to the specified gap both above and below the node.
|
||||||
|
*/
|
||||||
|
export function averagePositionPlacement(
|
||||||
|
nodeSize: Vec2,
|
||||||
|
{ screenBounds, selectedNodeRects, nodeRects }: Environment,
|
||||||
|
{ verticalGap = theme.node.vertical_gap }: PlacementOptions = {},
|
||||||
|
): Placement {
|
||||||
|
let totalPosition = new Vec2(0, 0)
|
||||||
|
let selectedNodeRectsCount = 0
|
||||||
|
for (const rect of selectedNodeRects) {
|
||||||
|
totalPosition = totalPosition.add(rect.pos)
|
||||||
|
selectedNodeRectsCount++
|
||||||
|
}
|
||||||
|
const initialPosition = totalPosition.scale(1.0 / selectedNodeRectsCount)
|
||||||
|
const nonSelectedNodeRects = []
|
||||||
|
outer: for (const rect of nodeRects) {
|
||||||
|
for (const sel of selectedNodeRects) {
|
||||||
|
if (sel.equals(rect)) {
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nonSelectedNodeRects.push(rect)
|
||||||
|
}
|
||||||
|
let top = initialPosition.y
|
||||||
|
const initialRect = new Rect(initialPosition, nodeSize)
|
||||||
|
const nodeRectsSorted = Array.from(nonSelectedNodeRects).sort((a, b) => a.top - b.top)
|
||||||
|
for (const rect of nodeRectsSorted) {
|
||||||
|
if (initialRect.intersectsX(rect) && rect.bottom + verticalGap > top) {
|
||||||
|
if (rect.top - (top + nodeSize.y) < verticalGap) {
|
||||||
|
top = rect.bottom + verticalGap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const finalPosition = new Vec2(initialPosition.x, top)
|
||||||
|
if (new Rect(finalPosition, nodeSize).within(screenBounds)) {
|
||||||
|
return { position: finalPosition }
|
||||||
|
} else {
|
||||||
|
return { position: finalPosition, pan: finalPosition.sub(initialPosition) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -26,6 +26,8 @@ import { useGraphStore } from '@/stores/graph'
|
|||||||
import type { RequiredImport } from '@/stores/graph/imports'
|
import type { RequiredImport } from '@/stores/graph/imports'
|
||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
import { groupColorVar, useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
import { groupColorVar, useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||||
|
import { assert, bail } from '@/util/assert'
|
||||||
|
import { BodyBlock } from '@/util/ast/abstract'
|
||||||
import { colorFromString } from '@/util/colors'
|
import { colorFromString } from '@/util/colors'
|
||||||
import { Rect } from '@/util/data/rect'
|
import { Rect } from '@/util/data/rect'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
@ -255,13 +257,23 @@ const graphBindingsHandler = graphBindings.handler({
|
|||||||
},
|
},
|
||||||
collapse() {
|
collapse() {
|
||||||
if (keyboardBusy()) return false
|
if (keyboardBusy()) return false
|
||||||
const selected = nodeSelection.selected
|
const selected = new Set(nodeSelection.selected)
|
||||||
if (selected.size == 0) return
|
if (selected.size == 0) return
|
||||||
try {
|
try {
|
||||||
const info = prepareCollapsedInfo(nodeSelection.selected, graphStore.db)
|
const info = prepareCollapsedInfo(selected, graphStore.db)
|
||||||
performCollapse(info)
|
const currentMethod = projectStore.executionContext.getStackTop()
|
||||||
|
const currentMethodName = graphStore.db.stackItemToMethodName(currentMethod)
|
||||||
|
if (currentMethodName == null) {
|
||||||
|
bail(`Cannot get the method name for the current execution stack item. ${currentMethod}`)
|
||||||
|
}
|
||||||
|
graphStore.editAst((module) => {
|
||||||
|
if (graphStore.moduleRoot == null) bail(`Module root is missing.`)
|
||||||
|
const topLevel = module.get(graphStore.moduleRoot)
|
||||||
|
assert(topLevel instanceof BodyBlock)
|
||||||
|
return performCollapse(info, module, topLevel, graphStore.db, currentMethodName)
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`Error while collapsing, this is not normal. ${err}`)
|
console.log('Error while collapsing, this is not normal.', err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enterNode() {
|
enterNode() {
|
||||||
|
@ -20,7 +20,7 @@ import { Vec2 } from '@/util/data/vec2'
|
|||||||
import { displayedIconOf } from '@/util/getIconName'
|
import { displayedIconOf } from '@/util/getIconName'
|
||||||
import { setIfUndefined } from 'lib0/map'
|
import { setIfUndefined } from 'lib0/map'
|
||||||
import type { ExprId, VisualizationIdentifier } from 'shared/yjsModel'
|
import type { ExprId, VisualizationIdentifier } from 'shared/yjsModel'
|
||||||
import { computed, ref, watch, watchEffect } from 'vue'
|
import { computed, onUnmounted, ref, watch, watchEffect } from 'vue'
|
||||||
|
|
||||||
const MAXIMUM_CLICK_LENGTH_MS = 300
|
const MAXIMUM_CLICK_LENGTH_MS = 300
|
||||||
const MAXIMUM_CLICK_DISTANCE_SQ = 50
|
const MAXIMUM_CLICK_DISTANCE_SQ = 50
|
||||||
@ -73,6 +73,8 @@ const outputPortsSet = computed(() => {
|
|||||||
const widthOverridePx = ref<number>()
|
const widthOverridePx = ref<number>()
|
||||||
const nodeId = computed(() => props.node.rootSpan.exprId)
|
const nodeId = computed(() => props.node.rootSpan.exprId)
|
||||||
|
|
||||||
|
onUnmounted(() => graph.unregisterNodeRect(nodeId.value))
|
||||||
|
|
||||||
const rootNode = ref<HTMLElement>()
|
const rootNode = ref<HTMLElement>()
|
||||||
const contentNode = ref<HTMLElement>()
|
const contentNode = ref<HTMLElement>()
|
||||||
const nodeSize = useResizeObserver(rootNode)
|
const nodeSize = useResizeObserver(rootNode)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { GraphDb } from '@/stores/graph/graphDatabase'
|
import { GraphDb } from '@/stores/graph/graphDatabase'
|
||||||
import { Ast } from '@/util/ast'
|
import { Ast } from '@/util/ast'
|
||||||
|
import { moduleMethodNames } from '@/util/ast/abstract'
|
||||||
import { unwrap } from '@/util/data/result'
|
import { unwrap } from '@/util/data/result'
|
||||||
import { tryIdentifier, type Identifier } from '@/util/qualifiedName'
|
import { tryIdentifier, type Identifier } from '@/util/qualifiedName'
|
||||||
import assert from 'assert'
|
import assert from 'assert'
|
||||||
@ -40,8 +41,8 @@ interface RefactoredInfo {
|
|||||||
id: ExprId
|
id: ExprId
|
||||||
/** The pattern of the refactored node. Included for convinience, collapsing does not affect it. */
|
/** The pattern of the refactored node. Included for convinience, collapsing does not affect it. */
|
||||||
pattern: string
|
pattern: string
|
||||||
/** The new expression of the refactored node. A call to the extracted function with the list of necessary arguments. */
|
/** The list of necessary arguments for a call of the collapsed function. */
|
||||||
expression: string
|
arguments: Identifier[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// === prepareCollapsedInfo ===
|
// === prepareCollapsedInfo ===
|
||||||
@ -55,19 +56,20 @@ export function prepareCollapsedInfo(selected: Set<ExprId>, graphDb: GraphDb): C
|
|||||||
const leaves = new Set([...selected])
|
const leaves = new Set([...selected])
|
||||||
const inputs: Identifier[] = []
|
const inputs: Identifier[] = []
|
||||||
let output: Output | null = null
|
let output: Output | null = null
|
||||||
for (const [targetExprId, sourceExprIds] of graphDb.connections.allReverse()) {
|
for (const [targetExprId, sourceExprIds] of graphDb.allConnections.allReverse()) {
|
||||||
const target = graphDb.getExpressionNodeId(targetExprId)
|
const target = graphDb.getExpressionNodeId(targetExprId)
|
||||||
if (target == null) throw new Error(`Connection target node for id ${targetExprId} not found.`)
|
if (target == null) continue
|
||||||
for (const sourceExprId of sourceExprIds) {
|
for (const sourceExprId of sourceExprIds) {
|
||||||
const source = graphDb.getPatternExpressionNodeId(sourceExprId)
|
const source = graphDb.getPatternExpressionNodeId(sourceExprId)
|
||||||
if (source == null)
|
const startsInside = source != null && selected.has(source)
|
||||||
throw new Error(`Connection source node for id ${sourceExprId} not found.`)
|
|
||||||
const startsInside = selected.has(source)
|
|
||||||
const endsInside = selected.has(target)
|
const endsInside = selected.has(target)
|
||||||
const stringIdentifier = graphDb.getOutputPortIdentifier(sourceExprId)
|
const stringIdentifier = graphDb.getOutputPortIdentifier(sourceExprId)
|
||||||
if (stringIdentifier == null) throw new Error(`Source node (${source}) has no pattern.`)
|
if (stringIdentifier == null)
|
||||||
|
throw new Error(`Source node (${source}) has no output identifier.`)
|
||||||
const identifier = unwrap(tryIdentifier(stringIdentifier))
|
const identifier = unwrap(tryIdentifier(stringIdentifier))
|
||||||
leaves.delete(source)
|
if (source != null) {
|
||||||
|
leaves.delete(source)
|
||||||
|
}
|
||||||
if (!startsInside && endsInside) {
|
if (!startsInside && endsInside) {
|
||||||
inputs.push(identifier)
|
inputs.push(identifier)
|
||||||
} else if (startsInside && !endsInside) {
|
} else if (startsInside && !endsInside) {
|
||||||
@ -105,21 +107,109 @@ export function prepareCollapsedInfo(selected: Set<ExprId>, graphDb: GraphDb): C
|
|||||||
refactored: {
|
refactored: {
|
||||||
id: output.node,
|
id: output.node,
|
||||||
pattern,
|
pattern,
|
||||||
expression: 'Main.collapsed' + (inputs.length > 0 ? ' ' : '') + inputs.join(' '),
|
arguments: inputs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === performRefactoring ===
|
/** Generate a safe method name for a collapsed function using `baseName` as a prefix. */
|
||||||
|
function findSafeMethodName(module: Ast.Module, baseName: string): string {
|
||||||
|
const allIdentifiers = moduleMethodNames(module)
|
||||||
|
if (!allIdentifiers.has(baseName)) {
|
||||||
|
return baseName
|
||||||
|
}
|
||||||
|
let index = 1
|
||||||
|
while (allIdentifiers.has(`${baseName}${index}`)) {
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return `${baseName}${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === performCollapse ===
|
||||||
|
|
||||||
|
// We support working inside `Main` module of the project at the moment.
|
||||||
|
const MODULE_NAME = 'Main'
|
||||||
|
const COLLAPSED_FUNCTION_NAME = 'collapsed'
|
||||||
|
|
||||||
/** Perform the actual AST refactoring for collapsing nodes. */
|
/** Perform the actual AST refactoring for collapsing nodes. */
|
||||||
export function performCollapse(_info: CollapsedInfo) {
|
export function performCollapse(
|
||||||
// The general flow of this function:
|
info: CollapsedInfo,
|
||||||
// 1. Create a new function with a unique name and a list of arguments from the `ExtractedInfo`.
|
module: Ast.Module,
|
||||||
// 2. Move all nodes with `ids` from the `ExtractedInfo` into this new function. Use the order of their original definition.
|
topLevel: Ast.BodyBlock,
|
||||||
// 3. Use a single identifier `output.identifier` as the return value of the function.
|
db: GraphDb,
|
||||||
// 4. Change the expression of the `RefactoredInfo.id` node to the `RefactoredINfo.expression`
|
currentMethodName: string,
|
||||||
throw new Error('Not yet implemented, requires AST editing.')
|
): Ast.MutableModule {
|
||||||
|
const functionAst = Ast.findModuleMethod(module, currentMethodName)
|
||||||
|
if (!(functionAst instanceof Ast.Function) || !(functionAst.body instanceof Ast.BodyBlock)) {
|
||||||
|
throw new Error(`Expected a collapsable function, found ${functionAst}.`)
|
||||||
|
}
|
||||||
|
const functionBlock = functionAst.body
|
||||||
|
const posToInsert = findInsertionPos(module, topLevel, currentMethodName)
|
||||||
|
const collapsedName = findSafeMethodName(module, COLLAPSED_FUNCTION_NAME)
|
||||||
|
const astIdsToExtract = new Set(
|
||||||
|
[...info.extracted.ids].map((nodeId) => db.nodeIdToNode.get(nodeId)?.outerExprId),
|
||||||
|
)
|
||||||
|
const astIdToReplace = db.nodeIdToNode.get(info.refactored.id)?.outerExprId
|
||||||
|
const collapsed = []
|
||||||
|
const refactored = []
|
||||||
|
const edit = module.edit()
|
||||||
|
const lines = functionBlock.lines()
|
||||||
|
for (const line of lines) {
|
||||||
|
const astId = line.expression?.node.exprId
|
||||||
|
const ast = astId != null ? module.get(astId) : null
|
||||||
|
if (ast == null) continue
|
||||||
|
if (astIdsToExtract.has(astId)) {
|
||||||
|
collapsed.push(ast)
|
||||||
|
if (astId === astIdToReplace) {
|
||||||
|
const newAst = collapsedCallAst(info, collapsedName, edit)
|
||||||
|
refactored.push({ expression: { node: newAst } })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
refactored.push({ expression: { node: ast } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const outputIdentifier = info.extracted.output?.identifier
|
||||||
|
if (outputIdentifier != null) {
|
||||||
|
collapsed.push(Ast.Ident.new(edit, outputIdentifier))
|
||||||
|
}
|
||||||
|
// Update the definiton of refactored function.
|
||||||
|
const refactoredBlock = Ast.BodyBlock.new(refactored, edit)
|
||||||
|
edit.replaceRef(functionBlock.exprId, refactoredBlock)
|
||||||
|
// new Ast.BodyBlock(edit, functionBlock.exprId, refactored)
|
||||||
|
|
||||||
|
const args: Ast.Ast[] = info.extracted.inputs.map((arg) => Ast.Ident.new(edit, arg))
|
||||||
|
const collapsedFunction = Ast.Function.new(edit, collapsedName, args, collapsed, true)
|
||||||
|
topLevel.insert(edit, posToInsert, collapsedFunction)
|
||||||
|
return edit
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prepare a method call expression for collapsed method. */
|
||||||
|
function collapsedCallAst(
|
||||||
|
info: CollapsedInfo,
|
||||||
|
collapsedName: string,
|
||||||
|
edit: Ast.MutableModule,
|
||||||
|
): Ast.Ast {
|
||||||
|
const pattern = info.refactored.pattern
|
||||||
|
const args = info.refactored.arguments
|
||||||
|
const functionName = `${MODULE_NAME}.${collapsedName}`
|
||||||
|
const expression = functionName + (args.length > 0 ? ' ' : '') + args.join(' ')
|
||||||
|
const assignment = Ast.Assignment.new(edit, pattern, Ast.parse(expression, edit))
|
||||||
|
return assignment
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find the position before the current method to insert a collapsed one. */
|
||||||
|
function findInsertionPos(
|
||||||
|
module: Ast.Module,
|
||||||
|
topLevel: Ast.BodyBlock,
|
||||||
|
currentMethodName: string,
|
||||||
|
): number {
|
||||||
|
const currentFuncPosition = topLevel.lines().findIndex((line) => {
|
||||||
|
const node = line.expression?.node
|
||||||
|
const expr = node ? module.get(node.exprId)?.innerExpression() : null
|
||||||
|
return expr instanceof Ast.Function && expr.name?.code() === currentMethodName
|
||||||
|
})
|
||||||
|
|
||||||
|
return currentFuncPosition === -1 ? 0 : currentFuncPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Tests ===
|
// === Tests ===
|
||||||
@ -148,7 +238,7 @@ if (import.meta.vitest) {
|
|||||||
}
|
}
|
||||||
refactored: {
|
refactored: {
|
||||||
replace: string
|
replace: string
|
||||||
with: { pattern: string; expression: string }
|
with: { pattern: string; arguments: string[] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -166,7 +256,7 @@ if (import.meta.vitest) {
|
|||||||
},
|
},
|
||||||
refactored: {
|
refactored: {
|
||||||
replace: 'c = A + B',
|
replace: 'c = A + B',
|
||||||
with: { pattern: 'c', expression: 'Main.collapsed a' },
|
with: { pattern: 'c', arguments: ['a'] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -182,7 +272,7 @@ if (import.meta.vitest) {
|
|||||||
},
|
},
|
||||||
refactored: {
|
refactored: {
|
||||||
replace: 'd = a + b',
|
replace: 'd = a + b',
|
||||||
with: { pattern: 'd', expression: 'Main.collapsed a b' },
|
with: { pattern: 'd', arguments: ['a', 'b'] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -198,7 +288,7 @@ if (import.meta.vitest) {
|
|||||||
},
|
},
|
||||||
refactored: {
|
refactored: {
|
||||||
replace: 'c = 50 + d',
|
replace: 'c = 50 + d',
|
||||||
with: { pattern: 'c', expression: 'Main.collapsed' },
|
with: { pattern: 'c', arguments: [] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -219,7 +309,7 @@ if (import.meta.vitest) {
|
|||||||
},
|
},
|
||||||
refactored: {
|
refactored: {
|
||||||
replace: 'vector = range.to_vector',
|
replace: 'vector = range.to_vector',
|
||||||
with: { pattern: 'vector', expression: 'Main.collapsed number1 number2' },
|
with: { pattern: 'vector', arguments: ['number1', 'number2'] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -261,6 +351,6 @@ if (import.meta.vitest) {
|
|||||||
expect(extracted.ids).toEqual(new Set(expectedIds))
|
expect(extracted.ids).toEqual(new Set(expectedIds))
|
||||||
expect(refactored.id).toEqual(expectedRefactoredId)
|
expect(refactored.id).toEqual(expectedRefactoredId)
|
||||||
expect(refactored.pattern).toEqual(expectedRefactored.with.pattern)
|
expect(refactored.pattern).toEqual(expectedRefactored.with.pattern)
|
||||||
expect(refactored.expression).toEqual(expectedRefactored.with.expression)
|
expect(refactored.arguments).toEqual(expectedRefactored.with.arguments)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -30,16 +30,7 @@ export function useStackNavigator() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function stackItemToLabel(item: StackItem): string {
|
function stackItemToLabel(item: StackItem): string {
|
||||||
switch (item.type) {
|
return graphStore.db.stackItemToMethodName(item) ?? 'unknown'
|
||||||
case 'ExplicitCall': {
|
|
||||||
return item.methodPointer.name
|
|
||||||
}
|
|
||||||
case 'LocalCall': {
|
|
||||||
const exprId = item.expressionId
|
|
||||||
const info = graphStore.db.getExpressionInfo(exprId)
|
|
||||||
return info?.methodCall?.methodPointer.name ?? 'unknown'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBreadcrumbClick(index: number) {
|
function handleBreadcrumbClick(index: number) {
|
||||||
|
@ -12,7 +12,7 @@ import { Vec2 } from '@/util/data/vec2'
|
|||||||
import { ReactiveDb, ReactiveIndex, ReactiveMapping } from '@/util/database/reactiveDb'
|
import { ReactiveDb, ReactiveIndex, ReactiveMapping } from '@/util/database/reactiveDb'
|
||||||
import * as random from 'lib0/random'
|
import * as random from 'lib0/random'
|
||||||
import * as set from 'lib0/set'
|
import * as set from 'lib0/set'
|
||||||
import { methodPointerEquals, type MethodCall } from 'shared/languageServerTypes'
|
import { methodPointerEquals, type MethodCall, type StackItem } from 'shared/languageServerTypes'
|
||||||
import {
|
import {
|
||||||
IdMap,
|
IdMap,
|
||||||
visMetadataEquals,
|
visMetadataEquals,
|
||||||
@ -137,17 +137,30 @@ export class GraphDb {
|
|||||||
// Display connection starting from existing node.
|
// Display connection starting from existing node.
|
||||||
//TODO[ao]: When implementing input nodes, they should be taken into account here.
|
//TODO[ao]: When implementing input nodes, they should be taken into account here.
|
||||||
if (srcNode == null) return []
|
if (srcNode == null) return []
|
||||||
function* allTargets(db: GraphDb): Generator<[ExprId, ExprId]> {
|
return Array.from(this.connectionsFromBindings(info, alias, srcNode))
|
||||||
for (const usage of info.usages) {
|
|
||||||
const targetNode = db.getExpressionNodeId(usage)
|
|
||||||
// Display only connections to existing targets and different than source node
|
|
||||||
if (targetNode == null || targetNode === srcNode) continue
|
|
||||||
yield [alias, usage]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(allTargets(this))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** Same as {@link GraphDb.connections}, but also includes connections without source node,
|
||||||
|
* e.g. input arguments of the collapsed function.
|
||||||
|
*/
|
||||||
|
allConnections = new ReactiveIndex(this.bindings.bindings, (alias, info) => {
|
||||||
|
const srcNode = this.getPatternExpressionNodeId(alias)
|
||||||
|
return Array.from(this.connectionsFromBindings(info, alias, srcNode))
|
||||||
|
})
|
||||||
|
|
||||||
|
private *connectionsFromBindings(
|
||||||
|
info: BindingInfo,
|
||||||
|
alias: ExprId,
|
||||||
|
srcNode: ExprId | undefined,
|
||||||
|
): Generator<[ExprId, ExprId]> {
|
||||||
|
for (const usage of info.usages) {
|
||||||
|
const targetNode = this.getExpressionNodeId(usage)
|
||||||
|
// Display only connections to existing targets and different than source node.
|
||||||
|
if (targetNode == null || targetNode === srcNode) continue
|
||||||
|
yield [alias, usage]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Output port bindings of the node. Lists all bindings that can be dragged out from a node. */
|
/** Output port bindings of the node. Lists all bindings that can be dragged out from a node. */
|
||||||
nodeOutputPorts = new ReactiveIndex(this.nodeIdToNode, (id, entry) => {
|
nodeOutputPorts = new ReactiveIndex(this.nodeIdToNode, (id, entry) => {
|
||||||
if (entry.pattern == null) return []
|
if (entry.pattern == null) return []
|
||||||
@ -206,6 +219,10 @@ export class GraphDb {
|
|||||||
return this.bindings.bindings.get(source)?.identifier
|
return this.bindings.bindings.get(source)?.identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allIdentifiers(): string[] {
|
||||||
|
return [...this.bindings.identifierToBindingId.allForward()].map(([ident, _]) => ident)
|
||||||
|
}
|
||||||
|
|
||||||
identifierUsed(ident: string): boolean {
|
identifierUsed(ident: string): boolean {
|
||||||
return this.bindings.identifierToBindingId.hasKey(ident)
|
return this.bindings.identifierToBindingId.hasKey(ident)
|
||||||
}
|
}
|
||||||
@ -249,6 +266,20 @@ export class GraphDb {
|
|||||||
this.nodeIdToNode.moveToLast(id)
|
this.nodeIdToNode.moveToLast(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the method name from the stack item. */
|
||||||
|
stackItemToMethodName(item: StackItem): string | undefined {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'ExplicitCall': {
|
||||||
|
return item.methodPointer.name
|
||||||
|
}
|
||||||
|
case 'LocalCall': {
|
||||||
|
const exprId = item.expressionId
|
||||||
|
const info = this.getExpressionInfo(exprId)
|
||||||
|
return info?.methodCall?.methodPointer.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
readFunctionAst(functionAst_: Ast.Function, getMeta: (id: ExprId) => NodeMetadata | undefined) {
|
readFunctionAst(functionAst_: Ast.Function, getMeta: (id: ExprId) => NodeMetadata | undefined) {
|
||||||
const currentNodeIds = new Set<ExprId>()
|
const currentNodeIds = new Set<ExprId>()
|
||||||
for (const nodeAst of functionAst_.bodyExpressions()) {
|
for (const nodeAst of functionAst_.bodyExpressions()) {
|
||||||
|
@ -63,6 +63,8 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
const astModule: Module = MutableModule.Observable()
|
const astModule: Module = MutableModule.Observable()
|
||||||
const moduleRoot = ref<AstId>()
|
const moduleRoot = ref<AstId>()
|
||||||
let moduleDirty = false
|
let moduleDirty = false
|
||||||
|
const nodeRects = reactive(new Map<ExprId, Rect>())
|
||||||
|
const vizRects = reactive(new Map<ExprId, Rect>())
|
||||||
|
|
||||||
// Initialize text and idmap once module is loaded (data != null)
|
// Initialize text and idmap once module is loaded (data != null)
|
||||||
watch(data, () => {
|
watch(data, () => {
|
||||||
@ -78,8 +80,6 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
toRef(suggestionDb, 'groups'),
|
toRef(suggestionDb, 'groups'),
|
||||||
proj.computedValueRegistry,
|
proj.computedValueRegistry,
|
||||||
)
|
)
|
||||||
const nodeRects = reactive(new Map<ExprId, Rect>())
|
|
||||||
const vizRects = reactive(new Map<ExprId, Rect>())
|
|
||||||
const portInstances = reactive(new Map<PortId, Set<PortViewInstance>>())
|
const portInstances = reactive(new Map<PortId, Set<PortViewInstance>>())
|
||||||
const editedNodeInfo = ref<NodeEditInfo>()
|
const editedNodeInfo = ref<NodeEditInfo>()
|
||||||
const imports = ref<{ import: Import; span: SourceRange }[]>([])
|
const imports = ref<{ import: Import; span: SourceRange }[]>([])
|
||||||
@ -224,11 +224,15 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
commitEdit(edit, new Map([[rhs.exprId, meta]]))
|
commitEdit(edit, new Map([[rhs.exprId, meta]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editAst(cb: (module: Ast.Module) => Ast.MutableModule) {
|
||||||
|
const edit = cb(astModule)
|
||||||
|
commitEdit(edit)
|
||||||
|
}
|
||||||
|
|
||||||
function deleteNode(id: ExprId) {
|
function deleteNode(id: ExprId) {
|
||||||
const node = db.nodeIdToNode.get(id)
|
const node = db.nodeIdToNode.get(id)
|
||||||
if (!node) return
|
if (!node) return
|
||||||
proj.module?.doc.metadata.delete(node.outerExprId)
|
proj.module?.doc.metadata.delete(node.outerExprId)
|
||||||
nodeRects.delete(id)
|
|
||||||
const root = moduleRoot.value
|
const root = moduleRoot.value
|
||||||
if (!root) {
|
if (!root) {
|
||||||
console.error(`BUG: Cannot delete node: No module root.`)
|
console.error(`BUG: Cannot delete node: No module root.`)
|
||||||
@ -316,6 +320,11 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
else vizRects.delete(id)
|
else vizRects.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unregisterNodeRect(id: ExprId) {
|
||||||
|
nodeRects.delete(id)
|
||||||
|
vizRects.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
function addPortInstance(id: PortId, instance: PortViewInstance) {
|
function addPortInstance(id: PortId, instance: PortViewInstance) {
|
||||||
map.setIfUndefined(portInstances, id, set.create).add(instance)
|
map.setIfUndefined(portInstances, id, set.create).add(instance)
|
||||||
}
|
}
|
||||||
@ -413,12 +422,15 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
moduleCode,
|
moduleCode,
|
||||||
nodeRects,
|
nodeRects,
|
||||||
vizRects,
|
vizRects,
|
||||||
|
unregisterNodeRect,
|
||||||
methodAst,
|
methodAst,
|
||||||
|
editAst,
|
||||||
astModule,
|
astModule,
|
||||||
createEdgeFromOutput,
|
createEdgeFromOutput,
|
||||||
disconnectSource,
|
disconnectSource,
|
||||||
disconnectTarget,
|
disconnectTarget,
|
||||||
clearUnconnected,
|
clearUnconnected,
|
||||||
|
moduleRoot,
|
||||||
createNode,
|
createNode,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
setNodeContent,
|
setNodeContent,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
import { entryQn, type SuggestionEntry, type SuggestionId } from '@/stores/suggestionDatabase/entry'
|
import { entryQn, type SuggestionEntry, type SuggestionId } from '@/stores/suggestionDatabase/entry'
|
||||||
import { applyUpdates, entryFromLs } from '@/stores/suggestionDatabase/lsUpdate'
|
import { applyUpdates, entryFromLs } from '@/stores/suggestionDatabase/lsUpdate'
|
||||||
import { type Opt } from '@/util/data/opt'
|
|
||||||
import { ReactiveDb, ReactiveIndex } from '@/util/database/reactiveDb'
|
import { ReactiveDb, ReactiveIndex } from '@/util/database/reactiveDb'
|
||||||
import { AsyncQueue, rpcWithRetries } from '@/util/net'
|
import { AsyncQueue, rpcWithRetries } from '@/util/net'
|
||||||
import { qnJoin, qnParent, tryQualifiedName, type QualifiedName } from '@/util/qualifiedName'
|
import { qnJoin, qnParent, tryQualifiedName, type QualifiedName } from '@/util/qualifiedName'
|
||||||
|
@ -1017,6 +1017,26 @@ export class Function extends Ast {
|
|||||||
this.body_ = body
|
this.body_ = body
|
||||||
setParent(module, this.exprId, ...this.concreteChildren())
|
setParent(module, this.exprId, ...this.concreteChildren())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static new(
|
||||||
|
module: MutableModule,
|
||||||
|
name: string,
|
||||||
|
args: Ast[],
|
||||||
|
exprs: Ast[],
|
||||||
|
trailingNewline?: boolean,
|
||||||
|
): Function {
|
||||||
|
const id = newAstId()
|
||||||
|
const exprs_: BlockLine[] = exprs.map((expr) => ({ expression: { node: expr } }))
|
||||||
|
if (trailingNewline) {
|
||||||
|
exprs_.push({ newline: { node: Token.new('\n') }, expression: null })
|
||||||
|
}
|
||||||
|
const body = BodyBlock.new(exprs_, module)
|
||||||
|
const args_ = args.map((arg) => [{ node: makeChild(module, arg, id) }])
|
||||||
|
const ident = { node: Ident.new(module, name).exprId }
|
||||||
|
const equals = { node: Token.new('=') }
|
||||||
|
return new Function(module, id, ident, args_, equals, { node: body.exprId })
|
||||||
|
}
|
||||||
|
|
||||||
*concreteChildren(): IterableIterator<NodeChild> {
|
*concreteChildren(): IterableIterator<NodeChild> {
|
||||||
yield this.name_
|
yield this.name_
|
||||||
for (const arg of this.args_) yield* arg
|
for (const arg of this.args_) yield* arg
|
||||||
@ -1582,6 +1602,16 @@ export function tokenTreeWithIds(root: Ast): TokenTree {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function moduleMethodNames(module: Module): Set<string> {
|
||||||
|
const result = new Set<string>()
|
||||||
|
for (const node of module.raw.nodes.values()) {
|
||||||
|
if (node instanceof Function && node.name) {
|
||||||
|
result.add(node.name.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: We should use alias analysis to handle ambiguous names correctly.
|
// FIXME: We should use alias analysis to handle ambiguous names correctly.
|
||||||
export function findModuleMethod(module: Module, name: string): Function | null {
|
export function findModuleMethod(module: Module, name: string): Function | null {
|
||||||
for (const node of module.raw.nodes.values()) {
|
for (const node of module.raw.nodes.values()) {
|
||||||
|
@ -7,7 +7,7 @@ import diff from 'fast-diff'
|
|||||||
import * as json from 'lib0/json'
|
import * as json from 'lib0/json'
|
||||||
import * as Y from 'yjs'
|
import * as Y from 'yjs'
|
||||||
import { TextEdit } from '../shared/languageServerTypes'
|
import { TextEdit } from '../shared/languageServerTypes'
|
||||||
import { IdMap, ModuleDoc, type NodeMetadata, type VisualizationMetadata } from '../shared/yjsModel'
|
import { ModuleDoc, type NodeMetadata, type VisualizationMetadata } from '../shared/yjsModel'
|
||||||
import * as fileFormat from './fileFormat'
|
import * as fileFormat from './fileFormat'
|
||||||
import { serializeIdMap } from './serialization'
|
import { serializeIdMap } from './serialization'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user