mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 10:42:05 +03:00
improve argument placeholder resolution for partial applications and … (#8794)
Fixes #8788 - Fixed missing argument lists on constructors, and improved handling for various cases of partially applied functions. - Extended tests to check for correct `self` argument placeholders. - Additionally reworked some questionable test code to maintain separation between server and client code. <img width="1241" alt="image" src="https://github.com/enso-org/enso/assets/919491/5377f57f-18f0-4a50-a8ab-9331862ca547">
This commit is contained in:
parent
14be36c401
commit
48a5599eb6
@ -5,9 +5,34 @@
|
||||
* - System validation dialogs are not reliable between computers, as they may have different
|
||||
* default fonts. */
|
||||
import { defineConfig } from '@playwright/test'
|
||||
import net from 'net'
|
||||
|
||||
const DEBUG = process.env.DEBUG_E2E === 'true'
|
||||
|
||||
async function findFreePortInRange(min: number, max: number) {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const portToCheck = Math.floor(Math.random() * (max - min + 1)) + min
|
||||
if (await checkAvailablePort(portToCheck)) return portToCheck
|
||||
}
|
||||
throw new Error('Failed to find a free port.')
|
||||
}
|
||||
|
||||
function checkAvailablePort(port: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer()
|
||||
server
|
||||
.unref()
|
||||
.on('error', (e: any) => ('EADDRINUSE' === e.code ? resolve(false) : reject(e)))
|
||||
.listen({ host: '0.0.0.0', port }, () => server.close(() => resolve(true)))
|
||||
})
|
||||
}
|
||||
|
||||
const portFromEnv = parseInt(process.env.PLAYWRIGHT_PORT ?? '', 10)
|
||||
const PORT = Number.isFinite(portFromEnv) ? portFromEnv : await findFreePortInRange(4300, 4999)
|
||||
// Make sure to set the env to actual port that is being used. This is necessary for workers to
|
||||
// pick up the same configuration.
|
||||
process.env.PLAYWRIGHT_PORT = `${PORT}`
|
||||
|
||||
export default defineConfig({
|
||||
globalSetup: './e2e/setup.ts',
|
||||
testDir: './e2e',
|
||||
@ -21,7 +46,6 @@ export default defineConfig({
|
||||
},
|
||||
use: {
|
||||
headless: !DEBUG,
|
||||
baseURL: 'http://localhost:4173',
|
||||
trace: 'on-first-retry',
|
||||
...(DEBUG
|
||||
? {}
|
||||
@ -85,8 +109,10 @@ export default defineConfig({
|
||||
env: {
|
||||
E2E: 'true',
|
||||
},
|
||||
command: 'npx vite build && npx vite preview',
|
||||
port: 4173,
|
||||
command: `npx vite build && npx vite preview --port ${PORT} --strictPort`,
|
||||
// Build from scratch apparently can take a while on CI machines.
|
||||
timeout: 120 * 1000,
|
||||
port: PORT,
|
||||
// We use our special, mocked version of server, thus do not want to re-use user's one.
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
|
28
app/gui2/shared/__tests__/ensoFile.test.ts
Normal file
28
app/gui2/shared/__tests__/ensoFile.test.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { combineFileParts, splitFileContents } from 'shared/ensoFile'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
const cases = [
|
||||
`foo`,
|
||||
`foo
|
||||
|
||||
|
||||
#### METADATA ####
|
||||
[]
|
||||
{}`,
|
||||
`a "#### METADATA ####"`,
|
||||
`a
|
||||
|
||||
|
||||
#### METADATA ####
|
||||
|
||||
|
||||
#### METADATA ####
|
||||
[]
|
||||
{}`,
|
||||
]
|
||||
|
||||
test.each(cases)('File split and combine roundtrip $#', (contents) => {
|
||||
const parts = splitFileContents(contents)
|
||||
const combined = combineFileParts(parts)
|
||||
expect(combined).toEqual(contents)
|
||||
})
|
38
app/gui2/shared/ensoFile.ts
Normal file
38
app/gui2/shared/ensoFile.ts
Normal file
@ -0,0 +1,38 @@
|
||||
const META_TAG = '\n\n\n#### METADATA ####'
|
||||
|
||||
export interface EnsoFileParts {
|
||||
code: string
|
||||
idMapJson: string | null
|
||||
metadataJson: string | null
|
||||
}
|
||||
|
||||
export function splitFileContents(content: string): EnsoFileParts {
|
||||
const splitPoint = content.lastIndexOf(META_TAG)
|
||||
if (splitPoint < 0) {
|
||||
return {
|
||||
code: content,
|
||||
idMapJson: null,
|
||||
metadataJson: null,
|
||||
}
|
||||
}
|
||||
const code = content.slice(0, splitPoint)
|
||||
const metadataString = content.slice(splitPoint + META_TAG.length)
|
||||
const metaLines = metadataString.trim().split('\n')
|
||||
const idMapJson = metaLines[0] ?? null
|
||||
const metadataJson = metaLines[1] ?? null
|
||||
return { code, idMapJson, metadataJson }
|
||||
}
|
||||
|
||||
export function combineFileParts(parts: EnsoFileParts): string {
|
||||
const hasMeta = parts.idMapJson != null || parts.metadataJson != null
|
||||
if (hasMeta) {
|
||||
return `${parts.code}${META_TAG}\n${parts.idMapJson ?? ''}\n${parts.metadataJson ?? ''}`
|
||||
} else {
|
||||
// If code segment contains meta tag, add another one to make sure it is not misinterpreted.
|
||||
if (parts.code.includes(META_TAG)) {
|
||||
return `${parts.code}${META_TAG}`
|
||||
} else {
|
||||
return parts.code
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,8 @@ import { provideGuiConfig } from '@/providers/guiConfig'
|
||||
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||
import { configValue, type ApplicationConfig, type ApplicationConfigValue } from '@/util/config'
|
||||
import ProjectView from '@/views/ProjectView.vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, toRaw } from 'vue'
|
||||
import { isDevMode } from './util/detect'
|
||||
|
||||
const props = defineProps<{
|
||||
config: ApplicationConfig
|
||||
@ -19,7 +20,12 @@ const classSet = provideAppClassSet()
|
||||
provideGuiConfig(computed((): ApplicationConfigValue => configValue(props.config)))
|
||||
|
||||
// Initialize suggestion db immediately, so it will be ready when user needs it.
|
||||
onMounted(() => useSuggestionDbStore())
|
||||
onMounted(() => {
|
||||
const suggestionDb = useSuggestionDbStore()
|
||||
if (isDevMode) {
|
||||
;(window as any).suggestionDb = toRaw(suggestionDb.entries)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { GraphDb } from '@/stores/graph/graphDatabase'
|
||||
import { assert } from '@/util/assert'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { moduleMethodNames } from '@/util/ast/abstract'
|
||||
import { unwrap } from '@/util/data/result'
|
||||
import { tryIdentifier, type Identifier } from '@/util/qualifiedName'
|
||||
import assert from 'assert'
|
||||
import * as set from 'lib0/set'
|
||||
import { IdMap, type ExprId } from 'shared/yjsModel'
|
||||
|
||||
|
@ -47,16 +47,23 @@ const interpreted = computed(() => {
|
||||
return interpretCall(props.input.value, methodCallInfo.value == null)
|
||||
})
|
||||
|
||||
const subjectInfo = computed(() => {
|
||||
const analyzed = interpreted.value
|
||||
if (analyzed.kind !== 'prefix') return
|
||||
const subject = getAccessOprSubject(analyzed.func)
|
||||
if (!subject) return
|
||||
return graph.db.getExpressionInfo(subject.exprId)
|
||||
})
|
||||
|
||||
const selfArgumentPreapplied = computed(() => {
|
||||
const info = methodCallInfo.value
|
||||
if (info?.staticallyApplied) return false
|
||||
const analyzed = interpreted.value
|
||||
if (analyzed.kind !== 'prefix') return false
|
||||
const subject = getAccessOprSubject(analyzed.func)
|
||||
if (!subject) return false
|
||||
const funcType = info?.methodCall.methodPointer.definedOnType
|
||||
const subjectInfo = graph.db.getExpressionInfo(subject.exprId)
|
||||
return funcType != null && subjectInfo?.typename !== `${funcType}.type`
|
||||
return funcType != null && subjectInfo.value?.typename !== `${funcType}.type`
|
||||
})
|
||||
|
||||
const subjectTypeMatchesMethod = computed(() => {
|
||||
const funcType = methodCallInfo.value?.methodCall.methodPointer.definedOnType
|
||||
return funcType != null && subjectInfo.value?.typename === `${funcType}.type`
|
||||
})
|
||||
|
||||
const application = computed(() => {
|
||||
@ -64,17 +71,16 @@ const application = computed(() => {
|
||||
if (!call) return null
|
||||
const noArgsCall = call.kind === 'prefix' ? graph.db.getMethodCall(call.func.exprId) : undefined
|
||||
|
||||
const info = methodCallInfo.value
|
||||
return ArgumentApplication.FromInterpretedWithInfo(
|
||||
call,
|
||||
{
|
||||
noArgsCall,
|
||||
appMethodCall: info?.methodCall,
|
||||
suggestion: info?.suggestion,
|
||||
widgetCfg: widgetConfiguration.value,
|
||||
},
|
||||
selfArgumentPreapplied.value,
|
||||
)
|
||||
return ArgumentApplication.FromInterpretedWithInfo(call, {
|
||||
suggestion: methodCallInfo.value?.suggestion,
|
||||
widgetCfg: widgetConfiguration.value,
|
||||
subjectAsSelf: selfArgumentPreapplied.value,
|
||||
notAppliedArguments:
|
||||
noArgsCall != null &&
|
||||
(!subjectTypeMatchesMethod.value || noArgsCall.notAppliedArguments.length > 0)
|
||||
? noArgsCall.notAppliedArguments
|
||||
: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
const innerInput = computed(() => {
|
||||
@ -298,7 +304,7 @@ export const widgetDefinition = defineWidget(WidgetInput.isFunctionCall, {
|
||||
if (ast instanceof Ast.App || ast instanceof Ast.OprApp) return Score.Perfect
|
||||
|
||||
const info = db.getMethodCallInfo(ast.exprId)
|
||||
if (prevFunctionState != null && info?.staticallyApplied === true && ast instanceof Ast.Ident) {
|
||||
if (prevFunctionState != null && info?.partiallyApplied === true && ast instanceof Ast.Ident) {
|
||||
return Score.Mismatch
|
||||
}
|
||||
return info != null ? Score.Perfect : Score.Mismatch
|
||||
|
@ -1,9 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { useVisualizationStore } from '@/stores/visualization'
|
||||
import { useAutoBlur } from '@/util/autoBlur'
|
||||
import type { URLString } from '@/util/data/urlString'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
import { computedAsync } from '@vueuse/core'
|
||||
import { visIdentifierEquals, type VisualizationIdentifier } from 'shared/yjsModel'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
@ -13,21 +11,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
const emit = defineEmits<{ hide: []; 'update:modelValue': [type: VisualizationIdentifier] }>()
|
||||
|
||||
// This dynamic import is required to break the circular import chain:
|
||||
// `VisualizationSelector.vue` -> `compilerMessaging.ts` -> `VisualizationContainer.vue` ->
|
||||
// `VisualizationSelector.vue`
|
||||
const visualizationStore = computedAsync<{
|
||||
icon: (type: VisualizationIdentifier) => Icon | URLString | undefined
|
||||
}>(
|
||||
async () => {
|
||||
return (await import('@/stores/visualization')).useVisualizationStore()
|
||||
},
|
||||
{
|
||||
icon() {
|
||||
return 'columns_increasing'
|
||||
},
|
||||
},
|
||||
)
|
||||
const visualizationStore = useVisualizationStore()
|
||||
|
||||
const rootNode = ref<HTMLElement>()
|
||||
useAutoBlur(rootNode)
|
||||
|
@ -268,7 +268,7 @@ export class GraphDb {
|
||||
getMethodCallInfo(
|
||||
id: ExprId,
|
||||
):
|
||||
| { methodCall: MethodCall; suggestion: SuggestionEntry; staticallyApplied: boolean }
|
||||
| { methodCall: MethodCall; suggestion: SuggestionEntry; partiallyApplied: boolean }
|
||||
| undefined {
|
||||
const info = this.getExpressionInfo(id)
|
||||
if (info == null) return
|
||||
@ -280,8 +280,8 @@ export class GraphDb {
|
||||
if (suggestionId == null) return
|
||||
const suggestion = this.suggestionDb.get(suggestionId)
|
||||
if (suggestion == null) return
|
||||
const staticallyApplied = mathodCallEquals(methodCall, payloadFuncSchema)
|
||||
return { methodCall, suggestion, staticallyApplied }
|
||||
const partiallyApplied = mathodCallEquals(methodCall, payloadFuncSchema)
|
||||
return { methodCall, suggestion, partiallyApplied }
|
||||
}
|
||||
|
||||
getNodeColorStyle(id: ExprId): string {
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { assert } from '@/util/assert'
|
||||
import { Ast } from '@/util/ast'
|
||||
import * as fs from 'fs'
|
||||
import { expect, test } from 'vitest'
|
||||
import { preParseContent } from '../../../../ydoc-server/edits'
|
||||
import { deserializeIdMap, serializeIdMap } from '../../../../ydoc-server/serialization'
|
||||
import { MutableModule } from '../abstract'
|
||||
|
||||
//const disabledCases = [
|
||||
@ -458,33 +455,6 @@ test('Delete expression', () => {
|
||||
expect(printed).toEqual('main =\n text1 = "foo"\n')
|
||||
})
|
||||
|
||||
test('full file IdMap round trip', () => {
|
||||
const content = fs.readFileSync(__dirname + '/fixtures/stargazers.enso').toString()
|
||||
const { code, idMapJson, metadataJson: _ } = preParseContent(content)
|
||||
const idMap = deserializeIdMap(idMapJson!)
|
||||
const ast = Ast.parseTransitional(code, idMap)
|
||||
const ast_ = Ast.parseTransitional(code, deserializeIdMap(idMapJson!))
|
||||
const ast2 = Ast.normalize(ast)
|
||||
const astTT = Ast.tokenTreeWithIds(ast)
|
||||
expect(ast2.code()).toBe(ast.code())
|
||||
expect(Ast.tokenTreeWithIds(ast2), 'Print/parse preserves IDs').toStrictEqual(astTT)
|
||||
expect(Ast.tokenTreeWithIds(ast_), 'All node IDs come from IdMap').toStrictEqual(astTT)
|
||||
|
||||
const idMapJson2 = serializeIdMap(idMap)
|
||||
expect(idMapJson2).toBe(idMapJson)
|
||||
const META_TAG = '\n\n\n#### METADATA ####'
|
||||
let metaContent = META_TAG + '\n'
|
||||
metaContent += idMapJson2 + '\n'
|
||||
const {
|
||||
code: code_,
|
||||
idMapJson: idMapJson_,
|
||||
metadataJson: __,
|
||||
} = preParseContent(code + metaContent)
|
||||
const idMap_ = deserializeIdMap(idMapJson_!)
|
||||
const ast3 = Ast.parseTransitional(code_, idMap_)
|
||||
expect(Ast.tokenTreeWithIds(ast3), 'Print/parse with serialized IdMap').toStrictEqual(astTT)
|
||||
})
|
||||
|
||||
test('Block lines interface', () => {
|
||||
const block = Ast.parseBlock('VLE \nSISI\nGNIK \n')
|
||||
// Sort alphabetically, but keep the blank line at the end.
|
||||
|
23
app/gui2/src/util/ast/__tests__/abstractFileIo.test.ts
Normal file
23
app/gui2/src/util/ast/__tests__/abstractFileIo.test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Ast } from '@/util/ast'
|
||||
import * as fs from 'fs'
|
||||
import { splitFileContents } from 'shared/ensoFile'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
// FIXME: This test pulls parts of the server code to read a fixture file. Move necessary parts of
|
||||
// file format handling to shared and create a test utility for easy *.enso file fixture loading.
|
||||
import { deserializeIdMap } from '../../../../ydoc-server/serialization'
|
||||
|
||||
test('full file IdMap round trip', () => {
|
||||
const content = fs.readFileSync(__dirname + '/fixtures/stargazers.enso').toString()
|
||||
const { code, idMapJson, metadataJson: _ } = splitFileContents(content)
|
||||
const idMapOriginal = deserializeIdMap(idMapJson!)
|
||||
const idMap = idMapOriginal.clone()
|
||||
const ast_ = Ast.parseTransitional(code, idMapOriginal.clone())
|
||||
const ast = Ast.parseTransitional(code, idMap)
|
||||
const ast2 = Ast.normalize(ast)
|
||||
const astTT = Ast.tokenTreeWithIds(ast)
|
||||
expect(ast2.code()).toBe(ast.code())
|
||||
expect(Ast.tokenTreeWithIds(ast2), 'Print/parse preserves IDs').toStrictEqual(astTT)
|
||||
expect(Ast.tokenTreeWithIds(ast_), 'All node IDs come from IdMap').toStrictEqual(astTT)
|
||||
expect([...idMap.entries()].sort()).toStrictEqual([...idMapOriginal.entries()].sort())
|
||||
})
|
@ -7,11 +7,10 @@ import {
|
||||
ArgumentPlaceholder,
|
||||
interpretCall,
|
||||
} from '@/util/callTree'
|
||||
import { isSome } from '@/util/data/opt'
|
||||
import type { MethodCall } from 'shared/languageServerTypes'
|
||||
import { assert, expect, test } from 'vitest'
|
||||
|
||||
const prefixFixture = {
|
||||
allowInfix: false,
|
||||
mockSuggestion: {
|
||||
...makeModuleMethod('local.Foo.Bar.func'),
|
||||
arguments: ['self', 'a', 'b', 'c', 'd'].map((name) => makeArgument(name)),
|
||||
@ -21,14 +20,10 @@ const prefixFixture = {
|
||||
['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',
|
||||
},
|
||||
}
|
||||
|
||||
const infixFixture = {
|
||||
allowInfix: true,
|
||||
mockSuggestion: {
|
||||
...makeMethod('local.Foo.Bar.Buz.+'),
|
||||
arguments: ['lhs', 'rhs'].map((name) => makeArgument(name)),
|
||||
@ -37,11 +32,6 @@ const infixFixture = {
|
||||
['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',
|
||||
},
|
||||
}
|
||||
|
||||
interface TestData {
|
||||
@ -51,71 +41,56 @@ interface TestData {
|
||||
}
|
||||
|
||||
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 ?b =d =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 ?c ?d =m'} | ${prefixFixture}
|
||||
${'x + y'} | ${'@lhs @rhs'} | ${infixFixture}
|
||||
${'x +'} | ${'@lhs ?rhs'} | ${infixFixture}
|
||||
expression | expectedPattern | fixture
|
||||
${'func '} | ${'?self ?a ?b ?c ?d'} | ${prefixFixture}
|
||||
${'a.func '} | ${'?a ?b ?c ?d'} | ${prefixFixture}
|
||||
${'a.func a=x c=x '} | ${'=a ?b =c ?d'} | ${prefixFixture}
|
||||
${'a.func a=x x c=x '} | ${'=a @b =c ?d'} | ${prefixFixture}
|
||||
${'a.func a=x d=x '} | ${'=a ?b ?c =d'} | ${prefixFixture}
|
||||
${'a.func a=x d=x b=x '} | ${'=a =d =b ?c'} | ${prefixFixture}
|
||||
${'a.func a=x d=x c=x '} | ${'=a ?b =d =c'} | ${prefixFixture}
|
||||
${'func a=x d=x c=x '} | ${'?self =a ?b =d =c'} | ${prefixFixture}
|
||||
${'func self=x d=x c=x '} | ${'=self ?a ?b =d =c'} | ${prefixFixture}
|
||||
${'a.func a=x c=x d=x '} | ${'=a ?b =c =d'} | ${prefixFixture}
|
||||
${'a.func b=x '} | ${'?a =b ?c ?d'} | ${prefixFixture}
|
||||
${'a.func b=x c=x '} | ${'?a =b =c ?d'} | ${prefixFixture}
|
||||
${'a.func b=x x x '} | ${'=b @a @c ?d'} | ${prefixFixture}
|
||||
${'a.func c=x b=x x '} | ${'=c =b @a ?d'} | ${prefixFixture}
|
||||
${'a.func d=x '} | ${'?a ?b ?c =d'} | ${prefixFixture}
|
||||
${'a.func d=x a c=x '} | ${'=d @a ?b =c'} | ${prefixFixture}
|
||||
${'a.func d=x x '} | ${'=d @a ?b ?c'} | ${prefixFixture}
|
||||
${'a.func d=x x '} | ${'=d @a ?b ?c'} | ${prefixFixture}
|
||||
${'a.func d=x x x '} | ${'=d @a @b ?c'} | ${prefixFixture}
|
||||
${'a.func d=x x x x '} | ${'=d @a @b @c'} | ${prefixFixture}
|
||||
${'a.func x '} | ${'@a ?b ?c ?d'} | ${prefixFixture}
|
||||
${'a.func x b=x c=x '} | ${'@a =b =c ?d'} | ${prefixFixture}
|
||||
${'a.func x b=x x '} | ${'@a =b @c ?d'} | ${prefixFixture}
|
||||
${'a.func x d=x '} | ${'@a ?b ?c =d'} | ${prefixFixture}
|
||||
${'a.func x x '} | ${'@a @b ?c ?d'} | ${prefixFixture}
|
||||
${'a.func x x x '} | ${'@a @b @c ?d'} | ${prefixFixture}
|
||||
${'a.func x x x x '} | ${'@a @b @c @d'} | ${prefixFixture}
|
||||
${'a.func a=x x m=x '} | ${'=a @b ?c ?d =m'} | ${prefixFixture}
|
||||
${'x + y'} | ${'@lhs @rhs'} | ${infixFixture}
|
||||
${'x +'} | ${'@lhs ?rhs'} | ${infixFixture}
|
||||
`(
|
||||
"Creating argument application's info: $expression $expectedPattern",
|
||||
({
|
||||
expression,
|
||||
expectedPattern,
|
||||
fixture: { mockSuggestion, argsParameters, methodPointer },
|
||||
fixture: { allowInfix, mockSuggestion, argsParameters },
|
||||
}: 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,
|
||||
notAppliedArguments,
|
||||
}
|
||||
|
||||
const funcMethodCall: MethodCall = {
|
||||
methodPointer,
|
||||
notAppliedArguments: Array.from(expectedArgs, (_, i) => i + 1),
|
||||
}
|
||||
|
||||
const configuration: widgetCfg.FunctionCall = {
|
||||
kind: 'FunctionCall',
|
||||
parameters: argsParameters,
|
||||
}
|
||||
|
||||
const interpreted = interpretCall(ast, true)
|
||||
const interpreted = interpretCall(ast, allowInfix)
|
||||
const call = ArgumentApplication.FromInterpretedWithInfo(interpreted, {
|
||||
appMethodCall: methodCall,
|
||||
noArgsCall: funcMethodCall,
|
||||
suggestion: mockSuggestion,
|
||||
widgetCfg: configuration,
|
||||
subjectAsSelf: true,
|
||||
})
|
||||
assert(call instanceof ArgumentApplication)
|
||||
expect(printArgPattern(call)).toEqual(expectedPattern)
|
||||
|
@ -5,7 +5,6 @@ import * as widgetCfg from '@/providers/widgetRegistry/configuration'
|
||||
import type { SuggestionEntry, SuggestionEntryArgument } from '@/stores/suggestionDatabase/entry'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { findLastIndex, tryGetIndex } from '@/util/data/array'
|
||||
import type { MethodCall } from 'shared/languageServerTypes'
|
||||
import { assert } from './assert'
|
||||
|
||||
export const enum ApplicationKind {
|
||||
@ -147,10 +146,10 @@ export function interpretCall(callRoot: Ast.Ast, allowInterpretAsInfix: boolean)
|
||||
}
|
||||
|
||||
interface CallInfo {
|
||||
noArgsCall?: MethodCall | undefined
|
||||
appMethodCall?: MethodCall | undefined
|
||||
notAppliedArguments?: number[] | undefined
|
||||
suggestion?: SuggestionEntry | undefined
|
||||
widgetCfg?: widgetCfg.FunctionCall | undefined
|
||||
subjectAsSelf?: boolean | undefined
|
||||
}
|
||||
|
||||
export class ArgumentApplication {
|
||||
@ -184,12 +183,8 @@ export class ArgumentApplication {
|
||||
)
|
||||
}
|
||||
|
||||
private static FromInterpretedPrefix(
|
||||
interpreted: InterpretedPrefix,
|
||||
callInfo: CallInfo,
|
||||
stripSelfArgument: boolean,
|
||||
) {
|
||||
const { noArgsCall, suggestion, widgetCfg } = callInfo
|
||||
private static FromInterpretedPrefix(interpreted: InterpretedPrefix, callInfo: CallInfo) {
|
||||
const { notAppliedArguments, suggestion, widgetCfg, subjectAsSelf } = callInfo
|
||||
const callId = interpreted.func.exprId
|
||||
|
||||
const knownArguments = suggestion?.arguments
|
||||
@ -198,15 +193,14 @@ export class ArgumentApplication {
|
||||
// when this is a method application with applied 'self', the subject of the access operator is
|
||||
// treated as a 'self' argument.
|
||||
if (
|
||||
stripSelfArgument &&
|
||||
subjectAsSelf &&
|
||||
knownArguments?.[0]?.name === 'self' &&
|
||||
getAccessOprSubject(interpreted.func) != null
|
||||
) {
|
||||
allPossiblePrefixArguments.shift()
|
||||
}
|
||||
const notAppliedOriginally = new Set(
|
||||
noArgsCall?.notAppliedArguments ?? allPossiblePrefixArguments,
|
||||
)
|
||||
|
||||
const notAppliedOriginally = new Set(notAppliedArguments ?? allPossiblePrefixArguments)
|
||||
const argumentsLeftToMatch = allPossiblePrefixArguments.filter((i) =>
|
||||
notAppliedOriginally.has(i),
|
||||
)
|
||||
@ -355,12 +349,11 @@ 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)
|
||||
return ArgumentApplication.FromInterpretedPrefix(interpreted, callInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
import diff from 'fast-diff'
|
||||
import * as json from 'lib0/json'
|
||||
import * as Y from 'yjs'
|
||||
import { combineFileParts, splitFileContents } from '../shared/ensoFile'
|
||||
import { TextEdit } from '../shared/languageServerTypes'
|
||||
import { ModuleDoc, type NodeMetadata, type VisualizationMetadata } from '../shared/yjsModel'
|
||||
import * as fileFormat from './fileFormat'
|
||||
@ -17,8 +18,6 @@ interface AppliedUpdates {
|
||||
newMetadata: fileFormat.Metadata
|
||||
}
|
||||
|
||||
const META_TAG = '\n\n\n#### METADATA ####'
|
||||
|
||||
export function applyDocumentUpdates(
|
||||
doc: ModuleDoc,
|
||||
syncedMeta: fileFormat.Metadata,
|
||||
@ -26,7 +25,7 @@ export function applyDocumentUpdates(
|
||||
dataKeys: Y.YMapEvent<Uint8Array>['keys'] | null,
|
||||
metadataKeys: Y.YMapEvent<NodeMetadata>['keys'] | null,
|
||||
): AppliedUpdates {
|
||||
const synced = preParseContent(syncedContent)
|
||||
const synced = splitFileContents(syncedContent)
|
||||
|
||||
let codeUpdated = false
|
||||
let idMapUpdated = false
|
||||
@ -46,25 +45,23 @@ export function applyDocumentUpdates(
|
||||
}
|
||||
}
|
||||
|
||||
let newContent = ''
|
||||
let newCode: string
|
||||
let idMapJson: string
|
||||
let metadataJson: string
|
||||
|
||||
const allEdits: TextEdit[] = []
|
||||
if (codeUpdated) {
|
||||
const text = doc.getCode()
|
||||
allEdits.push(...applyDiffAsTextEdits(0, synced.code, text))
|
||||
newContent += text
|
||||
newCode = text
|
||||
} else {
|
||||
newContent += synced.code
|
||||
newCode = synced.code
|
||||
}
|
||||
|
||||
const metaStartLine = (newContent.match(/\n/g) ?? []).length
|
||||
let metaContent = META_TAG + '\n'
|
||||
|
||||
if (idMapUpdated || synced.idMapJson == null) {
|
||||
const idMapJson = serializeIdMap(doc.getIdMap())
|
||||
metaContent += idMapJson + '\n'
|
||||
idMapJson = serializeIdMap(doc.getIdMap())
|
||||
} else {
|
||||
metaContent += (synced.idMapJson ?? '[]') + '\n'
|
||||
idMapJson = synced.idMapJson
|
||||
}
|
||||
|
||||
let newMetadata = syncedMeta
|
||||
@ -97,20 +94,26 @@ export function applyDocumentUpdates(
|
||||
newMetadata = { ...syncedMeta }
|
||||
newMetadata.ide = { ...syncedMeta.ide }
|
||||
newMetadata.ide.node = nodeMetadata
|
||||
const metadataJson = json.stringify(newMetadata)
|
||||
metaContent += metadataJson
|
||||
metadataJson = json.stringify(newMetadata)
|
||||
} else {
|
||||
metaContent += synced.metadataJson ?? '{}'
|
||||
metadataJson = synced.metadataJson ?? '{}'
|
||||
}
|
||||
|
||||
const newContent = combineFileParts({
|
||||
code: newCode,
|
||||
idMapJson,
|
||||
metadataJson,
|
||||
})
|
||||
|
||||
const oldMetaContent = syncedContent.slice(synced.code.length)
|
||||
const metaContent = newContent.slice(newCode.length)
|
||||
const metaStartLine = (newCode.match(/\n/g) ?? []).length
|
||||
allEdits.push(...applyDiffAsTextEdits(metaStartLine, oldMetaContent, metaContent))
|
||||
newContent += metaContent
|
||||
|
||||
return {
|
||||
edits: allEdits,
|
||||
newContent,
|
||||
newMetadata: newMetadata,
|
||||
newMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,29 +164,6 @@ export function translateVisualizationFromFile(
|
||||
}
|
||||
}
|
||||
|
||||
interface PreParsedContent {
|
||||
code: string
|
||||
idMapJson: string | null
|
||||
metadataJson: string | null
|
||||
}
|
||||
|
||||
export function preParseContent(content: string): PreParsedContent {
|
||||
const splitPoint = content.lastIndexOf(META_TAG)
|
||||
if (splitPoint < 0) {
|
||||
return {
|
||||
code: content,
|
||||
idMapJson: null,
|
||||
metadataJson: null,
|
||||
}
|
||||
}
|
||||
const code = content.slice(0, splitPoint)
|
||||
const metadataString = content.slice(splitPoint + META_TAG.length)
|
||||
const metaLines = metadataString.trim().split('\n')
|
||||
const idMapJson = metaLines[0] ?? null
|
||||
const metadataJson = metaLines[1] ?? null
|
||||
return { code, idMapJson, metadataJson }
|
||||
}
|
||||
|
||||
export function applyDiffAsTextEdits(
|
||||
lineOffset: number,
|
||||
oldString: string,
|
||||
|
@ -3,6 +3,7 @@ import * as map from 'lib0/map'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
import * as random from 'lib0/random'
|
||||
import * as Y from 'yjs'
|
||||
import { splitFileContents } from '../shared/ensoFile'
|
||||
import { LanguageServer, computeTextChecksum } from '../shared/languageServer'
|
||||
import { Checksum, FileEdit, Path, response } from '../shared/languageServerTypes'
|
||||
import { exponentialBackoff, printingCallbacks } from '../shared/retry'
|
||||
@ -13,12 +14,7 @@ import {
|
||||
type NodeMetadata,
|
||||
type Uuid,
|
||||
} from '../shared/yjsModel'
|
||||
import {
|
||||
applyDocumentUpdates,
|
||||
preParseContent,
|
||||
prettyPrintDiff,
|
||||
translateVisualizationFromFile,
|
||||
} from './edits'
|
||||
import { applyDocumentUpdates, prettyPrintDiff, translateVisualizationFromFile } from './edits'
|
||||
import * as fileFormat from './fileFormat'
|
||||
import { deserializeIdMap } from './serialization'
|
||||
import { WSSharedDoc } from './ydoc'
|
||||
@ -473,7 +469,7 @@ class ModulePersistence extends ObservableV2<{ removed: () => void }> {
|
||||
|
||||
private syncFileContents(content: string, version: Checksum) {
|
||||
this.doc.ydoc.transact(() => {
|
||||
const { code, idMapJson, metadataJson } = preParseContent(content)
|
||||
const { code, idMapJson, metadataJson } = splitFileContents(content)
|
||||
const metadata = fileFormat.tryParseMetadataOrFallback(metadataJson)
|
||||
const nodeMeta = metadata.ide.node
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user