mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Refactor widgets to handle dynamic config in input (#8624)
* Merged ArgumentAst and ArgumentPlaceholder into single class. * Created `AnyWidget` input being a "general use" widget input. Most wigets try to match with it; the `Argument` input is now solely for WidgetArgument(Name) or those handling arguments in a specific way (like selector which want to show on arg name click). * dynamic config is now part of widget input, and is properly propagated through vector editor/function widgets. # Important Notes The widgets still does not work perfectly: * The chosen options often don't have argument placeholders - that's because we don't display them for constructors. Needs to be added on our side, or engine should send us methodCall info for constructors. * There are issues with engine's messages sent to us. This makes widgets does not set up (so there is no drop-down, or vector adds `_` instead of default). I'm investigating them and going to fill issues.
This commit is contained in:
parent
cfab344fbe
commit
2e7d71d459
@ -7,7 +7,7 @@ export interface SuggestionEntryArgument {
|
||||
/** The argument name. */
|
||||
name: string
|
||||
/** The argument type. String 'Any' is used to specify generic types. */
|
||||
type: string
|
||||
reprType: string
|
||||
/** Indicates whether the argument is lazy. */
|
||||
isSuspended: boolean
|
||||
/** Indicates whether the argument has default value. */
|
||||
|
@ -9,7 +9,7 @@ import { computed, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
config: ApplicationConfig
|
||||
accessToken: string
|
||||
accessToken: string | null
|
||||
metadata: object
|
||||
unrecognizedOptions: string[]
|
||||
}>()
|
||||
|
@ -30,10 +30,11 @@ import { Rect } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
import * as set from 'lib0/set'
|
||||
import type { ExprId, NodeMetadata } from 'shared/yjsModel'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { toast } from 'vue3-toastify'
|
||||
import { computed, onMounted, onScopeDispose, ref, watch } from 'vue'
|
||||
import { toast, type Id as ToastId } from 'vue3-toastify'
|
||||
import { type Usage } from './ComponentBrowser/input'
|
||||
|
||||
const STARTUP_TOAST_DELAY_MS = 100
|
||||
const EXECUTION_MODES = ['design', 'live']
|
||||
// Assumed size of a newly created node. This is used to place the component browser.
|
||||
const DEFAULT_NODE_SIZE = new Vec2(0, 24)
|
||||
@ -52,19 +53,13 @@ const suggestionDb = useSuggestionDbStore()
|
||||
const interaction = provideInteractionHandler()
|
||||
|
||||
function initStartupToast() {
|
||||
const startupToast = toast.info('Initializing the project. This can take up to one minute.', {
|
||||
let startupToast = toast.info('Initializing the project. This can take up to one minute.', {
|
||||
autoClose: false,
|
||||
})
|
||||
projectStore.firstExecution.then(() => {
|
||||
if (startupToast != null) {
|
||||
toast.remove(startupToast)
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
if (startupToast != null) {
|
||||
toast.remove(startupToast)
|
||||
}
|
||||
})
|
||||
|
||||
const removeToast = () => toast.remove(startupToast)
|
||||
projectStore.firstExecution.then(removeToast)
|
||||
onScopeDispose(removeToast)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@ -548,14 +543,6 @@ function handleEdgeDrop(source: ExprId, position: Vec2) {
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleFileDrop($event)"
|
||||
>
|
||||
<ToastContainer
|
||||
position="top-center"
|
||||
theme="light"
|
||||
closeOnClick="false"
|
||||
draggable="false"
|
||||
toastClassName="text-sm leading-170 bg-frame-selected rounded-2xl backdrop-blur-3xl"
|
||||
transition="Vue-Toastification__bounce"
|
||||
/>
|
||||
<div :style="{ transform: graphNavigator.transform }" class="htmlLayer">
|
||||
<GraphNodes
|
||||
@nodeOutputPortDoubleClick="handleNodeOutputPortDoubleClick"
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { PortId } from '@/providers/portInfo'
|
||||
import { injectWidgetRegistry, type WidgetInput } from '@/providers/widgetRegistry'
|
||||
import type { WidgetConfiguration } from '@/providers/widgetRegistry/configuration'
|
||||
import { injectWidgetTree } from '@/providers/widgetTree'
|
||||
import {
|
||||
injectWidgetUsageInfo,
|
||||
@ -14,7 +13,6 @@ import { computed, proxyRefs } from 'vue'
|
||||
const props = defineProps<{
|
||||
input: WidgetInput
|
||||
nest?: boolean
|
||||
dynamicConfig?: WidgetConfiguration | undefined
|
||||
/**
|
||||
* 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`,
|
||||
@ -43,7 +41,6 @@ const selectedWidget = computed(() => {
|
||||
return registry.select(
|
||||
{
|
||||
input: props.input,
|
||||
config: props.dynamicConfig ?? undefined,
|
||||
nesting: nesting.value,
|
||||
},
|
||||
sameInputParentWidgets.value,
|
||||
@ -95,7 +92,6 @@ const spanStart = computed(() => {
|
||||
v-if="selectedWidget"
|
||||
ref="rootNode"
|
||||
:input="props.input"
|
||||
:config="dynamicConfig"
|
||||
:nesting="nesting"
|
||||
:data-span-start="spanStart"
|
||||
@update="updateHandler"
|
||||
|
@ -2,6 +2,7 @@
|
||||
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 { provideWidgetTree } from '@/providers/widgetTree'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { Ast } from '@/util/ast'
|
||||
@ -11,9 +12,10 @@ 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(props.ast)
|
||||
: props.ast
|
||||
? new ForcePort(input)
|
||||
: input
|
||||
})
|
||||
|
||||
const observedLayoutTransitions = new Set([
|
||||
|
@ -1,14 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { ForcePort } from '@/providers/portInfo'
|
||||
import { defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { AnyWidget, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { ArgumentApplication } from '@/util/callTree'
|
||||
import { ArgumentApplication, ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
const targetMaybePort = computed(() =>
|
||||
props.input.target instanceof Ast.Ast ? new ForcePort(props.input.target) : props.input.target,
|
||||
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 appClass = computed(() => {
|
||||
@ -39,7 +43,7 @@ export const widgetDefinition = defineWidget(ArgumentApplication, {
|
||||
<div v-if="props.input.infixOperator" class="infixOp" :style="operatorStyle">
|
||||
<NodeWidget :input="props.input.infixOperator" />
|
||||
</div>
|
||||
<NodeWidget :input="props.input.argument" :dynamicConfig="props.config" />
|
||||
<NodeWidget :input="props.input.argument" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
@ -13,5 +13,5 @@ export const widgetDefinition = defineWidget(ArgumentAst, {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NodeWidget :input="props.input.ast" nest />
|
||||
<NodeWidget :input="props.input.toAnyWidget()" nest />
|
||||
</template>
|
||||
|
@ -22,24 +22,23 @@ const primary = computed(() => props.nesting < 2)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget([ArgumentPlaceholder, ArgumentAst], {
|
||||
export const widgetDefinition = defineWidget([ArgumentAst.matchWithArgInfo, ArgumentPlaceholder], {
|
||||
priority: 1000,
|
||||
score: (props) =>
|
||||
props.input.info != null &&
|
||||
(props.input instanceof ArgumentPlaceholder ||
|
||||
(props.nesting < 2 && props.input.kind === ApplicationKind.Prefix))
|
||||
? Score.Perfect
|
||||
: Score.Mismatch,
|
||||
score: (props) => {
|
||||
const isPlaceholder = props.input instanceof ArgumentPlaceholder
|
||||
const isTopArg = props.nesting < 2 && props.input.kind === ApplicationKind.Prefix
|
||||
return isPlaceholder || isTopArg ? Score.Perfect : Score.Mismatch
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="WidgetArgumentName" :class="{ placeholder, primary }">
|
||||
<template v-if="showArgumentValue">
|
||||
<span class="value">{{ props.input.info!.name }}</span
|
||||
<span class="value">{{ props.input.argInfo.name }}</span
|
||||
><NodeWidget :input="props.input" />
|
||||
</template>
|
||||
<template v-else>{{ props.input.info!.name }}</template>
|
||||
<template v-else>{{ props.input.argInfo.name }}</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
const _props = defineProps(widgetProps(widgetDefinition))
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(Ast.Wildcard, {
|
||||
priority: 10,
|
||||
score: Score.Good,
|
||||
})
|
||||
export const widgetDefinition = defineWidget(
|
||||
(input) => input instanceof AnyWidget && input.ast instanceof Ast.Wildcard,
|
||||
{
|
||||
priority: 10,
|
||||
score: Score.Good,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import CheckboxWidget from '@/components/widgets/CheckboxWidget.vue'
|
||||
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@ -8,10 +8,11 @@ const props = defineProps(widgetProps(widgetDefinition))
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.input.code().endsWith('True') ?? false
|
||||
return props.input.ast?.code().endsWith('True') ?? false
|
||||
},
|
||||
set(value) {
|
||||
const node = getRawBoolNode(props.input)
|
||||
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)
|
||||
}
|
||||
@ -29,18 +30,15 @@ function getRawBoolNode(ast: Ast.Ast) {
|
||||
return null
|
||||
}
|
||||
|
||||
export const widgetDefinition = defineWidget(
|
||||
(input) => input instanceof Ast.PropertyAccess || input instanceof Ast.Ident,
|
||||
{
|
||||
priority: 10,
|
||||
score: (props) => {
|
||||
if (getRawBoolNode(props.input) != null) {
|
||||
return Score.Perfect
|
||||
}
|
||||
return Score.Mismatch
|
||||
},
|
||||
export const widgetDefinition = defineWidget(AnyWidget, {
|
||||
priority: 10,
|
||||
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
|
||||
},
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -2,13 +2,22 @@
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { injectFunctionInfo, provideFunctionInfo } from '@/providers/functionInfo'
|
||||
import type { PortId } from '@/providers/portInfo'
|
||||
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { widgetConfigurationSchema } from '@/providers/widgetRegistry/configuration'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import {
|
||||
argsWidgetConfigurationSchema,
|
||||
functionCallConfiguration,
|
||||
} from '@/providers/widgetRegistry/configuration'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { useProjectStore, type NodeVisualizationConfiguration } from '@/stores/project'
|
||||
import { assert, assertUnreachable } from '@/util/assert'
|
||||
import { assert } from '@/util/assert'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { ArgumentApplication, ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import {
|
||||
ArgumentApplication,
|
||||
ArgumentAst,
|
||||
ArgumentPlaceholder,
|
||||
getAccessOprSubject,
|
||||
interpretCall,
|
||||
} from '@/util/callTree'
|
||||
import type { Opt } from '@/util/data/opt'
|
||||
import type { ExprId } from 'shared/yjsModel'
|
||||
import { computed, proxyRefs } from 'vue'
|
||||
@ -19,17 +28,16 @@ const project = useProjectStore()
|
||||
|
||||
provideFunctionInfo(
|
||||
proxyRefs({
|
||||
callId: computed(() => props.input.exprId),
|
||||
callId: computed(() => props.input.ast.exprId),
|
||||
}),
|
||||
)
|
||||
|
||||
const methodCallInfo = computed(() => {
|
||||
const input: Ast.Ast = props.input
|
||||
return graph.db.getMethodCallInfo(input.exprId)
|
||||
return graph.db.getMethodCallInfo(props.input.ast.exprId)
|
||||
})
|
||||
|
||||
const interpreted = computed(() => {
|
||||
return ArgumentApplication.Interpret(props.input, methodCallInfo.value == null)
|
||||
return interpretCall(props.input.ast, methodCallInfo.value == null)
|
||||
})
|
||||
|
||||
const application = computed(() => {
|
||||
@ -38,13 +46,19 @@ const application = computed(() => {
|
||||
const noArgsCall = call.kind === 'prefix' ? graph.db.getMethodCall(call.func.exprId) : undefined
|
||||
|
||||
const info = methodCallInfo.value
|
||||
return ArgumentApplication.FromInterpretedWithInfo(
|
||||
const application = ArgumentApplication.FromInterpretedWithInfo(
|
||||
call,
|
||||
noArgsCall,
|
||||
info?.methodCall,
|
||||
info?.suggestion,
|
||||
{
|
||||
noArgsCall,
|
||||
appMethodCall: info?.methodCall,
|
||||
suggestion: info?.suggestion,
|
||||
widgetCfg: widgetConfiguration.value,
|
||||
},
|
||||
!info?.staticallyApplied,
|
||||
)
|
||||
return application instanceof ArgumentApplication
|
||||
? application
|
||||
: AnyWidget.Ast(application, props.input.dynamicConfig, props.input.argInfo)
|
||||
})
|
||||
|
||||
const escapeString = (str: string): string => {
|
||||
@ -53,19 +67,26 @@ const escapeString = (str: string): string => {
|
||||
}
|
||||
const makeArgsList = (args: string[]) => '[' + args.map(escapeString).join(', ') + ']'
|
||||
|
||||
const selfArgumentExprId = computed<Opt<ExprId>>(() => {
|
||||
const analyzed = ArgumentApplication.Interpret(props.input, true)
|
||||
const selfArgumentAstId = computed<Opt<ExprId>>(() => {
|
||||
const analyzed = interpretCall(props.input.ast, true)
|
||||
if (analyzed.kind === 'infix') {
|
||||
return analyzed.lhs?.exprId
|
||||
} else {
|
||||
return analyzed.args[0]?.argument.exprId
|
||||
const knownArguments = methodCallInfo.value?.suggestion?.arguments
|
||||
const selfArgument =
|
||||
knownArguments?.[0]?.name === 'self'
|
||||
? getAccessOprSubject(analyzed.func)
|
||||
: analyzed.args[0]?.argument
|
||||
return selfArgument?.exprId
|
||||
}
|
||||
})
|
||||
|
||||
const visualizationConfig = computed<Opt<NodeVisualizationConfiguration>>(() => {
|
||||
const tree = props.input
|
||||
const expressionId = selfArgumentExprId.value
|
||||
const astId = tree.exprId
|
||||
// If we inherit dynamic config, there is no point in attaching visualization.
|
||||
if (props.input.dynamicConfig) return null
|
||||
|
||||
const expressionId = selfArgumentAstId.value
|
||||
const astId = props.input.ast.exprId
|
||||
if (astId == null || expressionId == null) return null
|
||||
const info = graph.db.getMethodCallInfo(astId)
|
||||
if (!info) return null
|
||||
@ -86,11 +107,12 @@ const visualizationConfig = computed<Opt<NodeVisualizationConfiguration>>(() =>
|
||||
|
||||
const visualizationData = project.useVisualizationData(visualizationConfig)
|
||||
const widgetConfiguration = computed(() => {
|
||||
if (props.input.dynamicConfig?.kind === 'FunctionCall') return props.input.dynamicConfig
|
||||
const data = visualizationData.value
|
||||
if (data != null && data.ok) {
|
||||
const parseResult = widgetConfigurationSchema.safeParse(data.value)
|
||||
const parseResult = argsWidgetConfigurationSchema.safeParse(data.value)
|
||||
if (parseResult.success) {
|
||||
return parseResult.data
|
||||
return functionCallConfiguration(parseResult.data)
|
||||
} else {
|
||||
console.error('Unable to parse widget configuration.', data, parseResult.error)
|
||||
}
|
||||
@ -123,7 +145,7 @@ function handleArgUpdate(value: unknown, origin: PortId): boolean {
|
||||
console.error(`Don't know how to put this in a tree`, value)
|
||||
return true
|
||||
}
|
||||
const name = argApp.argument.insertAsNamed ? argApp.argument.info.name : null
|
||||
const name = argApp.argument.insertAsNamed ? argApp.argument.argInfo.name : null
|
||||
const ast = Ast.App.new(argApp.appTree, name, newArg, edit)
|
||||
props.onUpdate(ast, argApp.appTree.exprId)
|
||||
return true
|
||||
@ -168,10 +190,7 @@ function handleArgUpdate(value: unknown, origin: PortId): boolean {
|
||||
} else {
|
||||
// Process an argument to the right of the removed argument.
|
||||
assert(innerApp.appTree instanceof Ast.App)
|
||||
const infoName =
|
||||
innerApp.argument instanceof ArgumentAst && innerApp.argument.info != null
|
||||
? innerApp.argument.info?.name ?? null
|
||||
: null
|
||||
const infoName = innerApp.argument.argInfo?.name ?? null
|
||||
if (newArgs.length || (!innerApp.appTree.argumentName && infoName)) {
|
||||
// Positional arguments following the deleted argument must all be rewritten to named.
|
||||
newArgs.unshift({
|
||||
@ -204,10 +223,10 @@ function handleArgUpdate(value: unknown, origin: PortId): boolean {
|
||||
}
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget([Ast.App, Ast.Ident, Ast.OprApp], {
|
||||
export const widgetDefinition = defineWidget(AnyWidget.matchFunctionCall, {
|
||||
priority: -10,
|
||||
score: (props, db) => {
|
||||
const ast = props.input
|
||||
const ast = props.input.ast
|
||||
if (ast.exprId == null) return Score.Mismatch
|
||||
const prevFunctionState = injectFunctionInfo(true)
|
||||
|
||||
@ -229,5 +248,5 @@ export const widgetDefinition = defineWidget([Ast.App, Ast.Ident, Ast.OprApp], {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NodeWidget :input="application" :dynamicConfig="widgetConfiguration" @update="handleArgUpdate" />
|
||||
<NodeWidget :input="application" @update="handleArgUpdate" />
|
||||
</template>
|
||||
|
@ -1,35 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import { ForcePort } from '@/providers/portInfo'
|
||||
import type { WidgetInput } from '@/providers/widgetRegistry'
|
||||
import { defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { AnyWidget, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
|
||||
const spanClass = computed(() => props.input.typeName())
|
||||
const children = computed(() => [...props.input.children()])
|
||||
const spanClass = computed(() => props.input.ast.typeName())
|
||||
const children = computed(() => [...props.input.ast.children()])
|
||||
|
||||
function transformChild(child: WidgetInput) {
|
||||
if (!(props.input instanceof Ast.Ast)) return child
|
||||
if (props.input instanceof Ast.PropertyAccess) {
|
||||
if (child === props.input.lhs) {
|
||||
return new ForcePort(child)
|
||||
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 instanceof Ast.OprApp) {
|
||||
if (child === props.input.rhs || child === props.input.lhs) {
|
||||
return new ForcePort(child)
|
||||
} 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 instanceof Ast.UnaryOprApp && child === props.input.argument) {
|
||||
return new ForcePort(child)
|
||||
} else if (props.input.ast instanceof Ast.UnaryOprApp && child === props.input.ast.argument) {
|
||||
return new ForcePort(childInput)
|
||||
}
|
||||
return child
|
||||
return childInput
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(Ast.Ast, {
|
||||
export const widgetDefinition = defineWidget(AnyWidget.matchAst, {
|
||||
priority: 1001,
|
||||
})
|
||||
</script>
|
||||
|
@ -1,30 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import SliderWidget from '@/components/widgets/SliderWidget.vue'
|
||||
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
const value = computed({
|
||||
get() {
|
||||
return parseFloat(props.input.code())
|
||||
const valueStr = props.input.ast?.code() ?? props.input.argInfo?.defaultValue ?? ''
|
||||
return valueStr ? parseFloat(valueStr) : 0
|
||||
},
|
||||
set(value) {
|
||||
props.onUpdate(value.toString(), props.input.exprId)
|
||||
props.onUpdate(value.toString(), props.input.portId)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(
|
||||
(input): input is Ast.NumericLiteral | Ast.NegationOprApp =>
|
||||
input instanceof Ast.NumericLiteral ||
|
||||
(input instanceof Ast.NegationOprApp && input.argument instanceof Ast.NumericLiteral),
|
||||
{
|
||||
priority: 10,
|
||||
score: Score.Perfect,
|
||||
export const widgetDefinition = defineWidget(AnyWidget, {
|
||||
priority: 10,
|
||||
score: (props) => {
|
||||
if (
|
||||
props.input.ast instanceof Ast.NumericLiteral ||
|
||||
(props.input.ast instanceof Ast.NegationOprApp &&
|
||||
props.input.ast.argument instanceof Ast.NumericLiteral)
|
||||
)
|
||||
return Score.Perfect
|
||||
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'
|
||||
)
|
||||
return Score.Perfect
|
||||
return Score.Mismatch
|
||||
},
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -5,8 +5,7 @@ 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 type { WidgetInput } from '@/providers/widgetRegistry'
|
||||
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { injectWidgetTree } from '@/providers/widgetTree'
|
||||
import { PortViewInstance, useGraphStore } from '@/stores/graph'
|
||||
import { Ast } from '@/util/ast'
|
||||
@ -14,6 +13,7 @@ import { ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { Rect } from '@/util/data/rect'
|
||||
import { cachedGetter } from '@/util/reactivity'
|
||||
import { uuidv4 } from 'lib0/random'
|
||||
import type { ExprId } from 'shared/yjsModel'
|
||||
import {
|
||||
computed,
|
||||
markRaw,
|
||||
@ -27,7 +27,7 @@ import {
|
||||
watchEffect,
|
||||
} from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps<WidgetInput>(widgetDefinition))
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
|
||||
const graph = useGraphStore()
|
||||
|
||||
@ -37,7 +37,9 @@ const selection = injectGraphSelection(true)
|
||||
|
||||
const isHovered = ref(false)
|
||||
|
||||
const hasConnection = computed(() => graph.db.connections.reverseLookup(portId.value).size > 0)
|
||||
const hasConnection = computed(
|
||||
() => graph.db.connections.reverseLookup(portId.value as ExprId).size > 0,
|
||||
)
|
||||
const isCurrentEdgeHoverTarget = computed(
|
||||
() => isHovered.value && graph.unconnectedEdge != null && selection?.hoveredPort === portId.value,
|
||||
)
|
||||
@ -74,12 +76,6 @@ const innerWidget = computed(() =>
|
||||
props.input instanceof ForcePort ? props.input.inner : props.input,
|
||||
)
|
||||
|
||||
providePortInfo(
|
||||
proxyRefs({
|
||||
portId,
|
||||
connected: hasConnection,
|
||||
}),
|
||||
)
|
||||
providePortInfo(proxyRefs({ portId, connected: hasConnection }))
|
||||
|
||||
watch(nodeSize, updateRect)
|
||||
@ -115,39 +111,51 @@ function updateRect() {
|
||||
|
||||
<script lang="ts">
|
||||
function portIdOfInput(input: unknown): PortId | undefined {
|
||||
return input instanceof Ast.Ast
|
||||
? (input.exprId as string as PortId)
|
||||
: input instanceof ForcePort
|
||||
? (input.inner.exprId as string as PortId)
|
||||
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,
|
||||
ArgumentAst,
|
||||
ArgumentPlaceholder,
|
||||
(ast) =>
|
||||
ast instanceof Ast.Invalid ||
|
||||
ast instanceof Ast.BodyBlock ||
|
||||
ast instanceof Ast.Group ||
|
||||
ast instanceof Ast.NumericLiteral ||
|
||||
ast instanceof Ast.OprApp ||
|
||||
ast instanceof Ast.UnaryOprApp ||
|
||||
ast instanceof Ast.Wildcard ||
|
||||
ast instanceof Ast.TextLiteral,
|
||||
],
|
||||
[ForcePort, AnyWidget.matchAst, ArgumentAst, ArgumentPlaceholder],
|
||||
{
|
||||
priority: 0,
|
||||
score: (props, _db) => {
|
||||
const portInfo = injectPortInfo(true)
|
||||
if (portInfo != null && portInfo.portId === portIdOfInput(props.input)) {
|
||||
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
|
||||
} else {
|
||||
return Score.Perfect
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
return Score.Mismatch
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -168,7 +176,7 @@ export const widgetDefinition = defineWidget(
|
||||
@pointerenter="isHovered = true"
|
||||
@pointerleave="isHovered = false"
|
||||
>
|
||||
<NodeWidget :input="innerWidget" :dynamicConfig="props.config" />
|
||||
<NodeWidget :input="innerWidget" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,29 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import NodeWidget from '@/components/GraphEditor/NodeWidget.vue'
|
||||
import DropdownWidget from '@/components/widgets/DropdownWidget.vue'
|
||||
import { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import {
|
||||
functionCallConfiguration,
|
||||
type ArgumentWidgetConfiguration,
|
||||
} from '@/providers/widgetRegistry/configuration'
|
||||
import { ArgumentAst, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { qnJoin, qnSegments, tryQualifiedName } from '@/util/qualifiedName'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
|
||||
/** Static selection entry, label and value are the same. */
|
||||
interface StaticTag {
|
||||
kind: 'Static'
|
||||
interface Tag {
|
||||
label: string
|
||||
/** If not set, the value is same as label */
|
||||
value?: string
|
||||
parameters?: ArgumentWidgetConfiguration[]
|
||||
}
|
||||
|
||||
/** Dynamic selection entry, label and value can be different. */
|
||||
interface DynamicTag {
|
||||
kind: 'Dynamic'
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
type Tag = StaticTag | DynamicTag
|
||||
|
||||
const staticTags = computed<Tag[]>(() => {
|
||||
const tags = props.input.info?.tagValues
|
||||
const tags = props.input.argInfo?.tagValues
|
||||
if (tags == null) return []
|
||||
return tags.map((tag) => {
|
||||
const qualifiedName = tryQualifiedName(tag)
|
||||
@ -31,35 +28,55 @@ const staticTags = computed<Tag[]>(() => {
|
||||
const segments = qnSegments(qualifiedName.value).slice(-2)
|
||||
if (segments[0] == undefined) return { kind: 'Static', label: tag }
|
||||
if (segments[1] == undefined) return { kind: 'Static', label: segments[0] }
|
||||
return { kind: 'Static', label: qnJoin(segments[0], segments[1]) }
|
||||
return { label: qnJoin(segments[0], segments[1]) }
|
||||
})
|
||||
})
|
||||
|
||||
const dynamicTags = computed<Tag[]>(() => {
|
||||
const config = props.config
|
||||
if (config == null) return []
|
||||
const [_, widgetConfig] = config.find(([name]) => name === props.input.info?.name) ?? []
|
||||
if (widgetConfig && widgetConfig.kind == 'Single_Choice') {
|
||||
return widgetConfig.values.map((value) => ({
|
||||
kind: 'Dynamic',
|
||||
label: value.label || value.value,
|
||||
value: value.value,
|
||||
}))
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
const config = props.input.dynamicConfig
|
||||
if (config?.kind !== 'Single_Choice') return []
|
||||
return config.values.map((value) => ({
|
||||
label: value.label || value.value,
|
||||
value: value.value,
|
||||
parameters: value.parameters,
|
||||
}))
|
||||
})
|
||||
|
||||
const tags = computed(() => (dynamicTags.value.length > 0 ? dynamicTags.value : staticTags.value))
|
||||
const tagLabels = computed(() => tags.value.map((tag) => tag.label))
|
||||
const tagValues = computed(() => {
|
||||
return tags.value.map((tag) => (tag.kind == 'Static' ? tag.label : tag.value))
|
||||
})
|
||||
|
||||
const selectedIndex = ref<number>()
|
||||
const selectedTag = computed(() =>
|
||||
selectedIndex.value != null ? tags.value[selectedIndex.value] : undefined,
|
||||
)
|
||||
const selectedValue = computed(() => {
|
||||
if (selectedIndex.value == null) return props.input.info?.defaultValue ?? ''
|
||||
return tagValues.value[selectedIndex.value] ?? ''
|
||||
if (selectedTag.value == null) return props.input.argInfo?.defaultValue ?? ''
|
||||
return selectedTag.value.value ?? selectedTag.value.label
|
||||
})
|
||||
const innerWidgetInput = computed(() => {
|
||||
if (selectedTag.value == null) return props.input
|
||||
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,
|
||||
)
|
||||
})
|
||||
const showDropdownWidget = ref(false)
|
||||
|
||||
@ -69,30 +86,25 @@ function toggleDropdownWidget() {
|
||||
|
||||
// When the selected index changes, we update the expression content.
|
||||
watch(selectedIndex, (_index) => {
|
||||
// TODO: Handle the case for ArgumentPlaceholder once the AST has been updated,
|
||||
const id = props.input instanceof ArgumentAst ? props.input.ast.exprId : undefined
|
||||
const expression = selectedValue.value ?? ''
|
||||
if (id) props.onUpdate(expression, id)
|
||||
props.onUpdate(selectedValue.value, props.input.portId)
|
||||
showDropdownWidget.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget([ArgumentPlaceholder, ArgumentAst], {
|
||||
export const widgetDefinition = defineWidget([AnyWidget, ArgumentAst, ArgumentPlaceholder], {
|
||||
priority: 999,
|
||||
score: (props) => {
|
||||
const tags = props.input.info?.tagValues
|
||||
const [_, dynamicConfig] = props.config?.find(([name]) => name === props.input.info?.name) ?? []
|
||||
const isSuitableDynamicConfig = dynamicConfig && dynamicConfig.kind === 'Single_Choice'
|
||||
if (tags == null && !isSuitableDynamicConfig) return Score.Mismatch
|
||||
return Score.Perfect
|
||||
if (props.input.dynamicConfig?.kind === 'Single_Choice') return Score.Perfect
|
||||
if (props.input.argInfo?.tagValues != null) return Score.Perfect
|
||||
return Score.Mismatch
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="WidgetSelection" @pointerdown="toggleDropdownWidget">
|
||||
<NodeWidget :input="props.input" />
|
||||
<NodeWidget :input="innerWidgetInput" />
|
||||
<DropdownWidget
|
||||
v-if="showDropdownWidget"
|
||||
class="dropdownContainer"
|
||||
|
@ -18,7 +18,7 @@ export const widgetDefinition = defineWidget([ArgumentAst, ArgumentPlaceholder],
|
||||
|
||||
<template>
|
||||
<span class="WidgetTopLevelArgument">
|
||||
<NodeWidget :input="props.input" :dynamicConfig="props.config" nest />
|
||||
<NodeWidget :input="props.input" nest />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
@ -3,46 +3,62 @@ 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 { Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { AnyWidget, Score, defineWidget, widgetProps } from '@/providers/widgetRegistry'
|
||||
import { Ast, RawAst } from '@/util/ast'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
|
||||
const itemConfig = computed(() =>
|
||||
props.input.dynamicConfig?.kind === 'Vector_Editor'
|
||||
? props.input.dynamicConfig.item_editor
|
||||
: undefined,
|
||||
)
|
||||
|
||||
const defaultItem = computed(() => {
|
||||
if (props.input.dynamicConfig?.kind === 'Vector_Editor') {
|
||||
return Ast.parse(props.input.dynamicConfig.item_default)
|
||||
} else {
|
||||
return Ast.Wildcard.new()
|
||||
}
|
||||
})
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return Array.from(props.input.children()).filter(
|
||||
if (props.input.ast == null) return []
|
||||
return Array.from(props.input.ast.children()).filter(
|
||||
(child): child is Ast.Ast => child instanceof Ast.Ast,
|
||||
)
|
||||
},
|
||||
set(value) {
|
||||
const newCode = `[${value.map((item) => item.code()).join(', ')}]`
|
||||
props.onUpdate(newCode, props.input.exprId)
|
||||
props.onUpdate(newCode, props.input.portId)
|
||||
},
|
||||
})
|
||||
|
||||
const navigator = injectGraphNavigator(true)
|
||||
|
||||
function itemPort(item: Ast.Ast) {
|
||||
return new ForcePort(item)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export const widgetDefinition = defineWidget(Ast.Ast, {
|
||||
export const widgetDefinition = defineWidget(AnyWidget, {
|
||||
priority: 1000,
|
||||
score: (props) =>
|
||||
props.input.treeType === RawAst.Tree.Type.Array ? Score.Perfect : Score.Mismatch,
|
||||
score: (props) => {
|
||||
if (props.input.dynamicConfig?.kind === 'Vector_Editor') return Score.Perfect
|
||||
else if (props.input.argInfo?.reprType.startsWith('Standard.Base.Data.Vector.Vector'))
|
||||
return Score.Good
|
||||
else
|
||||
return props.input.ast?.treeType === RawAst.Tree.Type.Array ? Score.Perfect : Score.Mismatch
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ListWidget
|
||||
v-model="value"
|
||||
:default="Ast.Wildcard.new"
|
||||
:getKey="(item: Ast.Ast) => item.exprId"
|
||||
:default="() => defaultItem"
|
||||
:getKey="(ast: Ast.Ast) => ast.exprId"
|
||||
dragMimeType="application/x-enso-ast-node"
|
||||
:toPlainText="(item: Ast.Ast) => item.code()"
|
||||
:toPlainText="(ast: Ast.Ast) => ast.code()"
|
||||
:toDragPayload="(ast: Ast.Ast) => ast.serialize()"
|
||||
:fromDragPayload="Ast.deserialize"
|
||||
:toDragPosition="(p) => navigator?.clientToScenePos(p) ?? p"
|
||||
@ -50,7 +66,7 @@ export const widgetDefinition = defineWidget(Ast.Ast, {
|
||||
contenteditable="false"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<NodeWidget :input="itemPort(item)" />
|
||||
<NodeWidget :input="new ForcePort(AnyWidget.Ast(item, itemConfig))" />
|
||||
</template>
|
||||
</ListWidget>
|
||||
</template>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
AnyWidget,
|
||||
Score,
|
||||
WidgetRegistry,
|
||||
defineWidget,
|
||||
@ -6,12 +7,12 @@ import {
|
||||
type WidgetInput,
|
||||
type WidgetModule,
|
||||
} from '@/providers/widgetRegistry'
|
||||
import { DisplayMode, widgetConfigurationSchema } from '@/providers/widgetRegistry/configuration'
|
||||
import { GraphDb } from '@/stores/graph/graphDatabase'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { ApplicationKind, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import { DisplayMode, argsWidgetConfigurationSchema } from '../widgetRegistry/configuration'
|
||||
|
||||
describe('WidgetRegistry', () => {
|
||||
function makeMockWidget<T extends WidgetInput>(
|
||||
@ -26,7 +27,7 @@ describe('WidgetRegistry', () => {
|
||||
|
||||
const widgetA = makeMockWidget(
|
||||
'A',
|
||||
defineWidget(Ast.Ast, {
|
||||
defineWidget(AnyWidget, {
|
||||
priority: 1,
|
||||
}),
|
||||
)
|
||||
@ -48,23 +49,25 @@ describe('WidgetRegistry', () => {
|
||||
|
||||
const widgetD = makeMockWidget(
|
||||
'D',
|
||||
defineWidget(Ast.Ast, {
|
||||
defineWidget(AnyWidget, {
|
||||
priority: 20,
|
||||
score: (props) => (props.input.code() === '_' ? Score.Perfect : Score.Mismatch),
|
||||
score: (props) => (props.input.ast?.code() === '_' ? Score.Perfect : Score.Mismatch),
|
||||
}),
|
||||
)
|
||||
|
||||
const someAst = Ast.parse('foo')
|
||||
const blankAst = Ast.parse('_')
|
||||
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',
|
||||
type: 'Any',
|
||||
reprType: 'Any',
|
||||
isSuspended: false,
|
||||
hasDefault: false,
|
||||
},
|
||||
ApplicationKind.Prefix,
|
||||
false,
|
||||
)
|
||||
|
||||
const mockGraphDb = GraphDb.Mock()
|
||||
@ -75,19 +78,16 @@ describe('WidgetRegistry', () => {
|
||||
registry.registerWidgetModule(widgetD)
|
||||
|
||||
test('selects a widget based on the input type', () => {
|
||||
const forAst = registry.select({ input: someAst, config: undefined, nesting: 0 })
|
||||
const forArg = registry.select({ input: somePlaceholder, config: undefined, nesting: 0 })
|
||||
const forAst = registry.select({ input: someAst, nesting: 0 })
|
||||
const forArg = registry.select({ input: somePlaceholder, nesting: 0 })
|
||||
expect(forAst).toStrictEqual(widgetA)
|
||||
expect(forArg).toStrictEqual(widgetB)
|
||||
})
|
||||
|
||||
test('selects a widget outside of the excluded set', () => {
|
||||
const forAst = registry.select(
|
||||
{ input: someAst, config: undefined, nesting: 0 },
|
||||
new Set([widgetA.default]),
|
||||
)
|
||||
const forAst = registry.select({ input: someAst, nesting: 0 }, new Set([widgetA.default]))
|
||||
const forArg = registry.select(
|
||||
{ input: somePlaceholder, config: undefined, nesting: 0 },
|
||||
{ input: somePlaceholder, nesting: 0 },
|
||||
new Set([widgetB.default]),
|
||||
)
|
||||
expect(forAst).toStrictEqual(widgetC)
|
||||
@ -96,7 +96,7 @@ describe('WidgetRegistry', () => {
|
||||
|
||||
test('returns undefined when all options are exhausted', () => {
|
||||
const selected = registry.select(
|
||||
{ input: someAst, config: undefined, nesting: 0 },
|
||||
{ input: someAst, nesting: 0 },
|
||||
new Set([widgetA.default, widgetC.default]),
|
||||
)
|
||||
expect(selected).to.be.undefined
|
||||
@ -104,11 +104,11 @@ describe('WidgetRegistry', () => {
|
||||
|
||||
test('prefers low priority perfect over good high priority', () => {
|
||||
const selectedFirst = registry.select(
|
||||
{ input: blankAst, config: undefined, nesting: 0 },
|
||||
{ input: blankAst, nesting: 0 },
|
||||
new Set([widgetA.default]),
|
||||
)
|
||||
const selectedNext = registry.select(
|
||||
{ input: blankAst, config: undefined, nesting: 0 },
|
||||
{ input: blankAst, nesting: 0 },
|
||||
new Set([widgetA.default, widgetD.default]),
|
||||
)
|
||||
expect(selectedFirst).toStrictEqual(widgetD)
|
||||
@ -179,7 +179,7 @@ describe('Engine-provided configuration', () => {
|
||||
{ input: [singleChoiceData], expected: [singleChoiceExpected] },
|
||||
{ input: [vectorEditorData], expected: [vectorEditorExpected] },
|
||||
])('Testing engine configuration', ({ input, expected }) => {
|
||||
const res = widgetConfigurationSchema.safeParse(input)
|
||||
const res = argsWidgetConfigurationSchema.safeParse(input)
|
||||
expect(res).toMatchObject({ success: true, data: expected })
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createContextStore } from '@/providers'
|
||||
import type { AnyWidget } from '@/providers/widgetRegistry'
|
||||
import { GetUsageKey } from '@/providers/widgetUsageInfo'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { identity } from '@vueuse/core'
|
||||
import type { ExprId } from 'shared/yjsModel'
|
||||
|
||||
@ -20,7 +20,7 @@ const { provideFn, injectFn } = createContextStore('Port info', identity<PortInf
|
||||
* even if it wouldn't normally be rendered as such.
|
||||
*/
|
||||
export class ForcePort {
|
||||
constructor(public inner: Ast.Ast) {
|
||||
constructor(public inner: AnyWidget) {
|
||||
if (inner instanceof ForcePort) throw new Error('ForcePort cannot be nested')
|
||||
}
|
||||
[GetUsageKey]() {
|
||||
|
@ -1,11 +1,71 @@
|
||||
import { createContextStore } from '@/providers'
|
||||
import type { PortId } from '@/providers/portInfo'
|
||||
import { type WidgetConfiguration } from '@/providers/widgetRegistry/configuration'
|
||||
import type { WidgetConfiguration } from '@/providers/widgetRegistry/configuration'
|
||||
import type { GraphDb } from '@/stores/graph/graphDatabase'
|
||||
import type { SuggestionEntryArgument } 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)
|
||||
}
|
||||
|
||||
isPlaceholder() {
|
||||
return this.ast == null
|
||||
}
|
||||
|
||||
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(
|
||||
input: WidgetInput,
|
||||
): input is AnyWidget & { ast: 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
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
|
||||
@ -27,7 +87,9 @@ export type WidgetComponent<T extends WidgetInput> = Component<WidgetProps<T>>
|
||||
* 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 {}
|
||||
export interface WidgetInputTypes {
|
||||
[AnyWidgetKey]: AnyWidget
|
||||
}
|
||||
|
||||
/**
|
||||
* An union of all possible widget input types. A collection of all correctly declared value types
|
||||
@ -60,7 +122,6 @@ export enum Score {
|
||||
|
||||
export interface WidgetProps<T> {
|
||||
input: T
|
||||
config?: WidgetConfiguration | undefined
|
||||
nesting: number
|
||||
}
|
||||
|
||||
@ -76,7 +137,6 @@ export function widgetProps<T extends WidgetInput>(_def: WidgetDefinition<T>) {
|
||||
type: Object as PropType<T>,
|
||||
required: true,
|
||||
},
|
||||
config: { type: Object as PropType<WidgetConfiguration | undefined>, required: false },
|
||||
nesting: { type: Number, required: true },
|
||||
onUpdate: {
|
||||
type: Function as PropType<(value: unknown | undefined, origin: PortId) => void>,
|
||||
@ -290,7 +350,6 @@ export class WidgetRegistry {
|
||||
best = widgetModule
|
||||
}
|
||||
}
|
||||
// Once we've checked all widgets, return the best match found, if any.
|
||||
return best
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* An external configuration for a widget retreived from the language server.
|
||||
*
|
||||
* The expected configuration type is defined as Enso type `Widget` in the following file:
|
||||
* distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso
|
||||
*
|
||||
* To avoid ruining forward compatibility, only fields that are used by the IDE are defined here.
|
||||
*/
|
||||
export type WidgetConfiguration = z.infer<typeof widgetConfigurationSchema>
|
||||
|
||||
/** Intermediate step in the parsing process, when we rename `constructor` field to `kind`.
|
||||
*
|
||||
* It helps to avoid issues with TypeScript, which considers `constructor` as a reserved keyword in many contexts.
|
||||
@ -56,10 +46,17 @@ const choiceSchema = z.object({
|
||||
})
|
||||
export type Choice = z.infer<typeof choiceSchema>
|
||||
|
||||
/** Defining widget definition type explicitly is necessary because of recursive structure
|
||||
* of some variants, like VectorEditor. Zod can’t handle type inference by itself.
|
||||
/**
|
||||
* An external configuration for a widget retreived from the language server.
|
||||
*
|
||||
* The expected configuration type is defined as Enso type `Widget` in the following file:
|
||||
* distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso
|
||||
*
|
||||
* To avoid ruining forward compatibility, only fields that are used by the IDE are defined here.
|
||||
*/
|
||||
type WidgetDefinition =
|
||||
// Defining widget definition type explicitly is necessary because of recursive structure
|
||||
// of some variants, like VectorEditor. Zod can’t handle type inference by itself.
|
||||
export type WidgetConfiguration =
|
||||
| SingleChoice
|
||||
| VectorEditor
|
||||
| MultiChoice
|
||||
@ -69,10 +66,11 @@ type WidgetDefinition =
|
||||
| TextInput
|
||||
| FolderBrowse
|
||||
| FileBrowse
|
||||
| FunctionCall
|
||||
|
||||
export interface VectorEditor {
|
||||
kind: 'Vector_Editor'
|
||||
item_editor: WidgetDefinition
|
||||
item_editor: WidgetConfiguration
|
||||
item_default: string
|
||||
}
|
||||
|
||||
@ -110,37 +108,53 @@ export interface SingleChoice {
|
||||
values: Choice[]
|
||||
}
|
||||
|
||||
const widgetDefinitionSchema: z.ZodType<WidgetDefinition & WithDisplay, z.ZodTypeDef, any> =
|
||||
withKindSchema.pipe(
|
||||
z.discriminatedUnion('kind', [
|
||||
z
|
||||
.object({
|
||||
kind: z.literal('Single_Choice'),
|
||||
label: z.string().nullable(),
|
||||
values: z.array(choiceSchema),
|
||||
})
|
||||
.merge(withDisplay),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal('Vector_Editor'),
|
||||
/* eslint-disable camelcase */
|
||||
item_editor: z.lazy(() => widgetDefinitionSchema),
|
||||
item_default: z.string(),
|
||||
/* eslint-enable camelcase */
|
||||
})
|
||||
.merge(withDisplay),
|
||||
z.object({ kind: z.literal('Multi_Choice') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Code_Input') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Boolean_Input') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Numeric_Input') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Text_Input') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Folder_Browse') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('File_Browse') }).merge(withDisplay),
|
||||
]),
|
||||
)
|
||||
export interface FunctionCall {
|
||||
kind: 'FunctionCall'
|
||||
parameters: Map<string, (WidgetConfiguration & WithDisplay) | null>
|
||||
}
|
||||
|
||||
export const widgetConfigurationSchema: z.ZodType<
|
||||
WidgetConfiguration & WithDisplay,
|
||||
z.ZodTypeDef,
|
||||
any
|
||||
> = withKindSchema.pipe(
|
||||
z.discriminatedUnion('kind', [
|
||||
z
|
||||
.object({
|
||||
kind: z.literal('Single_Choice'),
|
||||
label: z.string().nullable(),
|
||||
values: z.array(choiceSchema),
|
||||
})
|
||||
.merge(withDisplay),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal('Vector_Editor'),
|
||||
/* eslint-disable camelcase */
|
||||
item_editor: z.lazy(() => widgetConfigurationSchema),
|
||||
item_default: z.string(),
|
||||
/* eslint-enable camelcase */
|
||||
})
|
||||
.merge(withDisplay),
|
||||
z.object({ kind: z.literal('Multi_Choice') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Code_Input') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Boolean_Input') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Numeric_Input') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Text_Input') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('Folder_Browse') }).merge(withDisplay),
|
||||
z.object({ kind: z.literal('File_Browse') }).merge(withDisplay),
|
||||
]),
|
||||
)
|
||||
|
||||
const argNameSchema = z.string()
|
||||
const argumentSchema = z.tuple([argNameSchema, widgetDefinitionSchema.nullable()])
|
||||
export type Argument = z.infer<typeof argumentSchema>
|
||||
const argumentSchema = z.tuple([argNameSchema, widgetConfigurationSchema.nullable()])
|
||||
export type ArgumentWidgetConfiguration = z.infer<typeof argumentSchema>
|
||||
|
||||
export const widgetConfigurationSchema = z.array(argumentSchema)
|
||||
export const argsWidgetConfigurationSchema = z.array(argumentSchema)
|
||||
export type ArgsWidgetConfiguration = z.infer<typeof argsWidgetConfigurationSchema>
|
||||
|
||||
export function functionCallConfiguration(parameters: ArgumentWidgetConfiguration[]): FunctionCall {
|
||||
return {
|
||||
kind: 'FunctionCall',
|
||||
parameters: new Map(parameters),
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { PortId } from '@/providers/portInfo'
|
||||
import { ComputedValueRegistry, type ExpressionInfo } from '@/stores/project/computedValueRegistry'
|
||||
import { SuggestionDb, groupColorStyle, type Group } from '@/stores/suggestionDatabase'
|
||||
import type { SuggestionEntry } from '@/stores/suggestionDatabase/entry'
|
||||
@ -138,7 +137,7 @@ export class GraphDb {
|
||||
// Display connection starting from existing node.
|
||||
//TODO[ao]: When implementing input nodes, they should be taken into account here.
|
||||
if (srcNode == null) return []
|
||||
function* allTargets(db: GraphDb): Generator<[ExprId, PortId]> {
|
||||
function* allTargets(db: GraphDb): Generator<[ExprId, ExprId]> {
|
||||
for (const usage of info.usages) {
|
||||
const targetNode = db.getExpressionNodeId(usage)
|
||||
// Display only connections to existing targets and different than source node
|
||||
|
@ -191,7 +191,7 @@ test('Adding new argument', () => {
|
||||
const test = new Fixture()
|
||||
const newArg: lsTypes.SuggestionEntryArgument = {
|
||||
name: 'c',
|
||||
type: 'Any',
|
||||
reprType: 'Any',
|
||||
hasDefault: false,
|
||||
isSuspended: false,
|
||||
}
|
||||
@ -212,13 +212,13 @@ test('Adding new argument', () => {
|
||||
test('Modifying arguments', () => {
|
||||
const newArg1 = {
|
||||
name: 'c',
|
||||
type: 'Standard.Base.Number',
|
||||
reprType: 'Standard.Base.Number',
|
||||
isSuspended: true,
|
||||
hasDefault: false,
|
||||
}
|
||||
const newArg2 = {
|
||||
name: 'b',
|
||||
type: 'Any',
|
||||
reprType: 'Any',
|
||||
isSuspended: false,
|
||||
hasDefault: true,
|
||||
defaultValue: 'Nothing',
|
||||
@ -275,14 +275,14 @@ class Fixture {
|
||||
]
|
||||
arg1 = {
|
||||
name: 'a',
|
||||
type: 'Any',
|
||||
reprType: 'Any',
|
||||
isSuspended: false,
|
||||
hasDefault: true,
|
||||
defaultValue: 'Nothing',
|
||||
}
|
||||
arg2 = {
|
||||
name: 'b',
|
||||
type: 'Any',
|
||||
reprType: 'Any',
|
||||
isSuspended: false,
|
||||
hasDefault: false,
|
||||
}
|
||||
|
@ -184,3 +184,12 @@ export function makeLocal(
|
||||
assert(isQualifiedName(returnType))
|
||||
return makeSimpleEntry(SuggestionKind.Local, definedIn, name, returnType)
|
||||
}
|
||||
|
||||
export function makeArgument(name: string, type: string = 'Any'): SuggestionEntryArgument {
|
||||
return {
|
||||
name,
|
||||
reprType: type,
|
||||
isSuspended: false,
|
||||
hasDefault: false,
|
||||
}
|
||||
}
|
||||
|
@ -304,7 +304,7 @@ function applyArgumentsUpdate(
|
||||
const nameUpdate = applyPropertyUpdate('name', arg, update)
|
||||
if (!nameUpdate.ok) return nameUpdate
|
||||
const typeUpdate = applyFieldUpdate('reprType', update, (type) => {
|
||||
arg.type = type
|
||||
arg.reprType = type
|
||||
})
|
||||
if (!typeUpdate.ok) return typeUpdate
|
||||
const isSuspendedUpdate = applyPropertyUpdate('isSuspended', arg, update)
|
||||
|
@ -1,76 +1,132 @@
|
||||
import { SuggestionKind, type SuggestionEntry } from '@/stores/suggestionDatabase/entry'
|
||||
import * as widgetCfg from '@/providers/widgetRegistry/configuration'
|
||||
import { makeArgument, makeMethod, makeModuleMethod } from '@/stores/suggestionDatabase/entry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { ArgumentApplication, ArgumentPlaceholder } from '@/util/callTree'
|
||||
import {
|
||||
ArgumentApplication,
|
||||
ArgumentAst,
|
||||
ArgumentPlaceholder,
|
||||
interpretCall,
|
||||
} from '@/util/callTree'
|
||||
import { isSome } from '@/util/data/opt'
|
||||
import { type Identifier, type QualifiedName } from '@/util/qualifiedName'
|
||||
import type { MethodCall } from 'shared/languageServerTypes'
|
||||
import { assert, expect, test } from 'vitest'
|
||||
|
||||
const mockSuggestion: SuggestionEntry = {
|
||||
kind: SuggestionKind.Method,
|
||||
name: 'func' as Identifier,
|
||||
definedIn: 'Foo.Bar' as QualifiedName,
|
||||
selfType: 'Foo.Bar',
|
||||
returnType: 'Any',
|
||||
arguments: [
|
||||
{ name: 'self', type: 'Any', isSuspended: false, hasDefault: false },
|
||||
{ name: 'a', type: 'Any', isSuspended: false, hasDefault: false },
|
||||
{ name: 'b', type: 'Any', isSuspended: false, hasDefault: false },
|
||||
{ name: 'c', type: 'Any', isSuspended: false, hasDefault: false },
|
||||
{ name: 'd', type: 'Any', isSuspended: false, hasDefault: false },
|
||||
],
|
||||
documentation: [],
|
||||
isPrivate: false,
|
||||
isUnstable: false,
|
||||
aliases: [],
|
||||
annotations: [],
|
||||
const prefixFixture = {
|
||||
mockSuggestion: {
|
||||
...makeModuleMethod('local.Foo.Bar.func'),
|
||||
arguments: ['self', 'a', 'b', 'c', 'd'].map((name) => makeArgument(name)),
|
||||
},
|
||||
argsParameters: new Map<string, widgetCfg.WidgetConfiguration & widgetCfg.WithDisplay>([
|
||||
['a', { kind: 'Multi_Choice', display: widgetCfg.DisplayMode.Always }],
|
||||
['b', { kind: 'Code_Input', display: widgetCfg.DisplayMode.Always }],
|
||||
['c', { kind: 'Boolean_Input', display: widgetCfg.DisplayMode.Always }],
|
||||
]),
|
||||
methodPointer: {
|
||||
name: 'func',
|
||||
definedOnType: 'Foo.Bar',
|
||||
module: 'local.Foo.Bar',
|
||||
},
|
||||
}
|
||||
|
||||
function testArgs(paddedExpression: string, pattern: string) {
|
||||
const expression = paddedExpression.trim()
|
||||
const notAppliedArguments = pattern
|
||||
.split(' ')
|
||||
.map((p) =>
|
||||
p.startsWith('?') ? mockSuggestion.arguments.findIndex((k) => p.slice(1) === k.name) : null,
|
||||
)
|
||||
.filter(isSome)
|
||||
const infixFixture = {
|
||||
mockSuggestion: {
|
||||
...makeMethod('local.Foo.Bar.Buz.+'),
|
||||
arguments: ['lhs', 'rhs'].map((name) => makeArgument(name)),
|
||||
},
|
||||
argsParameters: new Map<string, widgetCfg.WidgetConfiguration & widgetCfg.WithDisplay>([
|
||||
['lhs', { kind: 'Multi_Choice', display: widgetCfg.DisplayMode.Always }],
|
||||
['rhs', { kind: 'Code_Input', display: widgetCfg.DisplayMode.Always }],
|
||||
]),
|
||||
methodPointer: {
|
||||
name: '+',
|
||||
definedOnType: 'local.Foo.Bar.Buz',
|
||||
module: 'local.Foo.Bar',
|
||||
},
|
||||
}
|
||||
|
||||
test(`argument list: ${paddedExpression} ${pattern}`, () => {
|
||||
const ast = Ast.parse(expression)
|
||||
interface TestData {
|
||||
expression: string
|
||||
expectedPattern: string
|
||||
fixture: typeof prefixFixture | typeof infixFixture
|
||||
}
|
||||
|
||||
test.each`
|
||||
expression | expectedPattern | fixture
|
||||
${'func '} | ${'?a ?b ?c ?d'} | ${prefixFixture}
|
||||
${'func a=x c=x '} | ${'=a ?b =c ?d'} | ${prefixFixture}
|
||||
${'func a=x x c=x '} | ${'=a @b =c ?d'} | ${prefixFixture}
|
||||
${'func a=x d=x '} | ${'=a ?b ?c =d'} | ${prefixFixture}
|
||||
${'func a=x d=x b=x '} | ${'=a =d =b ?c'} | ${prefixFixture}
|
||||
${'func a=x d=x c=x '} | ${'=a =d ?b =c'} | ${prefixFixture}
|
||||
${'func a=x c=x d=x '} | ${'=a ?b =c =d'} | ${prefixFixture}
|
||||
${'func b=x '} | ${'?a =b ?c ?d'} | ${prefixFixture}
|
||||
${'func b=x c=x '} | ${'?a =b =c ?d'} | ${prefixFixture}
|
||||
${'func b=x x x '} | ${'=b @a @c ?d'} | ${prefixFixture}
|
||||
${'func c=x b=x x '} | ${'=c =b @a ?d'} | ${prefixFixture}
|
||||
${'func d=x '} | ${'?a ?b ?c =d'} | ${prefixFixture}
|
||||
${'func d=x a c=x '} | ${'=d @a ?b =c'} | ${prefixFixture}
|
||||
${'func d=x x '} | ${'=d @a ?b ?c'} | ${prefixFixture}
|
||||
${'func d=x x '} | ${'=d @a ?b ?c'} | ${prefixFixture}
|
||||
${'func d=x x x '} | ${'=d @a @b ?c'} | ${prefixFixture}
|
||||
${'func d=x x x x '} | ${'=d @a @b @c'} | ${prefixFixture}
|
||||
${'func x '} | ${'@a ?b ?c ?d'} | ${prefixFixture}
|
||||
${'func x b=x c=x '} | ${'@a =b =c ?d'} | ${prefixFixture}
|
||||
${'func x b=x x '} | ${'@a =b @c ?d'} | ${prefixFixture}
|
||||
${'func x d=x '} | ${'@a ?b ?c =d'} | ${prefixFixture}
|
||||
${'func x x '} | ${'@a @b ?c ?d'} | ${prefixFixture}
|
||||
${'func x x x '} | ${'@a @b @c ?d'} | ${prefixFixture}
|
||||
${'func x x x x '} | ${'@a @b @c @d'} | ${prefixFixture}
|
||||
${'func a=x x m=x '} | ${'=a @b =m ?c ?d'} | ${prefixFixture}
|
||||
${'x + y'} | ${'@lhs @rhs'} | ${infixFixture}
|
||||
${'x +'} | ${'@lhs ?rhs'} | ${infixFixture}
|
||||
`(
|
||||
"Creating argument application's info: $expression $expectedPattern",
|
||||
({
|
||||
expression,
|
||||
expectedPattern,
|
||||
fixture: { mockSuggestion, argsParameters, methodPointer },
|
||||
}: TestData) => {
|
||||
const expectedArgs = expectedPattern.split(' ')
|
||||
const notAppliedArguments = expectedArgs
|
||||
.map((p: string) =>
|
||||
p.startsWith('?') ? mockSuggestion.arguments.findIndex((k) => p.slice(1) === k.name) : null,
|
||||
)
|
||||
.filter(isSome)
|
||||
|
||||
const ast = Ast.parse(expression.trim())
|
||||
|
||||
const methodCall: MethodCall = {
|
||||
methodPointer: {
|
||||
name: 'func',
|
||||
definedOnType: 'Foo.Bar',
|
||||
module: 'Foo.Bar',
|
||||
},
|
||||
methodPointer,
|
||||
notAppliedArguments,
|
||||
}
|
||||
|
||||
const funcMethodCall: MethodCall = {
|
||||
methodPointer: {
|
||||
name: 'func',
|
||||
definedOnType: 'Foo.Bar',
|
||||
module: 'Foo.Bar',
|
||||
},
|
||||
notAppliedArguments: [1, 2, 3, 4],
|
||||
methodPointer,
|
||||
notAppliedArguments: Array.from(expectedArgs, (_, i) => i + 1),
|
||||
}
|
||||
|
||||
const interpreted = ArgumentApplication.Interpret(ast, false)
|
||||
const call = ArgumentApplication.FromInterpretedWithInfo(
|
||||
interpreted,
|
||||
funcMethodCall,
|
||||
methodCall,
|
||||
mockSuggestion,
|
||||
)
|
||||
const configuration: widgetCfg.FunctionCall = {
|
||||
kind: 'FunctionCall',
|
||||
parameters: argsParameters,
|
||||
}
|
||||
|
||||
const interpreted = interpretCall(ast, true)
|
||||
const call = ArgumentApplication.FromInterpretedWithInfo(interpreted, {
|
||||
appMethodCall: methodCall,
|
||||
noArgsCall: funcMethodCall,
|
||||
suggestion: mockSuggestion,
|
||||
widgetCfg: configuration,
|
||||
})
|
||||
assert(call instanceof ArgumentApplication)
|
||||
expect(printArgPattern(call)).toEqual(pattern)
|
||||
})
|
||||
}
|
||||
expect(printArgPattern(call)).toEqual(expectedPattern)
|
||||
checkArgsConfig(call, argsParameters)
|
||||
},
|
||||
)
|
||||
|
||||
function printArgPattern(application: ArgumentApplication | Ast.Ast) {
|
||||
const parts: string[] = []
|
||||
let current: ArgumentApplication['target'] = application
|
||||
|
||||
while (current instanceof ArgumentApplication) {
|
||||
const sigil =
|
||||
current.argument instanceof ArgumentPlaceholder
|
||||
@ -78,35 +134,23 @@ function printArgPattern(application: ArgumentApplication | Ast.Ast) {
|
||||
: current.appTree instanceof Ast.App && current.appTree.argumentName
|
||||
? '='
|
||||
: '@'
|
||||
const argInfo = 'info' in current.argument ? current.argument.info : undefined
|
||||
parts.push(sigil + (argInfo?.name ?? '_'))
|
||||
parts.push(sigil + (current.argument.argInfo?.name ?? '_'))
|
||||
current = current.target
|
||||
}
|
||||
if (current instanceof ArgumentPlaceholder) parts.push(`?${current.argInfo.name}`)
|
||||
if (current instanceof ArgumentAst) parts.push(`@${current.argInfo?.name}`)
|
||||
return parts.reverse().join(' ')
|
||||
}
|
||||
|
||||
testArgs('func ', '?a ?b ?c ?d')
|
||||
testArgs('func a=x c=x ', '=a ?b =c ?d')
|
||||
testArgs('func a=x x c=x ', '=a @b =c ?d')
|
||||
testArgs('func a=x d=x ', '=a ?b ?c =d')
|
||||
testArgs('func a=x d=x b=x ', '=a =d =b ?c')
|
||||
testArgs('func a=x d=x c=x ', '=a =d ?b =c')
|
||||
testArgs('func a=x c=x d=x ', '=a ?b =c =d')
|
||||
testArgs('func b=x ', '?a =b ?c ?d')
|
||||
testArgs('func b=x c=x ', '?a =b =c ?d')
|
||||
testArgs('func b=x x x ', '=b @a @c ?d')
|
||||
testArgs('func c=x b=x x ', '=c =b @a ?d')
|
||||
testArgs('func d=x ', '?a ?b ?c =d')
|
||||
testArgs('func d=x a c=x ', '=d @a ?b =c')
|
||||
testArgs('func d=x x ', '=d @a ?b ?c')
|
||||
testArgs('func d=x x ', '=d @a ?b ?c')
|
||||
testArgs('func d=x x x ', '=d @a @b ?c')
|
||||
testArgs('func d=x x x x ', '=d @a @b @c')
|
||||
testArgs('func x ', '@a ?b ?c ?d')
|
||||
testArgs('func x b=x c=x ', '@a =b =c ?d')
|
||||
testArgs('func x b=x x ', '@a =b @c ?d')
|
||||
testArgs('func x d=x ', '@a ?b ?c =d')
|
||||
testArgs('func x x ', '@a @b ?c ?d')
|
||||
testArgs('func x x x ', '@a @b @c ?d')
|
||||
testArgs('func x x x x ', '@a @b @c @d')
|
||||
testArgs('func a=x x m=x ', '=a @b =m ?c ?d')
|
||||
function checkArgsConfig(
|
||||
application: ArgumentApplication | Ast.Ast,
|
||||
argConfig: Map<string, widgetCfg.WidgetConfiguration | widgetCfg.WithDisplay>,
|
||||
) {
|
||||
let current: ArgumentApplication['target'] = application
|
||||
while (current instanceof ArgumentApplication) {
|
||||
const argName = current.argument.argInfo?.name
|
||||
const expected = argName ? argConfig.get(argName) : undefined
|
||||
expect(current.argument.dynamicConfig).toEqual(expected)
|
||||
current = current.target
|
||||
}
|
||||
}
|
||||
|
@ -1532,11 +1532,9 @@ export function deserialize(serialized: string): Ast {
|
||||
return Ast.deserialize(serialized)
|
||||
}
|
||||
|
||||
declare const AstKey: unique symbol
|
||||
declare const TokenKey: unique symbol
|
||||
declare module '@/providers/widgetRegistry' {
|
||||
export interface WidgetInputTypes {
|
||||
[AstKey]: Ast
|
||||
[TokenKey]: Token
|
||||
}
|
||||
}
|
||||
|
@ -225,10 +225,3 @@ class AstExtendedCtx<HasIdMap extends boolean> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
declare const AstExtendedKey: unique symbol
|
||||
declare module '@/providers/widgetRegistry' {
|
||||
export interface WidgetInputTypes {
|
||||
[AstExtendedKey]: AstExtended
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import type { PortId } from '@/providers/portInfo'
|
||||
import { AnyWidget, type 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'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { tryGetIndex } from '@/util/data/array'
|
||||
@ -17,11 +20,29 @@ export class ArgumentPlaceholder {
|
||||
constructor(
|
||||
public callId: string,
|
||||
public index: number,
|
||||
public info: SuggestionEntryArgument,
|
||||
public argInfo: SuggestionEntryArgument,
|
||||
public kind: ApplicationKind,
|
||||
public insertAsNamed: boolean,
|
||||
public dynamicConfig?: WidgetConfiguration | undefined,
|
||||
) {}
|
||||
|
||||
static WithRetrievedConfig(
|
||||
callId: string,
|
||||
index: number,
|
||||
info: SuggestionEntryArgument,
|
||||
kind: ApplicationKind,
|
||||
insertAsNamed: boolean,
|
||||
functionCallConfig: widgetCfg.FunctionCall | undefined,
|
||||
) {
|
||||
const cfg =
|
||||
info != null ? functionCallConfig?.parameters.get(info.name) ?? undefined : undefined
|
||||
return new ArgumentPlaceholder(callId, index, info, kind, insertAsNamed, cfg)
|
||||
}
|
||||
|
||||
toAnyWidget(): AnyWidget {
|
||||
return new AnyWidget(this.portId, undefined, this.dynamicConfig, this.argInfo)
|
||||
}
|
||||
|
||||
get portId(): PortId {
|
||||
return `${this.callId}[${this.index}]` as PortId
|
||||
}
|
||||
@ -31,12 +52,35 @@ export class ArgumentAst {
|
||||
constructor(
|
||||
public ast: Ast.Ast,
|
||||
public index: number | undefined,
|
||||
public info: SuggestionEntryArgument | undefined,
|
||||
public argInfo: SuggestionEntryArgument | undefined,
|
||||
public kind: ApplicationKind,
|
||||
public dynamicConfig?: WidgetConfiguration | undefined,
|
||||
) {}
|
||||
|
||||
static WithRetrievedConfig(
|
||||
ast: Ast.Ast,
|
||||
index: number | undefined,
|
||||
info: SuggestionEntryArgument | undefined,
|
||||
kind: ApplicationKind,
|
||||
functionCallConfig: widgetCfg.FunctionCall | undefined,
|
||||
) {
|
||||
const cfg =
|
||||
info != null ? functionCallConfig?.parameters.get(info.name) ?? undefined : undefined
|
||||
return new ArgumentAst(ast, index, info, kind, cfg)
|
||||
}
|
||||
|
||||
toAnyWidget(): AnyWidget {
|
||||
return new AnyWidget(this.portId, this.ast, this.dynamicConfig, this.argInfo)
|
||||
}
|
||||
|
||||
get portId(): PortId {
|
||||
return this.ast.exprId as string as PortId
|
||||
return this.ast.exprId
|
||||
}
|
||||
|
||||
static matchWithArgInfo(
|
||||
input: WidgetInput,
|
||||
): input is ArgumentAst & { argInfo: SuggestionEntryArgument } {
|
||||
return input instanceof ArgumentAst && input.argInfo != null
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,78 +106,87 @@ interface FoundApplication {
|
||||
argName: string | undefined
|
||||
}
|
||||
|
||||
export function interpretCall(callRoot: Ast.Ast, allowInterpretAsInfix: boolean): InterpretedCall {
|
||||
if (allowInterpretAsInfix && callRoot instanceof Ast.OprApp) {
|
||||
// Infix chains are handled one level at a time. Each application may have at most 2 arguments.
|
||||
return {
|
||||
kind: 'infix',
|
||||
appTree: callRoot,
|
||||
operator: callRoot.operator.ok ? callRoot.operator.value : undefined,
|
||||
lhs: callRoot.lhs ?? undefined,
|
||||
rhs: callRoot.rhs ?? undefined,
|
||||
}
|
||||
} else {
|
||||
// Prefix chains are handled all at once, as they may have arbitrary number of arguments.
|
||||
const foundApplications: FoundApplication[] = []
|
||||
let nextApplication = callRoot
|
||||
// Traverse the AST and find all arguments applied in sequence to the same function.
|
||||
while (nextApplication instanceof Ast.App) {
|
||||
foundApplications.push({
|
||||
appTree: nextApplication,
|
||||
argument: nextApplication.argument,
|
||||
argName: nextApplication.argumentName?.code() ?? undefined,
|
||||
})
|
||||
nextApplication = nextApplication.function
|
||||
}
|
||||
return {
|
||||
kind: 'prefix',
|
||||
func: nextApplication,
|
||||
// The applications are peeled away from outer to inner, so arguments are in reverse order. We
|
||||
// need to reverse them back to match them with the order in suggestion entry.
|
||||
args: foundApplications.reverse(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CallInfo {
|
||||
noArgsCall?: MethodCall | undefined
|
||||
appMethodCall?: MethodCall | undefined
|
||||
suggestion?: SuggestionEntry | undefined
|
||||
widgetCfg?: widgetCfg.FunctionCall | undefined
|
||||
}
|
||||
|
||||
export class ArgumentApplication {
|
||||
private constructor(
|
||||
public appTree: Ast.Ast,
|
||||
public target: ArgumentApplication | Ast.Ast | ArgumentPlaceholder | ArgumentAst,
|
||||
public infixOperator: Ast.Token | undefined,
|
||||
public argument: Ast.Ast | ArgumentAst | ArgumentPlaceholder,
|
||||
public argument: ArgumentAst | ArgumentPlaceholder,
|
||||
) {}
|
||||
|
||||
static Interpret(callRoot: Ast.Ast, allowInterpretAsInfix: boolean): InterpretedCall {
|
||||
if (allowInterpretAsInfix && callRoot instanceof Ast.OprApp) {
|
||||
// Infix chains are handled one level at a time. Each application may have at most 2 arguments.
|
||||
return {
|
||||
kind: 'infix',
|
||||
appTree: callRoot,
|
||||
operator: callRoot.operator.ok ? callRoot.operator.value : undefined,
|
||||
lhs: callRoot.lhs ?? undefined,
|
||||
rhs: callRoot.rhs ?? undefined,
|
||||
}
|
||||
} else {
|
||||
// Prefix chains are handled all at once, as they may have arbitrary number of arguments.
|
||||
const foundApplications: FoundApplication[] = []
|
||||
let nextApplication = callRoot
|
||||
// Traverse the AST and find all arguments applied in sequence to the same function.
|
||||
while (nextApplication instanceof Ast.App) {
|
||||
foundApplications.push({
|
||||
appTree: nextApplication,
|
||||
argument: nextApplication.argument,
|
||||
argName: nextApplication.argumentName?.code() ?? undefined,
|
||||
})
|
||||
nextApplication = nextApplication.function
|
||||
}
|
||||
return {
|
||||
kind: 'prefix',
|
||||
func: nextApplication,
|
||||
// The applications are peeled away from outer to inner, so arguments are in reverse order. We
|
||||
// need to reverse them back to match them with the order in suggestion entry.
|
||||
args: foundApplications.reverse(),
|
||||
}
|
||||
private static FromInterpretedInfix(interpreted: InterpretedInfix, callInfo: CallInfo) {
|
||||
// Access infixes are not real infix calls.
|
||||
if (isAccessOperator(interpreted.operator)) return interpreted.appTree
|
||||
const { suggestion, widgetCfg } = callInfo
|
||||
|
||||
const kind = ApplicationKind.Infix
|
||||
const callId = interpreted.appTree.exprId
|
||||
|
||||
const argFor = (key: 'lhs' | 'rhs', index: number) => {
|
||||
const tree = interpreted[key]
|
||||
const info = tryGetIndex(suggestion?.arguments, index) ?? unknownArgInfoNamed(key)
|
||||
return tree != null
|
||||
? ArgumentAst.WithRetrievedConfig(tree, index, info, kind, widgetCfg)
|
||||
: ArgumentPlaceholder.WithRetrievedConfig(callId, index, info, kind, false, widgetCfg)
|
||||
}
|
||||
return new ArgumentApplication(
|
||||
interpreted.appTree,
|
||||
argFor('lhs', 0),
|
||||
interpreted.operator,
|
||||
argFor('rhs', 1),
|
||||
)
|
||||
}
|
||||
|
||||
static FromInterpretedWithInfo(
|
||||
interpreted: InterpretedCall,
|
||||
noArgsCall: MethodCall | undefined,
|
||||
appMethodCall: MethodCall | undefined,
|
||||
suggestion: SuggestionEntry | undefined,
|
||||
stripSelfArgument: boolean = false,
|
||||
): ArgumentApplication | Ast.Ast {
|
||||
private static FromInterpretedPrefix(
|
||||
interpreted: InterpretedPrefix,
|
||||
callInfo: CallInfo,
|
||||
stripSelfArgument: boolean,
|
||||
) {
|
||||
const { noArgsCall, appMethodCall, suggestion, widgetCfg } = callInfo
|
||||
const kind = ApplicationKind.Prefix
|
||||
const callId = interpreted.func.exprId
|
||||
|
||||
const knownArguments = suggestion?.arguments
|
||||
|
||||
const callId =
|
||||
interpreted.kind === 'infix' ? interpreted.appTree.exprId : interpreted.func.exprId
|
||||
|
||||
if (interpreted.kind === 'infix') {
|
||||
const isAccess = isAccessOperator(interpreted.operator)
|
||||
const argFor = (key: 'lhs' | 'rhs', index: number) => {
|
||||
const tree = interpreted[key]
|
||||
const info = tryGetIndex(knownArguments, index) ?? unknownArgInfoNamed(key)
|
||||
return tree != null
|
||||
? isAccess
|
||||
? tree
|
||||
: new ArgumentAst(tree, index, info, ApplicationKind.Infix)
|
||||
: new ArgumentPlaceholder(callId, index, info, ApplicationKind.Infix, false)
|
||||
}
|
||||
return new ArgumentApplication(
|
||||
interpreted.appTree,
|
||||
argFor('lhs', 0),
|
||||
interpreted.operator,
|
||||
argFor('rhs', 1),
|
||||
)
|
||||
}
|
||||
|
||||
const notAppliedArguments = appMethodCall?.notAppliedArguments ?? []
|
||||
const placeholdersToInsert = notAppliedArguments.slice()
|
||||
const notAppliedSet = new Set(notAppliedArguments)
|
||||
@ -168,12 +221,13 @@ export class ArgumentApplication {
|
||||
if (argIndex != null && argInfo != null) {
|
||||
prefixArgsToDisplay.push({
|
||||
appTree,
|
||||
argument: new ArgumentPlaceholder(
|
||||
argument: ArgumentPlaceholder.WithRetrievedConfig(
|
||||
callId,
|
||||
argIndex,
|
||||
argInfo,
|
||||
ApplicationKind.Prefix,
|
||||
!canInsertPositional,
|
||||
widgetCfg,
|
||||
),
|
||||
})
|
||||
|
||||
@ -188,13 +242,12 @@ export class ArgumentApplication {
|
||||
if (argIndex != null) insertPlaceholdersUpto(argIndex, realArg.appTree.function)
|
||||
prefixArgsToDisplay.push({
|
||||
appTree: realArg.appTree,
|
||||
argument: new ArgumentAst(
|
||||
argument: ArgumentAst.WithRetrievedConfig(
|
||||
realArg.argument,
|
||||
argIndex,
|
||||
// If we have more arguments applied than we know about, display that argument anyway, but
|
||||
// mark it as unknown.
|
||||
tryGetIndex(knownArguments, argIndex),
|
||||
ApplicationKind.Prefix,
|
||||
kind,
|
||||
widgetCfg,
|
||||
),
|
||||
})
|
||||
} else {
|
||||
@ -203,15 +256,17 @@ export class ArgumentApplication {
|
||||
const name = realArg.argName
|
||||
const foundIdx = argumentsLeftToMatch.findIndex((i) => knownArguments?.[i]?.name === name)
|
||||
const argIndex = foundIdx === -1 ? undefined : argumentsLeftToMatch.splice(foundIdx, 1)[0]
|
||||
|
||||
if (argIndex != null && foundIdx === 0)
|
||||
insertPlaceholdersUpto(argIndex, realArg.appTree.function)
|
||||
prefixArgsToDisplay.push({
|
||||
appTree: realArg.appTree,
|
||||
argument: new ArgumentAst(
|
||||
argument: ArgumentAst.WithRetrievedConfig(
|
||||
realArg.argument,
|
||||
argIndex,
|
||||
tryGetIndex(knownArguments, argIndex) ?? unknownArgInfoNamed(name),
|
||||
ApplicationKind.Prefix,
|
||||
kind,
|
||||
widgetCfg,
|
||||
),
|
||||
})
|
||||
}
|
||||
@ -227,6 +282,18 @@ export class ArgumentApplication {
|
||||
)
|
||||
}
|
||||
|
||||
static FromInterpretedWithInfo(
|
||||
interpreted: InterpretedCall,
|
||||
callInfo: CallInfo = {},
|
||||
stripSelfArgument: boolean = false,
|
||||
): ArgumentApplication | Ast.Ast {
|
||||
if (interpreted.kind === 'infix') {
|
||||
return ArgumentApplication.FromInterpretedInfix(interpreted, callInfo)
|
||||
} else {
|
||||
return ArgumentApplication.FromInterpretedPrefix(interpreted, callInfo, stripSelfArgument)
|
||||
}
|
||||
}
|
||||
|
||||
*iterApplications(): IterableIterator<ArgumentApplication> {
|
||||
let current: typeof this.target = this
|
||||
while (current instanceof ArgumentApplication) {
|
||||
@ -238,12 +305,12 @@ export class ArgumentApplication {
|
||||
|
||||
const unknownArgInfoNamed = (name: string) => ({
|
||||
name,
|
||||
type: 'Any',
|
||||
reprType: 'Any',
|
||||
isSuspended: false,
|
||||
hasDefault: false,
|
||||
})
|
||||
|
||||
function getAccessOprSubject(app: Ast.Ast): Ast.Ast | undefined {
|
||||
export function getAccessOprSubject(app: Ast.Ast): Ast.Ast | undefined {
|
||||
if (app instanceof Ast.PropertyAccess) return app.lhs ?? undefined
|
||||
}
|
||||
|
||||
@ -252,8 +319,8 @@ function isAccessOperator(opr: Ast.Token | undefined): boolean {
|
||||
}
|
||||
|
||||
declare const ArgumentApplicationKey: unique symbol
|
||||
declare const ArgumentPlaceholderKey: unique symbol
|
||||
declare const ArgumentAstKey: unique symbol
|
||||
declare const ArgumentPlaceholderKey: unique symbol
|
||||
declare module '@/providers/widgetRegistry' {
|
||||
export interface WidgetInputTypes {
|
||||
[ArgumentApplicationKey]: ArgumentApplication
|
||||
|
@ -451,7 +451,7 @@ interface SuggestionEntryArgument {
|
||||
/** The argument name. */
|
||||
name: string;
|
||||
/** The argument type. String 'Any' is used to specify generic types. */
|
||||
type: string;
|
||||
reprType: string;
|
||||
/** Indicates whether the argument is lazy. */
|
||||
isSuspended: boolean;
|
||||
/** Indicates whether the argument has default value. */
|
||||
|
Loading…
Reference in New Issue
Block a user