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:
Paweł Grabarz 2024-01-18 14:13:31 +01:00 committed by GitHub
parent 14be36c401
commit 48a5599eb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 224 additions and 199 deletions

View File

@ -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,
},

View 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)
})

View 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
}
}
}

View File

@ -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>

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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.

View 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())
})

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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