mirror of
https://github.com/enso-org/enso.git
synced 2024-11-29 17:22:57 +03:00
Widget refactoring & show widget in placeholders (#8687)
[Screencast from 2024-01-05 12-18-55.webm](https://github.com/enso-org/enso/assets/3919101/f083512f-f698-42d1-b43a-4e50546b958a) * Before most widgets didn't show in placeholders, because the argument name widget did not create further widgets. Now it try in more cases, and ignores the problem if no widget is found. * WidgetInput is no longer a set of classes - instead it contains a set of common fields, and every widget can just extend it with new fields. Adjusted priorities accordingly. * Updated widget definitions: they better try to match types, and take care about default values. # Important Notes Setting value on placeholder breaks widgets. The issue is also present in develop (when connecting to placeholder). To restore node, you have to reopen the project.
This commit is contained in:
parent
942e6c2305
commit
c64e833307
@ -13,6 +13,7 @@ import { computed, proxyRefs } from 'vue'
|
||||
const props = defineProps<{
|
||||
input: WidgetInput
|
||||
nest?: boolean
|
||||
allowEmpty?: boolean
|
||||
/**
|
||||
* A function that intercepts and handles a value update emitted by this widget. When it returns
|
||||
* `false`, the update continues to be propagated to the parent widget. When it returns `true`,
|
||||
@ -97,7 +98,7 @@ const spanStart = computed(() => {
|
||||
@update="updateHandler"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
v-else-if="!props.allowEmpty"
|
||||
:title="`No matching widget for input: ${
|
||||
Object.getPrototypeOf(props.input)?.constructor?.name ?? JSON.stringify(props.input)
|
||||
}`"
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { useTransitioning } from '@/composables/animation'
|
||||
import { ForcePort, type PortId } from '@/providers/portInfo'
|
||||
import { AnyWidget } from '@/providers/widgetRegistry'
|
||||
import { type PortId } from '@/providers/portInfo'
|
||||
import { WidgetInput } from '@/providers/widgetRegistry'
|
||||
import { provideWidgetTree } from '@/providers/widgetTree'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { Ast } from '@/util/ast'
|
||||
@ -12,10 +12,11 @@ import { computed, toRef } from 'vue'
|
||||
const props = defineProps<{ ast: Ast.Ast }>()
|
||||
const graph = useGraphStore()
|
||||
const rootPort = computed(() => {
|
||||
const input = AnyWidget.Ast(props.ast)
|
||||
return props.ast instanceof Ast.Ident && !graph.db.isKnownFunctionCall(props.ast.exprId)
|
||||
? new ForcePort(input)
|
||||
: input
|
||||
const input = WidgetInput.FromAst(props.ast)
|
||||
if (props.ast instanceof Ast.Ident && !graph.db.isKnownFunctionCall(props.ast.exprId)) {
|
||||
input.forcePort = true
|
||||
}
|
||||
return input
|
||||
})
|
||||
|
||||
const observedLayoutTransitions = new Set([
|
||||
|
@ -1,27 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { ForcePort } from '@/providers/portInfo'
|
||||
import { AnyWidget, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { ArgumentApplication, ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { ArgumentApplicationKey, ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
const targetMaybePort = computed(() =>
|
||||
props.input.target instanceof ArgumentPlaceholder || props.input.target instanceof ArgumentAst
|
||||
? new ForcePort(props.input.target.toAnyWidget())
|
||||
: props.input.target instanceof Ast.Ast
|
||||
? AnyWidget.Ast(props.input.target)
|
||||
: props.input.target,
|
||||
)
|
||||
const application = computed(() => props.input[ArgumentApplicationKey])
|
||||
const targetMaybePort = computed(() => {
|
||||
const target = application.value.target
|
||||
return target instanceof ArgumentPlaceholder || target instanceof ArgumentAst
|
||||
? { ...target.toWidgetInput(), forcePort: true }
|
||||
: target instanceof Ast.Ast
|
||||
? WidgetInput.FromAst(target)
|
||||
: target.toWidgetInput()
|
||||
})
|
||||
|
||||
const appClass = computed(() => {
|
||||
return props.input.infixOperator != null ? 'infix' : 'prefix'
|
||||
return application.value.infixOperator != null ? 'infix' : 'prefix'
|
||||
})
|
||||
|
||||
const operatorStyle = computed(() => {
|
||||
if (props.input.appTree instanceof Ast.OprApp) {
|
||||
const [_lhs, opr, rhs] = props.input.appTree.concreteChildren()
|
||||
if (application.value.appTree instanceof Ast.OprApp) {
|
||||
const [_lhs, opr, rhs] = application.value.appTree.concreteChildren()
|
||||
return {
|
||||
'--whitespace-pre': `${JSON.stringify(opr?.whitespace ?? '')}`,
|
||||
'--whitespace-post': `${JSON.stringify(rhs?.whitespace ?? '')}`,
|
||||
@ -32,18 +33,18 @@ const operatorStyle = computed(() => {
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(ArgumentApplication, {
|
||||
priority: 1000,
|
||||
export const widgetDefinition = defineWidget(ArgumentApplicationKey, {
|
||||
priority: -20,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="WidgetApplication" :class="appClass">
|
||||
<NodeWidget :input="targetMaybePort" />
|
||||
<div v-if="props.input.infixOperator" class="infixOp" :style="operatorStyle">
|
||||
<NodeWidget :input="props.input.infixOperator" />
|
||||
<div v-if="application.infixOperator" class="infixOp" :style="operatorStyle">
|
||||
<NodeWidget :input="WidgetInput.FromAst(application.infixOperator)" />
|
||||
</div>
|
||||
<NodeWidget :input="props.input.argument" />
|
||||
<NodeWidget :input="application.argument.toWidgetInput()" nest />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { ArgumentAst } from '@/util/callTree'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(ArgumentAst, {
|
||||
priority: 1001,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NodeWidget :input="props.input.toAnyWidget()" nest />
|
||||
</template>
|
@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { injectPortInfo } from '@/providers/portInfo'
|
||||
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { ApplicationKind, ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { ApplicationKind, ArgumentInfoKey, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import type { SuggestionEntryArgument } from 'shared/languageServerTypes/suggestions'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
@ -10,10 +12,10 @@ const props = defineProps(widgetProps(widgetDefinition))
|
||||
const portInfo = injectPortInfo(true)
|
||||
const showArgumentValue = computed(() => {
|
||||
return (
|
||||
props.input instanceof ArgumentAst &&
|
||||
(portInfo == null ||
|
||||
!portInfo.connected ||
|
||||
(portInfo.portId as string) !== (props.input.ast.exprId as string))
|
||||
!WidgetInput.isAst(props.input) ||
|
||||
portInfo == null ||
|
||||
!portInfo.connected ||
|
||||
(portInfo.portId as string) !== (props.input.value.exprId as string)
|
||||
)
|
||||
})
|
||||
|
||||
@ -22,11 +24,19 @@ const primary = computed(() => props.nesting < 2)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget([ArgumentAst.matchWithArgInfo, ArgumentPlaceholder], {
|
||||
function hasKnownArgumentName(input: WidgetInput): input is WidgetInput & {
|
||||
value: Ast.Ast | string | undefined
|
||||
[ArgumentInfoKey]: { info: SuggestionEntryArgument }
|
||||
} {
|
||||
return !WidgetInput.isToken(input) && input[ArgumentInfoKey]?.info != null
|
||||
}
|
||||
|
||||
export const widgetDefinition = defineWidget(hasKnownArgumentName, {
|
||||
priority: 1000,
|
||||
score: (props) => {
|
||||
const isPlaceholder = props.input instanceof ArgumentPlaceholder
|
||||
const isTopArg = props.nesting < 2 && props.input.kind === ApplicationKind.Prefix
|
||||
const isPlaceholder = !(props.input.value instanceof Ast.Ast)
|
||||
const isTopArg =
|
||||
props.nesting < 2 && props.input[ArgumentInfoKey].appKind === ApplicationKind.Prefix
|
||||
return isPlaceholder || isTopArg ? Score.Perfect : Score.Mismatch
|
||||
},
|
||||
})
|
||||
@ -35,10 +45,10 @@ export const widgetDefinition = defineWidget([ArgumentAst.matchWithArgInfo, Argu
|
||||
<template>
|
||||
<div class="WidgetArgumentName" :class="{ placeholder, primary }">
|
||||
<template v-if="showArgumentValue">
|
||||
<span class="value">{{ props.input.argInfo.name }}</span
|
||||
><NodeWidget :input="props.input" />
|
||||
<span class="name">{{ props.input[ArgumentInfoKey].info.name }}</span
|
||||
><NodeWidget :input="props.input" allowEmpty />
|
||||
</template>
|
||||
<template v-else>{{ props.input.argInfo.name }}</template>
|
||||
<template v-else>{{ props.input[ArgumentInfoKey].info.name }}</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -50,11 +60,12 @@ export const widgetDefinition = defineWidget([ArgumentAst.matchWithArgInfo, Argu
|
||||
}
|
||||
|
||||
.placeholder,
|
||||
.value {
|
||||
.name {
|
||||
color: rgb(255 255 255 / 0.5);
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-right: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,17 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
const _props = defineProps(widgetProps(widgetDefinition))
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(
|
||||
(input) => input instanceof AnyWidget && input.ast instanceof Ast.Wildcard,
|
||||
{
|
||||
priority: 10,
|
||||
score: Score.Good,
|
||||
},
|
||||
)
|
||||
export const widgetDefinition = defineWidget(WidgetInput.astMatcher(Ast.Wildcard), {
|
||||
priority: 1001,
|
||||
score: Score.Good,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import CheckboxWidget from '@/components/widgets/CheckboxWidget.vue'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@ -8,13 +8,16 @@ const props = defineProps(widgetProps(widgetDefinition))
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.input.ast?.code().endsWith('True') ?? false
|
||||
return WidgetInput.valueRepr(props.input)?.endsWith('True') ?? false
|
||||
},
|
||||
set(value) {
|
||||
if (props.input.ast == null) return // TODO[ao] set value on placeholder here.
|
||||
const node = getRawBoolNode(props.input.ast)
|
||||
if (node != null) {
|
||||
props.onUpdate(value ? 'True' : 'False', node.exprId)
|
||||
if (props.input.value instanceof Ast.Ast) {
|
||||
const node = getRawBoolNode(props.input.value)
|
||||
if (node != null) {
|
||||
props.onUpdate(value ? 'True' : 'False', node.exprId)
|
||||
}
|
||||
} else {
|
||||
props.onUpdate(value ? 'Boolean.True' : 'Boolean.False', props.input.portId)
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -30,13 +33,14 @@ function getRawBoolNode(ast: Ast.Ast) {
|
||||
return null
|
||||
}
|
||||
|
||||
export const widgetDefinition = defineWidget(AnyWidget, {
|
||||
priority: 10,
|
||||
export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
|
||||
priority: 1001,
|
||||
score: (props) => {
|
||||
if (props.input.ast == null)
|
||||
return props.input.argInfo?.reprType === 'Standard.Base.Bool' ? Score.Good : Score.Mismatch
|
||||
if (getRawBoolNode(props.input.ast) != null) return Score.Perfect
|
||||
return Score.Mismatch
|
||||
if (props.input.value instanceof Ast.Ast && getRawBoolNode(props.input.value) != null)
|
||||
return Score.Perfect
|
||||
return props.input.expectedType === 'Standard.Base.Data.Boolean.Boolean'
|
||||
? Score.Good
|
||||
: Score.Mismatch
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { injectFunctionInfo, provideFunctionInfo } from '@/providers/functionInfo'
|
||||
import type { PortId } from '@/providers/portInfo'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import {
|
||||
argsWidgetConfigurationSchema,
|
||||
functionCallConfiguration,
|
||||
@ -13,6 +13,7 @@ import { assert } from '@/util/assert'
|
||||
import { Ast } from '@/util/ast'
|
||||
import {
|
||||
ArgumentApplication,
|
||||
ArgumentApplicationKey,
|
||||
ArgumentAst,
|
||||
ArgumentPlaceholder,
|
||||
getAccessOprSubject,
|
||||
@ -28,25 +29,25 @@ const project = useProjectStore()
|
||||
|
||||
provideFunctionInfo(
|
||||
proxyRefs({
|
||||
callId: computed(() => props.input.ast.exprId),
|
||||
callId: computed(() => props.input.value.exprId),
|
||||
}),
|
||||
)
|
||||
|
||||
const methodCallInfo = computed(() => {
|
||||
return graph.db.getMethodCallInfo(props.input.ast.exprId)
|
||||
return graph.db.getMethodCallInfo(props.input.value.exprId)
|
||||
})
|
||||
|
||||
const interpreted = computed(() => {
|
||||
return interpretCall(props.input.ast, methodCallInfo.value == null)
|
||||
return interpretCall(props.input.value, methodCallInfo.value == null)
|
||||
})
|
||||
|
||||
const application = computed(() => {
|
||||
const call = interpreted.value
|
||||
if (!call) return props.input
|
||||
if (!call) return null
|
||||
const noArgsCall = call.kind === 'prefix' ? graph.db.getMethodCall(call.func.exprId) : undefined
|
||||
|
||||
const info = methodCallInfo.value
|
||||
const application = ArgumentApplication.FromInterpretedWithInfo(
|
||||
return ArgumentApplication.FromInterpretedWithInfo(
|
||||
call,
|
||||
{
|
||||
noArgsCall,
|
||||
@ -56,9 +57,14 @@ const application = computed(() => {
|
||||
},
|
||||
!info?.staticallyApplied,
|
||||
)
|
||||
return application instanceof ArgumentApplication
|
||||
? application
|
||||
: AnyWidget.Ast(application, props.input.dynamicConfig, props.input.argInfo)
|
||||
})
|
||||
|
||||
const innerInput = computed(() => {
|
||||
if (application.value instanceof ArgumentApplication) {
|
||||
return application.value.toWidgetInput()
|
||||
} else {
|
||||
return props.input
|
||||
}
|
||||
})
|
||||
|
||||
const escapeString = (str: string): string => {
|
||||
@ -68,7 +74,7 @@ const escapeString = (str: string): string => {
|
||||
const makeArgsList = (args: string[]) => '[' + args.map(escapeString).join(', ') + ']'
|
||||
|
||||
const selfArgumentAstId = computed<Opt<ExprId>>(() => {
|
||||
const analyzed = interpretCall(props.input.ast, true)
|
||||
const analyzed = interpretCall(props.input.value, true)
|
||||
if (analyzed.kind === 'infix') {
|
||||
return analyzed.lhs?.exprId
|
||||
} else {
|
||||
@ -86,7 +92,7 @@ const visualizationConfig = computed<Opt<NodeVisualizationConfiguration>>(() =>
|
||||
if (props.input.dynamicConfig) return null
|
||||
|
||||
const expressionId = selfArgumentAstId.value
|
||||
const astId = props.input.ast.exprId
|
||||
const astId = props.input.value.exprId
|
||||
if (astId == null || expressionId == null) return null
|
||||
const info = graph.db.getMethodCallInfo(astId)
|
||||
if (!info) return null
|
||||
@ -223,10 +229,12 @@ function handleArgUpdate(value: unknown, origin: PortId): boolean {
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(AnyWidget.matchFunctionCall, {
|
||||
export const widgetDefinition = defineWidget(WidgetInput.isFunctionCall, {
|
||||
priority: -10,
|
||||
score: (props, db) => {
|
||||
const ast = props.input.ast
|
||||
// If ArgumentApplicationKey is stored, we already are handled by some WidgetFunction.
|
||||
if (props.input[ArgumentApplicationKey]) return Score.Mismatch
|
||||
const ast = props.input.value
|
||||
if (ast.exprId == null) return Score.Mismatch
|
||||
const prevFunctionState = injectFunctionInfo(true)
|
||||
|
||||
@ -248,5 +256,5 @@ export const widgetDefinition = defineWidget(AnyWidget.matchFunctionCall, {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NodeWidget :input="application" @update="handleArgUpdate" />
|
||||
<NodeWidget :input="innerInput" @update="handleArgUpdate" />
|
||||
</template>
|
||||
|
@ -1,36 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { ForcePort } from '@/providers/portInfo'
|
||||
import { AnyWidget, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
|
||||
const spanClass = computed(() => props.input.ast.typeName())
|
||||
const children = computed(() => [...props.input.ast.children()])
|
||||
const spanClass = computed(() => props.input.value.typeName())
|
||||
const children = computed(() => [...props.input.value.children()])
|
||||
|
||||
function transformChild(child: Ast.Ast | Ast.Token) {
|
||||
if (child instanceof Ast.Token) return child
|
||||
const childInput = AnyWidget.Ast(child)
|
||||
if (props.input.ast instanceof Ast.PropertyAccess) {
|
||||
if (child === props.input.ast.lhs) {
|
||||
return new ForcePort(childInput)
|
||||
}
|
||||
} else if (props.input.ast instanceof Ast.OprApp) {
|
||||
if (child === props.input.ast.rhs || child === props.input.ast.lhs) {
|
||||
return new ForcePort(childInput)
|
||||
}
|
||||
} else if (props.input.ast instanceof Ast.UnaryOprApp && child === props.input.ast.argument) {
|
||||
return new ForcePort(childInput)
|
||||
}
|
||||
const childInput = WidgetInput.FromAst(child)
|
||||
if (props.input.value instanceof Ast.PropertyAccess && child === props.input.value.lhs)
|
||||
childInput.forcePort = true
|
||||
if (
|
||||
props.input.value instanceof Ast.OprApp &&
|
||||
(child === props.input.value.rhs || child === props.input.value.lhs)
|
||||
)
|
||||
childInput.forcePort = true
|
||||
if (props.input.value instanceof Ast.UnaryOprApp && child === props.input.value.argument)
|
||||
childInput.forcePort = true
|
||||
return childInput
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(AnyWidget.matchAst, {
|
||||
priority: 1001,
|
||||
export const widgetDefinition = defineWidget(WidgetInput.isAst, {
|
||||
priority: 2000,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import SliderWidget from '@/components/widgets/SliderWidget.vue'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
const value = computed({
|
||||
get() {
|
||||
const valueStr = props.input.ast?.code() ?? props.input.argInfo?.defaultValue ?? ''
|
||||
const valueStr = WidgetInput.valueRepr(props.input)
|
||||
return valueStr ? parseFloat(valueStr) : 0
|
||||
},
|
||||
set(value) {
|
||||
@ -17,19 +17,20 @@ const value = computed({
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(AnyWidget, {
|
||||
priority: 10,
|
||||
export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
|
||||
priority: 1001,
|
||||
score: (props) => {
|
||||
if (
|
||||
props.input.ast instanceof Ast.NumericLiteral ||
|
||||
(props.input.ast instanceof Ast.NegationOprApp &&
|
||||
props.input.ast.argument instanceof Ast.NumericLiteral)
|
||||
props.input.value instanceof Ast.NumericLiteral ||
|
||||
(props.input.value instanceof Ast.NegationOprApp &&
|
||||
props.input.value.argument instanceof Ast.NumericLiteral)
|
||||
)
|
||||
return Score.Perfect
|
||||
const type = props.input.expectedType
|
||||
if (
|
||||
props.input.argInfo?.reprType === 'Standard.Base.Data.Number' ||
|
||||
props.input.argInfo?.reprType === 'Standard.Base.Data.Numbers.Integer' ||
|
||||
props.input.argInfo?.reprType === 'Standard.Data.Numbers.Float'
|
||||
type === 'Standard.Base.Data.Number' ||
|
||||
type === 'Standard.Base.Data.Numbers.Integer' ||
|
||||
type === 'Standard.Data.Numbers.Float'
|
||||
)
|
||||
return Score.Perfect
|
||||
return Score.Mismatch
|
||||
|
@ -4,12 +4,12 @@ import { useRaf } from '@/composables/animation'
|
||||
import { useResizeObserver } from '@/composables/events'
|
||||
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||
import { injectGraphSelection } from '@/providers/graphSelection'
|
||||
import { ForcePort, injectPortInfo, providePortInfo, type PortId } from '@/providers/portInfo'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { injectPortInfo, providePortInfo, type PortId } from '@/providers/portInfo'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { injectWidgetTree } from '@/providers/widgetTree'
|
||||
import { PortViewInstance, useGraphStore } from '@/stores/graph'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { ArgumentInfoKey } from '@/util/callTree'
|
||||
import { Rect } from '@/util/data/rect'
|
||||
import { cachedGetter } from '@/util/reactivity'
|
||||
import { uuidv4 } from 'lib0/random'
|
||||
@ -69,12 +69,12 @@ const randomUuid = uuidv4() as PortId
|
||||
// its result in an intermediate ref, and update it only when the value actually changes. That way
|
||||
// effects depending on the port ID value will not be re-triggered unnecessarily.
|
||||
const portId = cachedGetter<PortId>(() => {
|
||||
return portIdOfInput(props.input) ?? (`RAND-${randomUuid}` as PortId)
|
||||
return props.input.portId
|
||||
})
|
||||
|
||||
const innerWidget = computed(() =>
|
||||
props.input instanceof ForcePort ? props.input.inner : props.input,
|
||||
)
|
||||
const innerWidget = computed(() => {
|
||||
return { ...props.input, forcePort: false }
|
||||
})
|
||||
|
||||
providePortInfo(proxyRefs({ portId, connected: hasConnection }))
|
||||
|
||||
@ -110,55 +110,37 @@ function updateRect() {
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
function portIdOfInput(input: unknown): PortId | undefined {
|
||||
return input instanceof AnyWidget
|
||||
? input.portId
|
||||
: input instanceof ForcePort && input.inner.ast != null
|
||||
? (input.inner.ast.exprId as string as PortId)
|
||||
: input instanceof ArgumentPlaceholder || input instanceof ArgumentAst
|
||||
? input.portId
|
||||
: undefined
|
||||
}
|
||||
|
||||
export const widgetDefinition = defineWidget(
|
||||
[ForcePort, AnyWidget.matchAst, ArgumentAst, ArgumentPlaceholder],
|
||||
{
|
||||
priority: 0,
|
||||
score: (props, _db) => {
|
||||
const portInfo = injectPortInfo(true)
|
||||
const ast =
|
||||
props.input instanceof ArgumentPlaceholder
|
||||
? undefined
|
||||
: props.input instanceof ForcePort
|
||||
? props.input.inner.ast
|
||||
: props.input.ast
|
||||
if (portInfo != null && portInfo.portId === ast?.exprId) {
|
||||
return Score.Mismatch
|
||||
}
|
||||
|
||||
if (
|
||||
props.input instanceof ForcePort ||
|
||||
props.input instanceof ArgumentAst ||
|
||||
props.input instanceof ArgumentPlaceholder
|
||||
)
|
||||
return Score.Perfect
|
||||
|
||||
if (
|
||||
props.input.ast instanceof Ast.Invalid ||
|
||||
props.input.ast instanceof Ast.BodyBlock ||
|
||||
props.input.ast instanceof Ast.Group ||
|
||||
props.input.ast instanceof Ast.NumericLiteral ||
|
||||
props.input.ast instanceof Ast.OprApp ||
|
||||
props.input.ast instanceof Ast.UnaryOprApp ||
|
||||
props.input.ast instanceof Ast.Wildcard ||
|
||||
props.input.ast instanceof Ast.TextLiteral
|
||||
)
|
||||
return Score.Perfect
|
||||
|
||||
export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
|
||||
priority: 0,
|
||||
score: (props, _db) => {
|
||||
const portInfo = injectPortInfo(true)
|
||||
const value = props.input.value
|
||||
if (portInfo != null && value instanceof Ast.Ast && portInfo.portId === value.exprId) {
|
||||
return Score.Mismatch
|
||||
},
|
||||
}
|
||||
|
||||
if (
|
||||
props.input.forcePort ||
|
||||
WidgetInput.isPlaceholder(props.input) ||
|
||||
props.input[ArgumentInfoKey] != undefined
|
||||
)
|
||||
return Score.Perfect
|
||||
|
||||
if (
|
||||
props.input.value instanceof Ast.Invalid ||
|
||||
props.input.value instanceof Ast.BodyBlock ||
|
||||
props.input.value instanceof Ast.Group ||
|
||||
props.input.value instanceof Ast.NumericLiteral ||
|
||||
props.input.value instanceof Ast.OprApp ||
|
||||
props.input.value instanceof Ast.UnaryOprApp ||
|
||||
props.input.value instanceof Ast.Wildcard ||
|
||||
props.input.value instanceof Ast.TextLiteral
|
||||
)
|
||||
return Score.Perfect
|
||||
|
||||
return Score.Mismatch
|
||||
},
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import DropdownWidget from '@/components/widgets/DropdownWidget.vue'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import {
|
||||
functionCallConfiguration,
|
||||
type ArgumentWidgetConfiguration,
|
||||
} from '@/providers/widgetRegistry/configuration'
|
||||
import { ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { ArgumentInfoKey } from '@/util/callTree'
|
||||
import { qnJoin, qnSegments, tryQualifiedName } from '@/util/qualifiedName'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
@ -20,7 +20,7 @@ interface Tag {
|
||||
}
|
||||
|
||||
const staticTags = computed<Tag[]>(() => {
|
||||
const tags = props.input.argInfo?.tagValues
|
||||
const tags = props.input[ArgumentInfoKey]?.info?.tagValues
|
||||
if (tags == null) return []
|
||||
return tags.map((tag) => {
|
||||
const qualifiedName = tryQualifiedName(tag)
|
||||
@ -50,7 +50,7 @@ const selectedTag = computed(() =>
|
||||
selectedIndex.value != null ? tags.value[selectedIndex.value] : undefined,
|
||||
)
|
||||
const selectedValue = computed(() => {
|
||||
if (selectedTag.value == null) return props.input.argInfo?.defaultValue ?? ''
|
||||
if (selectedTag.value == null) return WidgetInput.valueRepr(props.input)
|
||||
return selectedTag.value.value ?? selectedTag.value.label
|
||||
})
|
||||
const innerWidgetInput = computed(() => {
|
||||
@ -58,25 +58,7 @@ const innerWidgetInput = computed(() => {
|
||||
const parameters = selectedTag.value.parameters
|
||||
if (!parameters) return props.input
|
||||
const config = functionCallConfiguration(parameters)
|
||||
if (props.input instanceof AnyWidget)
|
||||
return new AnyWidget(props.input.portId, props.input.ast, config, props.input.argInfo)
|
||||
else if (props.input instanceof ArgumentAst)
|
||||
return new ArgumentAst(
|
||||
props.input.ast,
|
||||
props.input.index,
|
||||
props.input.argInfo,
|
||||
props.input.kind,
|
||||
config,
|
||||
)
|
||||
else
|
||||
return new ArgumentPlaceholder(
|
||||
props.input.callId,
|
||||
props.input.index,
|
||||
props.input.argInfo,
|
||||
props.input.kind,
|
||||
props.input.insertAsNamed,
|
||||
config,
|
||||
)
|
||||
return { ...props.input, dynamicConfig: config }
|
||||
})
|
||||
const showDropdownWidget = ref(false)
|
||||
|
||||
@ -92,11 +74,11 @@ watch(selectedIndex, (_index) => {
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget([AnyWidget, ArgumentAst, ArgumentPlaceholder], {
|
||||
export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
|
||||
priority: 999,
|
||||
score: (props) => {
|
||||
if (props.input.dynamicConfig?.kind === 'Single_Choice') return Score.Perfect
|
||||
if (props.input.argInfo?.tagValues != null) return Score.Perfect
|
||||
if (props.input[ArgumentInfoKey]?.info?.tagValues != null) return Score.Perfect
|
||||
return Score.Mismatch
|
||||
},
|
||||
})
|
||||
|
@ -1,17 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
|
||||
const spanClass = computed(() => props.input.typeName())
|
||||
const repr = computed(() => props.input.code())
|
||||
const spanClass = computed(() => props.input.value.typeName())
|
||||
const repr = computed(() => props.input.value.code())
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget((expression) => expression instanceof Ast.Token, {
|
||||
priority: 1000,
|
||||
export const widgetDefinition = defineWidget(WidgetInput.isToken, {
|
||||
priority: 0,
|
||||
score: Score.Good,
|
||||
})
|
||||
</script>
|
||||
|
@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { defineWidget, Score, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { ApplicationKind, ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { ApplicationKind, ArgumentInfoKey } from '@/util/callTree'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget([ArgumentAst, ArgumentPlaceholder], {
|
||||
export const widgetDefinition = defineWidget(ArgumentInfoKey, {
|
||||
priority: -1,
|
||||
score: (props) =>
|
||||
props.nesting < 2 && props.input.kind === ApplicationKind.Prefix
|
||||
props.nesting < 2 && props.input[ArgumentInfoKey].appKind === ApplicationKind.Prefix
|
||||
? Score.Perfect
|
||||
: Score.Mismatch,
|
||||
})
|
||||
@ -18,7 +18,7 @@ export const widgetDefinition = defineWidget([ArgumentAst, ArgumentPlaceholder],
|
||||
|
||||
<template>
|
||||
<span class="WidgetTopLevelArgument">
|
||||
<NodeWidget :input="props.input" nest />
|
||||
<NodeWidget :input="props.input" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
@ -2,8 +2,7 @@
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import ListWidget from '@/components/widgets/ListWidget.vue'
|
||||
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||
import { ForcePort } from '@/providers/portInfo'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast, RawAst } from '@/util/ast'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@ -25,12 +24,13 @@ const defaultItem = computed(() => {
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
if (props.input.ast == null) return []
|
||||
return Array.from(props.input.ast.children()).filter(
|
||||
if (!(props.input.value instanceof Ast.Ast)) return []
|
||||
return Array.from(props.input.value.children()).filter(
|
||||
(child): child is Ast.Ast => child instanceof Ast.Ast,
|
||||
)
|
||||
},
|
||||
set(value) {
|
||||
// TODO[ao]: here we re-create AST. It would be better to reuse existing AST nodes.
|
||||
const newCode = `[${value.map((item) => item.code()).join(', ')}]`
|
||||
props.onUpdate(newCode, props.input.portId)
|
||||
},
|
||||
@ -40,14 +40,15 @@ const navigator = injectGraphNavigator(true)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(AnyWidget, {
|
||||
priority: 1000,
|
||||
export const widgetDefinition = defineWidget(WidgetInput.isAstOrPlaceholder, {
|
||||
priority: 1001,
|
||||
score: (props) => {
|
||||
if (props.input.dynamicConfig?.kind === 'Vector_Editor') return Score.Perfect
|
||||
else if (props.input.argInfo?.reprType.startsWith('Standard.Base.Data.Vector.Vector'))
|
||||
else if (props.input.expectedType?.startsWith('Standard.Base.Data.Vector.Vector'))
|
||||
return Score.Good
|
||||
else
|
||||
return props.input.ast?.treeType === RawAst.Tree.Type.Array ? Score.Perfect : Score.Mismatch
|
||||
else if (props.input.value instanceof Ast.Ast)
|
||||
return props.input.value.treeType === RawAst.Tree.Type.Array ? Score.Perfect : Score.Mismatch
|
||||
else return Score.Mismatch
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -66,7 +67,10 @@ export const widgetDefinition = defineWidget(AnyWidget, {
|
||||
contenteditable="false"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<NodeWidget :input="new ForcePort(AnyWidget.Ast(item, itemConfig))" />
|
||||
<NodeWidget
|
||||
:input="{ ...WidgetInput.FromAst(item), dynamicConfig: itemConfig, forcePort: true }"
|
||||
nest
|
||||
/>
|
||||
</template>
|
||||
</ListWidget>
|
||||
</template>
|
||||
|
@ -1,17 +1,17 @@
|
||||
import {
|
||||
AnyWidget,
|
||||
Score,
|
||||
WidgetInput,
|
||||
WidgetRegistry,
|
||||
defineWidget,
|
||||
type WidgetDefinition,
|
||||
type WidgetInput,
|
||||
type WidgetModule,
|
||||
} from '@/providers/widgetRegistry'
|
||||
import { GraphDb } from '@/stores/graph/graphDatabase'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { ApplicationKind, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { ApplicationKind, ArgumentInfoKey } from '@/util/callTree'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import type { PortId } from '../portInfo'
|
||||
import { DisplayMode, argsWidgetConfigurationSchema } from '../widgetRegistry/configuration'
|
||||
|
||||
describe('WidgetRegistry', () => {
|
||||
@ -27,15 +27,15 @@ describe('WidgetRegistry', () => {
|
||||
|
||||
const widgetA = makeMockWidget(
|
||||
'A',
|
||||
defineWidget(AnyWidget, {
|
||||
defineWidget(WidgetInput.isAstOrPlaceholder, {
|
||||
priority: 1,
|
||||
}),
|
||||
)
|
||||
|
||||
const widgetB = makeMockWidget(
|
||||
'B',
|
||||
defineWidget(ArgumentPlaceholder, {
|
||||
priority: 2,
|
||||
defineWidget(ArgumentInfoKey, {
|
||||
priority: 0,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -49,26 +49,28 @@ describe('WidgetRegistry', () => {
|
||||
|
||||
const widgetD = makeMockWidget(
|
||||
'D',
|
||||
defineWidget(AnyWidget, {
|
||||
defineWidget(WidgetInput.isAstOrPlaceholder, {
|
||||
priority: 20,
|
||||
score: (props) => (props.input.ast?.code() === '_' ? Score.Perfect : Score.Mismatch),
|
||||
score: (props) =>
|
||||
WidgetInput.valueRepr(props.input) === '_' ? Score.Perfect : Score.Mismatch,
|
||||
}),
|
||||
)
|
||||
|
||||
const someAst = AnyWidget.Ast(Ast.parse('foo'))
|
||||
const blankAst = AnyWidget.Ast(Ast.parse('_'))
|
||||
const somePlaceholder = new ArgumentPlaceholder(
|
||||
'57d429dc-df85-49f8-b150-567c7d1fb502',
|
||||
0,
|
||||
{
|
||||
name: 'foo',
|
||||
reprType: 'Any',
|
||||
isSuspended: false,
|
||||
hasDefault: false,
|
||||
const someAst = WidgetInput.FromAst(Ast.parse('foo'))
|
||||
const blankAst = WidgetInput.FromAst(Ast.parse('_'))
|
||||
const someArgPlaceholder: WidgetInput = {
|
||||
portId: '57d429dc-df85-49f8-b150-567c7d1fb502' as PortId,
|
||||
value: 'bar',
|
||||
[ArgumentInfoKey]: {
|
||||
info: {
|
||||
name: 'foo',
|
||||
reprType: 'Any',
|
||||
isSuspended: false,
|
||||
hasDefault: false,
|
||||
},
|
||||
appKind: ApplicationKind.Prefix,
|
||||
},
|
||||
ApplicationKind.Prefix,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
const mockGraphDb = GraphDb.Mock()
|
||||
const registry = new WidgetRegistry(mockGraphDb)
|
||||
@ -79,7 +81,7 @@ describe('WidgetRegistry', () => {
|
||||
|
||||
test('selects a widget based on the input type', () => {
|
||||
const forAst = registry.select({ input: someAst, nesting: 0 })
|
||||
const forArg = registry.select({ input: somePlaceholder, nesting: 0 })
|
||||
const forArg = registry.select({ input: someArgPlaceholder, nesting: 0 })
|
||||
expect(forAst).toStrictEqual(widgetA)
|
||||
expect(forArg).toStrictEqual(widgetB)
|
||||
})
|
||||
@ -87,8 +89,8 @@ describe('WidgetRegistry', () => {
|
||||
test('selects a widget outside of the excluded set', () => {
|
||||
const forAst = registry.select({ input: someAst, nesting: 0 }, new Set([widgetA.default]))
|
||||
const forArg = registry.select(
|
||||
{ input: somePlaceholder, nesting: 0 },
|
||||
new Set([widgetB.default]),
|
||||
{ input: someArgPlaceholder, nesting: 0 },
|
||||
new Set([widgetA.default, widgetB.default]),
|
||||
)
|
||||
expect(forAst).toStrictEqual(widgetC)
|
||||
expect(forArg).toStrictEqual(widgetC)
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { createContextStore } from '@/providers'
|
||||
import type { AnyWidget } from '@/providers/widgetRegistry'
|
||||
import { GetUsageKey } from '@/providers/widgetUsageInfo'
|
||||
import { identity } from '@vueuse/core'
|
||||
import type { ExprId } from 'shared/yjsModel'
|
||||
|
||||
declare const portIdBrand: unique symbol
|
||||
/**
|
||||
* Port identification. A port represents a fragment of code displayed/modified by the widget;
|
||||
* usually Ast nodes, but other ids are also possible (like argument placeholders).
|
||||
*/
|
||||
export type PortId = ExprId | (string & { [portIdBrand]: never })
|
||||
|
||||
interface PortInfo {
|
||||
@ -14,23 +16,3 @@ interface PortInfo {
|
||||
|
||||
export { injectFn as injectPortInfo, provideFn as providePortInfo }
|
||||
const { provideFn, injectFn } = createContextStore('Port info', identity<PortInfo>)
|
||||
|
||||
/**
|
||||
* Widget input type that can be used to force a specific AST to be rendered as a port widget,
|
||||
* even if it wouldn't normally be rendered as such.
|
||||
*/
|
||||
export class ForcePort {
|
||||
constructor(public inner: AnyWidget) {
|
||||
if (inner instanceof ForcePort) throw new Error('ForcePort cannot be nested')
|
||||
}
|
||||
[GetUsageKey]() {
|
||||
return this.inner
|
||||
}
|
||||
}
|
||||
|
||||
declare const ForcePortKey: unique symbol
|
||||
declare module '@/providers/widgetRegistry' {
|
||||
export interface WidgetInputTypes {
|
||||
[ForcePortKey]: ForcePort
|
||||
}
|
||||
}
|
||||
|
@ -2,105 +2,108 @@ import { createContextStore } from '@/providers'
|
||||
import type { PortId } from '@/providers/portInfo'
|
||||
import type { WidgetConfiguration } from '@/providers/widgetRegistry/configuration'
|
||||
import type { GraphDb } from '@/stores/graph/graphDatabase'
|
||||
import type { SuggestionEntryArgument } from '@/stores/suggestionDatabase/entry'
|
||||
import type { Typename } from '@/stores/suggestionDatabase/entry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { computed, shallowReactive, type Component, type PropType } from 'vue'
|
||||
|
||||
export type WidgetComponent<T extends WidgetInput> = Component<WidgetProps<T>>
|
||||
|
||||
/**
|
||||
* A WidgetInput variant meant to match wide range of "general" widgets.
|
||||
*
|
||||
* Any widget which wants to work in different contexts (inside function calls, constructors, list
|
||||
* elements) should match with this, using provided information.
|
||||
*
|
||||
* When your widget want to display some non-specific subwidget (like WidgetVector which displays
|
||||
* elements of any type), this should be provided as their input, passing as much information as
|
||||
* possible.
|
||||
*/
|
||||
export class AnyWidget {
|
||||
constructor(
|
||||
/** A port id to refer when updating changes */
|
||||
public portId: PortId,
|
||||
/**
|
||||
* Ast represented by widget. May be not defined if widget is a placeholder for
|
||||
* not-yet-written argument
|
||||
*/
|
||||
public ast: Ast.Ast | undefined,
|
||||
/** Configuration retrieved from the backend */
|
||||
public dynamicConfig?: WidgetConfiguration | undefined,
|
||||
/** The information about argument of some function call, which this widget is setting (if any) */
|
||||
public argInfo?: SuggestionEntryArgument | undefined,
|
||||
) {}
|
||||
|
||||
static Ast(
|
||||
ast: Ast.Ast,
|
||||
dynamicConfig?: WidgetConfiguration | undefined,
|
||||
argInfo?: SuggestionEntryArgument | undefined,
|
||||
) {
|
||||
return new AnyWidget(ast.exprId, ast, dynamicConfig, argInfo)
|
||||
export namespace WidgetInput {
|
||||
export function FromAst(ast: Ast.Ast | Ast.Token): WidgetInput {
|
||||
return {
|
||||
portId: ast.exprId,
|
||||
value: ast,
|
||||
}
|
||||
}
|
||||
|
||||
isPlaceholder() {
|
||||
return this.ast == null
|
||||
export function valueRepr(input: WidgetInput): string | undefined {
|
||||
if (typeof input.value === 'string') return input.value
|
||||
else return input.value?.code()
|
||||
}
|
||||
|
||||
static matchPlaceholder(input: WidgetInput): input is AnyWidget & { ast: undefined } {
|
||||
return input instanceof AnyWidget && input.isPlaceholder()
|
||||
}
|
||||
|
||||
static matchAst(input: WidgetInput): input is AnyWidget & { ast: Ast.Ast } {
|
||||
return input instanceof AnyWidget && !input.isPlaceholder()
|
||||
}
|
||||
|
||||
static matchFunctionCall(
|
||||
/** Check if input is placeholder, i.e. it does not represent any Ast node. */
|
||||
export function isPlaceholder(
|
||||
input: WidgetInput,
|
||||
): input is AnyWidget & { ast: Ast.App | Ast.Ident | Ast.OprApp } {
|
||||
): input is WidgetInput & { value: string | undefined } {
|
||||
return input.value == null || typeof input.value === 'string'
|
||||
}
|
||||
|
||||
/** Match input against a specific AST node type. */
|
||||
export function astMatcher<T extends Ast.Ast>(nodeType: new (...args: any[]) => T) {
|
||||
return (input: WidgetInput): input is WidgetInput & { value: T } =>
|
||||
input.value instanceof nodeType
|
||||
}
|
||||
|
||||
export function isAst(input: WidgetInput): input is WidgetInput & { value: Ast.Ast } {
|
||||
return input.value instanceof Ast.Ast
|
||||
}
|
||||
|
||||
/** Rule out token inputs. */
|
||||
export function isAstOrPlaceholder(
|
||||
input: WidgetInput,
|
||||
): input is WidgetInput & { value: Ast.Ast | string | undefined } {
|
||||
return isPlaceholder(input) || isAst(input)
|
||||
}
|
||||
|
||||
export function isToken(input: WidgetInput): input is WidgetInput & { value: Ast.Token } {
|
||||
return input.value instanceof Ast.Token
|
||||
}
|
||||
|
||||
export function isFunctionCall(
|
||||
input: WidgetInput,
|
||||
): input is WidgetInput & { value: Ast.App | Ast.Ident | Ast.OprApp } {
|
||||
return (
|
||||
input instanceof AnyWidget &&
|
||||
(input.ast instanceof Ast.App ||
|
||||
input.ast instanceof Ast.Ident ||
|
||||
input.ast instanceof Ast.OprApp)
|
||||
input.value instanceof Ast.App ||
|
||||
input.value instanceof Ast.Ident ||
|
||||
input.value instanceof Ast.OprApp
|
||||
)
|
||||
}
|
||||
}
|
||||
declare const AnyWidgetKey: unique symbol
|
||||
|
||||
/**
|
||||
* A type representing any kind of input that can have a widget attached to it. It is defined as an
|
||||
* interface to allow for extension by widgets themselves. The actual input received by the widget
|
||||
* will always be one of the types defined in this interface. The key of each property is used as a
|
||||
* key for the discriminated union, so it should be unique.
|
||||
* Widget instance input.
|
||||
*
|
||||
* Declare new widget input types by declaring a new unique symbol key, then extending this
|
||||
* interface with a new property with that key and a value of the new input type.
|
||||
* This input is first used to decide which widget should be instantiated, then by the widget
|
||||
* instance to show proper value.
|
||||
*
|
||||
* This interface can be extended by specific widgets to add their data to be propagated to
|
||||
* subwidgets - to avoid breaking other widgets, additional property should be optional indexed
|
||||
* by symbols, for example:
|
||||
*
|
||||
* ```ts
|
||||
* declare const MyCustomInputTypeKey: unique symbol;
|
||||
* export const ArgumentApplicationKey: unique symbol = Symbol('ArgumentApplicationKey')
|
||||
* export const ArgumentInfoKey: unique symbol = Symbol('ArgumentInfoKey')
|
||||
* declare module '@/providers/widgetRegistry' {
|
||||
* export interface WidgetInputTypes {
|
||||
* [MyCustomInputTypeKey]: MyCustomInputType
|
||||
* export interface WidgetInput {
|
||||
* [ArgumentApplicationKey]?: ArgumentApplication
|
||||
* [ArgumentInfoKey]?: {
|
||||
* appKind: ApplicationKind
|
||||
* info: SuggestionEntryArgument | undefined
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* All declared widget input types must have unique symbols, and all values must be objects.
|
||||
* Declarations that do not follow these rules will be ignored or will cause type errors.
|
||||
*/
|
||||
export interface WidgetInputTypes {
|
||||
[AnyWidgetKey]: AnyWidget
|
||||
export interface WidgetInput {
|
||||
/**
|
||||
* Port identification. See {@link PortId}
|
||||
*
|
||||
* Also, used as usage key (see {@link usageKeyForInput})
|
||||
*/
|
||||
portId: PortId
|
||||
/**
|
||||
* An expected widget value. If Ast.Ast or Ast.Token, the widget represents an existing part of
|
||||
* code. If string, it may be e.g. a default value of an argument.
|
||||
*/
|
||||
value: Ast.Ast | Ast.Token | string | undefined
|
||||
/** An expected type which widget should set. */
|
||||
expectedType?: Typename | undefined
|
||||
/** Configuration provided by engine. */
|
||||
dynamicConfig?: WidgetConfiguration | undefined
|
||||
/** Force the widget to be a connectible port. */
|
||||
forcePort?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* An union of all possible widget input types. A collection of all correctly declared value types
|
||||
* on the extendable {@link WidgetInputTypes} interface.
|
||||
*
|
||||
* From the defined input types, it accepts only keys that are declared as symbols and have values
|
||||
* that are objects. If your extension of {@link WidgetInputTypes} does not appear in this union,
|
||||
* check if you have followed those rules.
|
||||
*/
|
||||
export type WidgetInput = Extract<WidgetInputTypes[keyof WidgetInputTypes & symbol], object>
|
||||
|
||||
/**
|
||||
* Description of how well a widget matches given input. Used to determine which widget should be
|
||||
* used, or whether the applied widget override is valid in given context.
|
||||
@ -145,20 +148,16 @@ export function widgetProps<T extends WidgetInput>(_def: WidgetDefinition<T>) {
|
||||
} as const
|
||||
}
|
||||
|
||||
/**
|
||||
* A class which instances have type `T`. Accepts classes that have a private constructors, such as
|
||||
* `Ast` or `ArgumentPlaceholder`.
|
||||
*/
|
||||
type Class<T extends WidgetInput> = Function & { prototype: T }
|
||||
type InputMatcherFn<T extends WidgetInput> = (input: WidgetInput) => input is T
|
||||
type InputMatcher<T extends WidgetInput> = InputMatcherFn<T> | Class<T>
|
||||
type InputMatcherSymbol<T extends WidgetInput> = symbol & keyof T
|
||||
type InputMatcher<T extends WidgetInput> = InputMatcherSymbol<T> | InputMatcherFn<T>
|
||||
|
||||
type InputTy<M> = M extends (infer T)[]
|
||||
? InputTy<T>
|
||||
: M extends InputMatcherFn<infer T>
|
||||
? T
|
||||
: M extends Class<infer T>
|
||||
? T
|
||||
: M extends symbol & keyof WidgetInput
|
||||
? WidgetInput & { [S in M]: Required<WidgetInput>[S] }
|
||||
: never
|
||||
|
||||
export interface WidgetOptions<T extends WidgetInput> {
|
||||
@ -230,12 +229,12 @@ function isWidgetDefinition(config: unknown): config is WidgetDefinition<any> {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param matchInputs Filter the widget input type to only accept specific types of input. The
|
||||
* @param matchInputs Filter the widget input to only accept specific types of input. The
|
||||
* declared widget props will depend on the type of input accepted by this filter. Only widgets that
|
||||
* pass this filter will be scored and potentially used.
|
||||
*
|
||||
* The filter can be a type guard function `(input: WidgetInput) => input is MyType`, a class
|
||||
* constructor `MyType`, or an array combining any of the above. When an array is provided, the
|
||||
* The filter can be a type guard function `(input: WidgetInput) => input is MyType`, a symbol representing
|
||||
* required property `MySymbol`, or an array combining any of the above. When an array is provided, the
|
||||
* widget will match if any of the filters in the array match.
|
||||
*/
|
||||
export function defineWidget<M extends InputMatcher<any> | InputMatcher<any>[]>(
|
||||
@ -264,12 +263,11 @@ function makeInputMatcher<T extends WidgetInput>(
|
||||
if (Array.isArray(matcher)) {
|
||||
const matchers = matcher.map(makeInputMatcher)
|
||||
return (input: WidgetInput): input is T => matchers.some((f) => f(input))
|
||||
} else if (Object.getOwnPropertyDescriptor(matcher, 'prototype')?.writable === false) {
|
||||
// A function with an existing non-writable prototype is a class constructor.
|
||||
return (input: WidgetInput): input is T => input instanceof matcher
|
||||
} else if (typeof matcher === 'function') {
|
||||
// When matcher is a function with assignable prototype, it must be a type guard.
|
||||
return matcher as (input: WidgetInput) => input is T
|
||||
} else if (typeof matcher === 'symbol') {
|
||||
return (input: WidgetInput): input is T => matcher in input
|
||||
} else {
|
||||
throw new Error('Invalid widget input matcher definiton: ' + matcher)
|
||||
}
|
||||
|
@ -16,8 +16,7 @@ interface WidgetUsageInfo {
|
||||
* An object which is used to distinguish between distinct nodes in a widget tree. When selecting
|
||||
* a widget type for an input value with the same `usageKey` as in parent widget, the widget types
|
||||
* that were previously used for this input value are not considered for selection. The key is
|
||||
* determined by the widget input's method defined on {@link GetUsageKey} symbol key. When no such
|
||||
* method is defined, the input value itself is used as the key.
|
||||
* determined by {@link usageKeyForInput} method - currently it's just the widget's port Id.
|
||||
*/
|
||||
usageKey: unknown
|
||||
/** All widget types that were rendered so far using the same AST node. */
|
||||
@ -27,17 +26,8 @@ interface WidgetUsageInfo {
|
||||
}
|
||||
|
||||
/**
|
||||
* A symbol key used for defining a widget input method's usage key. A method with this key can be
|
||||
* declared for widget input types that are not unique by themselves, but are just a thin wrapper
|
||||
* around another input value, and don't want to be considered as a completely separate entity for
|
||||
* the purposes of widget type selection.
|
||||
* Get usage key for given input. See {@link WidgetUsageInfo} for details.
|
||||
*/
|
||||
export const GetUsageKey = Symbol('GetUsageKey')
|
||||
|
||||
export function usageKeyForInput(widget: WidgetInput): unknown {
|
||||
if (GetUsageKey in widget && typeof widget[GetUsageKey] === 'function') {
|
||||
return widget[GetUsageKey]()
|
||||
} else {
|
||||
return widget
|
||||
}
|
||||
return widget.portId
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { PortId } from '@/providers/portInfo'
|
||||
import { AnyWidget, type WidgetInput } from '@/providers/widgetRegistry'
|
||||
import { WidgetInput } from '@/providers/widgetRegistry'
|
||||
import type { WidgetConfiguration } from '@/providers/widgetRegistry/configuration'
|
||||
import * as widgetCfg from '@/providers/widgetRegistry/configuration'
|
||||
import type { SuggestionEntry, SuggestionEntryArgument } from '@/stores/suggestionDatabase/entry'
|
||||
@ -39,8 +39,14 @@ export class ArgumentPlaceholder {
|
||||
return new ArgumentPlaceholder(callId, index, info, kind, insertAsNamed, cfg)
|
||||
}
|
||||
|
||||
toAnyWidget(): AnyWidget {
|
||||
return new AnyWidget(this.portId, undefined, this.dynamicConfig, this.argInfo)
|
||||
toWidgetInput(): WidgetInput {
|
||||
return {
|
||||
portId: this.portId,
|
||||
value: this.argInfo.defaultValue,
|
||||
expectedType: this.argInfo.reprType,
|
||||
[ArgumentInfoKey]: { info: this.argInfo, appKind: this.kind },
|
||||
dynamicConfig: this.dynamicConfig,
|
||||
}
|
||||
}
|
||||
|
||||
get portId(): PortId {
|
||||
@ -69,19 +75,19 @@ export class ArgumentAst {
|
||||
return new ArgumentAst(ast, index, info, kind, cfg)
|
||||
}
|
||||
|
||||
toAnyWidget(): AnyWidget {
|
||||
return new AnyWidget(this.portId, this.ast, this.dynamicConfig, this.argInfo)
|
||||
toWidgetInput(): WidgetInput {
|
||||
return {
|
||||
portId: this.portId,
|
||||
value: this.ast,
|
||||
expectedType: this.argInfo?.reprType,
|
||||
[ArgumentInfoKey]: { appKind: this.kind, info: this.argInfo },
|
||||
dynamicConfig: this.dynamicConfig,
|
||||
}
|
||||
}
|
||||
|
||||
get portId(): PortId {
|
||||
return this.ast.exprId
|
||||
}
|
||||
|
||||
static matchWithArgInfo(
|
||||
input: WidgetInput,
|
||||
): input is ArgumentAst & { argInfo: SuggestionEntryArgument } {
|
||||
return input instanceof ArgumentAst && input.argInfo != null
|
||||
}
|
||||
}
|
||||
|
||||
type InterpretedCall = InterpretedInfix | InterpretedPrefix
|
||||
@ -301,6 +307,17 @@ export class ArgumentApplication {
|
||||
current = current.target
|
||||
}
|
||||
}
|
||||
|
||||
toWidgetInput(): WidgetInput {
|
||||
return {
|
||||
portId:
|
||||
this.argument instanceof ArgumentAst
|
||||
? this.appTree.exprId
|
||||
: (`app:${this.argument.portId}` as PortId),
|
||||
value: this.appTree,
|
||||
[ArgumentApplicationKey]: this,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unknownArgInfoNamed = (name: string) => ({
|
||||
@ -318,13 +335,14 @@ function isAccessOperator(opr: Ast.Token | undefined): boolean {
|
||||
return opr != null && opr.code() === '.'
|
||||
}
|
||||
|
||||
declare const ArgumentApplicationKey: unique symbol
|
||||
declare const ArgumentAstKey: unique symbol
|
||||
declare const ArgumentPlaceholderKey: unique symbol
|
||||
export const ArgumentApplicationKey: unique symbol = Symbol('ArgumentApplicationKey')
|
||||
export const ArgumentInfoKey: unique symbol = Symbol('ArgumentInfoKey')
|
||||
declare module '@/providers/widgetRegistry' {
|
||||
export interface WidgetInputTypes {
|
||||
[ArgumentApplicationKey]: ArgumentApplication
|
||||
[ArgumentPlaceholderKey]: ArgumentPlaceholder
|
||||
[ArgumentAstKey]: ArgumentAst
|
||||
export interface WidgetInput {
|
||||
[ArgumentApplicationKey]?: ArgumentApplication
|
||||
[ArgumentInfoKey]?: {
|
||||
appKind: ApplicationKind
|
||||
info: SuggestionEntryArgument | undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user