Fix missing dropdowns in constructor subexpressions (#10382)

Fixes #10341

<img width="487" alt="image" src="https://github.com/enso-org/enso/assets/919491/af946c1c-a27f-4ed8-8346-b5098e7d5f08">

Also added a new version of vue devtools that is embedded into the dev app itself, and has much better performance than the browser plugin.
This commit is contained in:
Paweł Grabarz 2024-06-27 17:11:28 +02:00 committed by GitHub
parent db4f7ab3b5
commit 2f7adb9deb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1005 additions and 1644 deletions

View File

@ -139,8 +139,8 @@
"tsx": "^4.7.1",
"typescript": "~5.2.2",
"unbzip2-stream": "^1.4.3",
"vite": "^4.4.9",
"vite-plugin-inspect": "^0.7.38",
"vite": "^5.3.1",
"vite-plugin-vue-devtools": "7.3.4",
"vitest": "^1.3.1",
"vue-react-wrapper": "^0.3.1",
"vue-tsc": "^1.8.27"

View File

@ -40,22 +40,24 @@ const staticMethod = {
}
test.each`
code | callSuggestion | subjectSpan | selfSpan | subjectType | methodName
${'val1.method val2'} | ${method} | ${[0, 4]} | ${[0, 4]} | ${'local.Project.Type'} | ${'.method'}
${'local.Project.Type.method val1 val2'} | ${method} | ${[0, 18]} | ${[26, 30]} | ${'local.Project.Type.type'} | ${'.method'}
${'Type.method val1'} | ${method} | ${[0, 4]} | ${[12, 16]} | ${'local.Project.Type.type'} | ${'.method'}
${'local.Project.Type.method'} | ${method} | ${[0, 18]} | ${[0, 18]} | ${'local.Project.Type.type'} | ${'.method'}
${'local.Project.Type.static_method val1'} | ${staticMethod} | ${[0, 18]} | ${[0, 18]} | ${'local.Project.Type.type'} | ${'.static_method'}
${'Type.Con val1'} | ${con} | ${[0, 4]} | ${[0, 4]} | ${'local.Project.Type.type'} | ${'.Con'}
${'..Con val1'} | ${con} | ${null} | ${null} | ${null} | ${'.Con'}
${'local.Project.module_method val1'} | ${moduleMethod} | ${[0, 13]} | ${[0, 13]} | ${'local.Project'} | ${'.module_method'}
code | callSuggestion | subjectSpan | attachedSpan | subjectType | methodName
${'val1.method val2'} | ${method} | ${[0, 4]} | ${[0, 4]} | ${'local.Project.Type'} | ${'.method'}
${'local.Project.Type.method val1 val2'} | ${method} | ${[0, 18]} | ${[26, 30]} | ${'local.Project.Type.type'} | ${'.method'}
${'Type.method val1'} | ${method} | ${[0, 4]} | ${[12, 16]} | ${'local.Project.Type.type'} | ${'.method'}
${'local.Project.Type.method'} | ${method} | ${[0, 18]} | ${null} | ${'local.Project.Type.type'} | ${'.method'}
${'foo.method'} | ${method} | ${[0, 3]} | ${null} | ${'local.Project.Type.type'} | ${'.method'}
${'foo.method'} | ${method} | ${[0, 3]} | ${[0, 3]} | ${'local.Project.Type'} | ${'.method'}
${'local.Project.Type.static_method val1'} | ${staticMethod} | ${[0, 18]} | ${[0, 18]} | ${'local.Project.Type.type'} | ${'.static_method'}
${'Type.Con val1'} | ${con} | ${[0, 4]} | ${[0, 4]} | ${'local.Project.Type.type'} | ${'.Con'}
${'..Con val1'} | ${con} | ${null} | ${null} | ${null} | ${'.Con'}
${'local.Project.module_method val1'} | ${moduleMethod} | ${[0, 13]} | ${[0, 13]} | ${'local.Project'} | ${'.module_method'}
`(
'Visualization config for $code',
({ code, callSuggestion, subjectSpan, selfSpan, subjectType, methodName }) => {
({ code, callSuggestion, subjectSpan, attachedSpan, subjectType, methodName }) => {
const spans = {
entireFunction: [0, code.length] as [number, number],
...(subjectSpan != null ? { subject: subjectSpan as [number, number] } : {}),
...(selfSpan != null ? { self: selfSpan as [number, number] } : {}),
...(attachedSpan != null ? { attached: attachedSpan as [number, number] } : {}),
}
const { ast, eid, id } = parseWithSpans(code, spans)
const line = ast.lines[0]?.expression
@ -101,10 +103,11 @@ test.each`
if (typeof visConfig.value.expression === 'string') {
expect(visConfig.value.expressionId).toBe(eid('entireFunction'))
expect(visConfig.value.expression).toBe(
`_ -> ${WIDGETS_ENSO_MODULE}.${GET_WIDGETS_METHOD} ${callSuggestion.definedIn}`,
`_ -> ${WIDGETS_ENSO_MODULE}.${GET_WIDGETS_METHOD} ${callSuggestion.memberOf}`,
)
expect(eid('attached')).toBeUndefined()
} else {
expect(visConfig.value.expressionId).toBe(eid('self'))
expect(visConfig.value.expressionId).toBe(eid('attached'))
}
expect(visConfig.value.positionalArgumentsExpressions![0]).toBe(methodName)
expect(visConfig.value.positionalArgumentsExpressions![1]).toBe("['arg']")

View File

@ -38,13 +38,8 @@ export function useWidgetFunctionCallInfo(
useVisualizationData(config: Ref<Opt<NodeVisualizationConfiguration>>): Ref<Result<any> | null>
},
) {
const methodCallInfo = computed(() => {
return getMethodCallInfoRecursively(toValue(input).value, graphDb)
})
const interpreted = computed(() => {
return interpretCall(toValue(input).value, methodCallInfo.value == null)
})
const methodCallInfo = computed(() => getMethodCallInfoRecursively(toValue(input).value, graphDb))
const interpreted = computed(() => interpretCall(toValue(input).value))
const subjectInfo = computed(() => {
const analyzed = interpreted.value
@ -60,22 +55,31 @@ export function useWidgetFunctionCallInfo(
return funcType != null && subjectInfo.value?.typename !== `${funcType}.type`
})
const selfArgumentExternalId = computed<Opt<ExternalId>>(() => {
const widgetQuerySubjectExpressionId = computed<Opt<ExternalId>>(() => {
const analyzed = interpreted.value
if (analyzed.kind === 'infix') {
return analyzed.lhs?.externalId
} else if (methodCallInfo.value?.suggestion.selfType != null) {
const knownArguments = methodCallInfo.value?.suggestion?.arguments
const hasSelfArgument = knownArguments?.[0]?.name === 'self'
const selfArgument =
hasSelfArgument && !selfArgumentPreapplied.value ?
analyzed.args.find((a) => a.argName === 'self' || a.argName == null)?.argument
: getAccessOprSubject(analyzed.func) ?? analyzed.args[0]?.argument
return selfArgument?.externalId
} else {
return null
}
const knownArguments = methodCallInfo.value?.suggestion?.arguments
const hasKnownSelfArgument = knownArguments?.[0]?.name === 'self'
// First we always want to attach the visualization to the `self` argument,
// whenever we can find an unambiguous expression for it.
if (hasKnownSelfArgument && !selfArgumentPreapplied.value) {
return analyzed.args.find((a) => a.argName === 'self' || a.argName == null)?.argument
?.externalId
}
// When no `self` argument can be resolved or it is already applied, attach to the access
// chain subject. This will correctly handle constructors and most common cases with not
// yet resolved methods.
const accessSubject = getAccessOprSubject(analyzed.func)
if (accessSubject) {
return accessSubject.externalId
}
// In other cases (e.g. autoscoped expression) there is no good existing
// expression to attach the visualization to. Fallback to synthetic type-based expression.
return null
})
const visualizationConfig = computed<Opt<NodeVisualizationConfiguration>>(() => {
@ -84,7 +88,6 @@ export function useWidgetFunctionCallInfo(
methodCallInfo.value,
)
const selfArgId = selfArgumentExternalId.value
const info = methodCallInfo.value
if (!info) return null
const annotatedArgs = info.suggestion.annotations
@ -95,9 +98,11 @@ export function useWidgetFunctionCallInfo(
Ast.Vector.build(annotatedArgs, Ast.TextLiteral.new).code(),
Ast.TextLiteral.new(JSON.stringify(args)).code(),
]
if (selfArgId != null) {
const expressionId = widgetQuerySubjectExpressionId.value
if (expressionId != null) {
return {
expressionId: selfArgId,
expressionId,
visualizationModule: WIDGETS_ENSO_MODULE,
expression: {
module: WIDGETS_ENSO_MODULE,
@ -107,12 +112,12 @@ export function useWidgetFunctionCallInfo(
positionalArgumentsExpressions,
}
} else {
// In the case when no self argument is present (for example in autoscoped constructor),
// we assume that this is a static function call.
// In the case when no clear subject expression exists (for example in autoscoped constructor),
// we assume that this is a static function call and create the subject by using resolved type name.
return {
expressionId: toValue(input).value.externalId,
visualizationModule: WIDGETS_ENSO_MODULE,
expression: `_ -> ${WIDGETS_ENSO_MODULE}.${GET_WIDGETS_METHOD} ${info.suggestion.definedIn}`,
expression: `_ -> ${WIDGETS_ENSO_MODULE}.${GET_WIDGETS_METHOD} ${info.suggestion.memberOf ?? info.suggestion.definedIn}`,
positionalArgumentsExpressions,
}
}

View File

@ -73,7 +73,7 @@ export interface DropdownEntry {
<template>
<div class="DropdownWidget" :style="styleVars">
<ul class="list scrollable" @wheel.stop>
<ul class="list scrollable" @wheel.stop.passive>
<li
v-for="entry in sortedValues"
:key="entry.value"

View File

@ -157,8 +157,8 @@ interface FoundApplication {
argName: string | undefined
}
export function interpretCall(callRoot: Ast.Ast, allowInterpretAsInfix: boolean): InterpretedCall {
if (allowInterpretAsInfix && callRoot instanceof Ast.OprApp) {
export function interpretCall(callRoot: Ast.Ast): InterpretedCall {
if (callRoot instanceof Ast.OprApp) {
// Infix chains are handled one level at a time. Each application may have at most 2 arguments.
return {
kind: 'infix',

View File

@ -8,6 +8,7 @@ import postcssNesting from 'postcss-nesting'
import tailwindcss from 'tailwindcss'
import tailwindcssNesting from 'tailwindcss/nesting'
import { defineConfig, type Plugin } from 'vite'
import VueDevTools from 'vite-plugin-vue-devtools'
// @ts-expect-error
import * as tailwindConfig from 'enso-dashboard/tailwind.config'
import { createGatewayServer } from './ydoc-server'
@ -27,6 +28,7 @@ export default defineConfig({
publicDir: fileURLToPath(new URL('./public', import.meta.url)),
envDir: fileURLToPath(new URL('.', import.meta.url)),
plugins: [
VueDevTools(),
vue(),
react({
include: fileURLToPath(new URL('../ide-desktop/lib/dashboard/**/*.tsx', import.meta.url)),

View File

@ -44,7 +44,7 @@
"sharp": "^0.31.2",
"to-ico": "^1.1.5",
"tsx": "^4.7.1",
"vite": "^5.1.4"
"vite": "^5.3.1"
},
"optionalDependencies": {
"@esbuild/darwin-x64": "^0.17.15",

View File

@ -95,7 +95,7 @@
"tailwindcss-react-aria-components": "^1.1.1",
"ts-plugin-namespace-auto-import": "^1.0.0",
"typescript": "~5.2.2",
"vite": "^4.4.9",
"vite": "^5.3.1",
"vitest": "^1.3.1"
},
"optionalDependencies": {

2549
package-lock.json generated

File diff suppressed because it is too large Load Diff