Do not crash on empty main function body. (#10990)

Fixes #10976

https://github.com/user-attachments/assets/00b2279d-2acf-468b-8c3c-aa6885cba23d


Addressed issue of empty body block being incorrectly "repaired" into an empty group, geneating invalid `()` syntax. Appending nodes to an empty function now actually replaces its body block, instead of creating a temporary orphan body block node.

Note that empty main function is still considered an error on the engine side, but it doesn't impact the IDE in negative way. Things work again as soon as a node is inserted.

Also fixed a few issues causing hot-reloading to break. Now edited AST code properly hot-reloads all affected modules without breaking the app.
This commit is contained in:
Paweł Grabarz 2024-09-06 20:45:20 +02:00 committed by GitHub
parent 3420a05a84
commit 6bfdda33a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 46 additions and 27 deletions

View File

@ -417,7 +417,7 @@ watchEffect(() => {
@pointerleave="(nodeHovered = false), updateNodeHover(undefined)"
@pointermove="updateNodeHover"
>
<Teleport v-if="navigator && !edited" :to="graphNodeSelections">
<Teleport v-if="navigator && !edited && graphNodeSelections" :to="graphNodeSelections">
<GraphNodeSelection
:data-node-id="nodeId"
:nodePosition="props.node.position"

View File

@ -164,20 +164,20 @@ export function useNavigator(
function panToImpl(points: Partial<Vec2>[]) {
let target = viewport.value
for (const point of points.reverse()) target = target.offsetToInclude(point) ?? target
targetCenter.value = target.center()
targetCenter.value = target.center().finiteOrZero()
}
/** Pan immediately to center the viewport at the given point, in scene coordinates. */
function scrollTo(newCenter: Vec2) {
resetTargetFollowing()
targetCenter.value = newCenter
targetCenter.value = newCenter.finiteOrZero()
center.skip()
}
/** Set viewport center point and scale value immediately, skipping animations. */
function setCenterAndScale(newCenter: Vec2, newScale: number) {
resetTargetFollowing()
targetCenter.value = newCenter
targetCenter.value = newCenter.finiteOrZero()
targetScale.value = newScale
scale.skip()
center.skip()
@ -325,7 +325,7 @@ export function useNavigator(
const scenePos0 = clientToScenePos(clientPos)
const result = f()
const scenePos1 = clientToScenePos(clientPos)
targetCenter.value = center.value.add(scenePos0.sub(scenePos1))
targetCenter.value = center.value.add(scenePos0.sub(scenePos1)).finiteOrZero()
center.skip()
return result
}

View File

@ -31,7 +31,7 @@ const MISSING = Symbol('MISSING')
* [Context API]: https://vuejs.org/guide/components/provide-inject.html#provide-inject
*/
export function createContextStore<F extends (...args: any[]) => any>(name: string, factory: F) {
const provideKey = Symbol(name) as InjectionKey<ReturnType<F>>
const provideKey = Symbol.for(`contextStore-${name}`) as InjectionKey<ReturnType<F>>
/**
* Create the instance of a store and store it in the current component's context. All child

View File

@ -44,7 +44,7 @@ export class BindingsDb {
readFunctionAst(
func: Ast.Function,
rawFunc: RawAst.Tree.Function,
rawFunc: RawAst.Tree.Function | undefined,
moduleCode: string,
getSpan: (id: AstId) => SourceRange | undefined,
) {
@ -422,7 +422,7 @@ export class GraphDb {
/** Deeply scan the function to perform alias-analysis. */
updateBindings(
functionAst_: Ast.Function,
rawFunction: RawAst.Tree.Function,
rawFunction: RawAst.Tree.Function | undefined,
moduleCode: string,
getSpan: (id: AstId) => SourceRange | undefined,
) {

View File

@ -179,7 +179,6 @@ export const { injectFn: useGraphStore, provideFn: provideGraphStore } = createC
const methodSpan = moduleSource.getSpan(method.id)
assert(methodSpan != null)
const rawFunc = toRaw.get(sourceRangeKey(methodSpan))
assert(rawFunc != null)
const getSpan = (id: AstId) => moduleSource.getSpan(id)
db.updateBindings(method, rawFunc, moduleSource.text, getSpan)
})

View File

@ -15,7 +15,15 @@ import { LazySyncEffectSet, NonReactiveView } from '@/util/reactivity'
import * as map from 'lib0/map'
import { ObservableV2 } from 'lib0/observable'
import * as set from 'lib0/set'
import { computed, effectScope, reactive, toRaw, type ComputedRef, type DebuggerOptions } from 'vue'
import {
computed,
effectScope,
onScopeDispose,
reactive,
toRaw,
type ComputedRef,
type DebuggerOptions,
} from 'vue'
export type OnDelete = (cleanupFn: () => void) => void
@ -218,14 +226,18 @@ export class ReactiveIndex<K, V, IK, IV> {
constructor(db: ReactiveDb<K, V>, indexer: Indexer<K, V, IK, IV>) {
this.forward = reactive(map.create())
this.reverse = reactive(map.create())
this.effects = new LazySyncEffectSet()
db.on('entryAdded', (key, value, onDelete) => {
const stopEffect = this.effects.lazyEffect((onCleanup) => {
const keyValues = indexer(key, value)
keyValues.forEach(([key, value]) => this.writeToIndex(key, value))
onCleanup(() => keyValues.forEach(([key, value]) => this.removeFromIndex(key, value)))
const scope = effectScope()
this.effects = new LazySyncEffectSet(scope)
scope.run(() => {
const handler = db.on('entryAdded', (key, value, onDelete) => {
const stopEffect = this.effects.lazyEffect((onCleanup) => {
const keyValues = indexer(key, value)
keyValues.forEach(([key, value]) => this.writeToIndex(key, value))
onCleanup(() => keyValues.forEach(([key, value]) => this.removeFromIndex(key, value)))
})
onDelete(() => stopEffect())
})
onDelete(() => stopEffect())
onScopeDispose(() => db.off('entryAdded', handler))
})
}
@ -353,16 +365,20 @@ export class ReactiveMapping<K, V, M> {
*/
constructor(db: ReactiveDb<K, V>, indexer: Mapper<K, V, M>, debugOptions?: DebuggerOptions) {
this.computed = reactive(map.create())
db.on('entryAdded', (key, value, onDelete) => {
const scope = effectScope()
const mappedValue = scope.run(() =>
computed(() => scope.run(() => indexer(key, value)), debugOptions),
)! // This non-null assertion is SAFE, as the scope is initially active.
this.computed.set(key, mappedValue)
onDelete(() => {
scope.stop()
this.computed.delete(key)
const scope = effectScope()
scope.run(() => {
const handler = db.on('entryAdded', (key, value, onDelete) => {
const scope = effectScope()
const mappedValue = scope.run(() =>
computed(() => scope.run(() => indexer(key, value)), debugOptions),
)! // This non-null assertion is SAFE, as the scope is initially active.
this.computed.set(key, mappedValue)
onDelete(() => {
scope.stop()
this.computed.delete(key)
})
})
onScopeDispose(() => db.off('entryAdded', handler))
})
}

View File

@ -45,9 +45,10 @@ export type StopEffect = () => void
*/
export class LazySyncEffectSet {
_dirtyRunners = new Set<() => void>()
_scope = effectScope()
_boundFlush = this.flush.bind(this)
constructor(private _scope = effectScope()) {}
/**
* Add an effect to the lazy set. The effect will run once immediately, and any subsequent runs
* will be delayed until the next flush. Only effects that were notified about a dependency change

View File

@ -668,6 +668,8 @@ function checkSpans(expected: NodeSpanMap, encountered: NodeSpanMap, code: strin
const lostBlock = new Array<Ast>()
for (const [key, ast] of lost) {
const [start, end] = sourceRangeFromKey(key)
// Do not report lost empty body blocks, we don't want them to be considered for repair.
if (start === end && ast instanceof BodyBlock) continue
;(code.substring(start, end).match(/[\r\n]/) ? lostBlock : lostInline).push(ast)
}
return { lostInline, lostBlock }

View File

@ -1962,6 +1962,7 @@ export class MutableFunction extends Function implements MutableAst {
if (oldBody instanceof MutableBodyBlock) return oldBody
const newBody = BodyBlock.new([], this.module)
if (oldBody) newBody.push(oldBody.take())
this.setBody(newBody)
return newBody
}
}