diff --git a/app/gui/src/project-view/components/CodeEditor/tooltips.ts b/app/gui/src/project-view/components/CodeEditor/tooltips.ts index 1f69dfa026a1..0c9cfb609b4d 100644 --- a/app/gui/src/project-view/components/CodeEditor/tooltips.ts +++ b/app/gui/src/project-view/components/CodeEditor/tooltips.ts @@ -69,7 +69,7 @@ export function ensoHoverTooltip( nodeId, syntax: syn.name, graphDb: graphStore.db, - suggestionDbStore: suggestionDbStore, + suggestionDbStore, }) }) } diff --git a/app/gui/src/project-view/components/ComponentBrowser/__tests__/component.test.ts b/app/gui/src/project-view/components/ComponentBrowser/__tests__/component.test.ts index 65e778d4b31c..4666bf30e75b 100644 --- a/app/gui/src/project-view/components/ComponentBrowser/__tests__/component.test.ts +++ b/app/gui/src/project-view/components/ComponentBrowser/__tests__/component.test.ts @@ -1,5 +1,3 @@ -import { expect, test } from 'vitest' - import { compareSuggestions, labelOfEntry, @@ -14,9 +12,10 @@ import { makeModule, makeModuleMethod, makeStaticMethod, -} from '@/stores/suggestionDatabase/entry' +} from '@/stores/suggestionDatabase/mockSuggestion' import { allRanges } from '@/util/data/range' import shuffleSeed from 'shuffle-seed' +import { expect, test } from 'vitest' test.each([ [makeModuleMethod('Standard.Base.Data.read'), 'Data.read'], diff --git a/app/gui/src/project-view/components/ComponentBrowser/__tests__/filtering.test.ts b/app/gui/src/project-view/components/ComponentBrowser/__tests__/filtering.test.ts index 8780dca65d02..621acf811ca6 100644 --- a/app/gui/src/project-view/components/ComponentBrowser/__tests__/filtering.test.ts +++ b/app/gui/src/project-view/components/ComponentBrowser/__tests__/filtering.test.ts @@ -1,8 +1,6 @@ -import { expect, test } from 'vitest' - import { Filtering, type MatchResult } from '@/components/ComponentBrowser/filtering' +import { entryQn, SuggestionEntry } from '@/stores/suggestionDatabase/entry' import { - entryQn, makeConstructor, makeFunction, makeLocal, @@ -10,9 +8,9 @@ import { makeModule, makeModuleMethod, makeStaticMethod, - SuggestionEntry, -} from '@/stores/suggestionDatabase/entry' +} from '@/stores/suggestionDatabase/mockSuggestion' import { qnLastSegment, QualifiedName } from '@/util/qualifiedName' +import { expect, test } from 'vitest' import { Opt } from 'ydoc-shared/util/data/opt' test.each([ diff --git a/app/gui/src/project-view/components/ComponentBrowser/component.ts b/app/gui/src/project-view/components/ComponentBrowser/component.ts index 3b40ee410031..d05afea3744a 100644 --- a/app/gui/src/project-view/components/ComponentBrowser/component.ts +++ b/app/gui/src/project-view/components/ComponentBrowser/component.ts @@ -8,11 +8,11 @@ import { import { compareOpt } from '@/util/compare' import { isSome } from '@/util/data/opt' import { Range } from '@/util/data/range' +import { ANY_TYPE_QN } from '@/util/ensoTypes' import { displayedIconOf } from '@/util/getIconName' import type { Icon } from '@/util/iconName' import type { QualifiedName } from '@/util/qualifiedName' import { qnLastSegmentIndex, tryQualifiedName } from '@/util/qualifiedName' -import { unwrap } from 'ydoc-shared/util/data/result' interface ComponentLabelInfo { label: string @@ -109,14 +109,12 @@ export function makeComponent({ id, entry, match }: ComponentInfo): Component { } } -const ANY_TYPE = unwrap(tryQualifiedName('Standard.Base.Any.Any')) - /** Create {@link Component} list from filtered suggestions. */ export function makeComponentList(db: SuggestionDb, filtering: Filtering): Component[] { function* matchSuggestions() { // All types are descendants of `Any`, so we can safely prepopulate it here. // This way, we will use it even when `selfArg` is not a valid qualified name. - const additionalSelfTypes: QualifiedName[] = [ANY_TYPE] + const additionalSelfTypes: QualifiedName[] = [ANY_TYPE_QN] if (filtering.selfArg?.type === 'known') { const maybeName = tryQualifiedName(filtering.selfArg.typename) if (maybeName.ok) populateAdditionalSelfTypes(db, additionalSelfTypes, maybeName.value) @@ -140,7 +138,7 @@ export function makeComponentList(db: SuggestionDb, filtering: Filtering): Compo function populateAdditionalSelfTypes(db: SuggestionDb, list: QualifiedName[], name: QualifiedName) { let entry = db.getEntryByQualifiedName(name) // We don’t need to add `Any` to the list, because the caller already did that. - while (entry != null && entry.parentType != null && entry.parentType !== ANY_TYPE) { + while (entry != null && entry.parentType != null && entry.parentType !== ANY_TYPE_QN) { list.push(entry.parentType) entry = db.getEntryByQualifiedName(entry.parentType) } diff --git a/app/gui/src/project-view/components/GraphEditor.vue b/app/gui/src/project-view/components/GraphEditor.vue index 093244559f95..6b42c5843f09 100644 --- a/app/gui/src/project-view/components/GraphEditor.vue +++ b/app/gui/src/project-view/components/GraphEditor.vue @@ -580,6 +580,7 @@ async function handleFileDrop(event: DragEvent) { if (!event.dataTransfer?.items) return ;[...event.dataTransfer.items].forEach(async (item, index) => { if (item.kind === 'file') { + if (!graphStore.methodAst.ok) return const file = item.getAsFile() if (!file) return const clientPos = new Vec2(event.clientX, event.clientY) @@ -591,7 +592,7 @@ async function handleFileDrop(event: DragEvent) { pos, projectStore.isOnLocalBackend, event.shiftKey, - projectStore.executionContext.getStackTop(), + graphStore.methodAst.value.externalId, ) const uploadResult = await uploader.upload() if (uploadResult.ok) { diff --git a/app/gui/src/project-view/components/GraphEditor/GraphNodes.vue b/app/gui/src/project-view/components/GraphEditor/GraphNodes.vue index a8398cc5a04a..b091c5309cbb 100644 --- a/app/gui/src/project-view/components/GraphEditor/GraphNodes.vue +++ b/app/gui/src/project-view/components/GraphEditor/GraphNodes.vue @@ -12,8 +12,7 @@ import { useProjectStore } from '@/stores/project' import type { AstId } from '@/util/ast/abstract' import type { Vec2 } from '@/util/data/vec2' import { set } from 'lib0' -import { computed, shallowRef, toRaw } from 'vue' -import { stackItemsEqual } from 'ydoc-shared/languageServerTypes' +import { computed, shallowRef } from 'vue' const emit = defineEmits<{ nodeOutputPortDoubleClick: [portId: AstId] @@ -48,9 +47,9 @@ useEvent(window, 'keydown', displacingWithArrows.events.keydown) const uploadingFiles = computed<[FileName, File][]>(() => { const uploads = [...projectStore.awareness.allUploads()] - if (uploads.length == 0) return [] - const currentStackItem = toRaw(projectStore.executionContext.getStackTop()) - return uploads.filter(([, file]) => stackItemsEqual(file.stackItem, currentStackItem)) + if (uploads.length == 0 || !graphStore.methodAst.ok) return [] + const currentMethod = graphStore.methodAst.value.externalId + return uploads.filter(([, file]) => file.method === currentMethod) }) const graphNodeSelections = shallowRef() diff --git a/app/gui/src/project-view/components/GraphEditor/upload.ts b/app/gui/src/project-view/components/GraphEditor/upload.ts index be9e90d4196a..d35a42917970 100644 --- a/app/gui/src/project-view/components/GraphEditor/upload.ts +++ b/app/gui/src/project-view/components/GraphEditor/upload.ts @@ -4,11 +4,11 @@ import { Vec2 } from '@/util/data/vec2' import type { DataServer } from '@/util/net/dataServer' import { Keccak, sha3_224 as SHA3 } from '@noble/hashes/sha3' import type { Hash } from '@noble/hashes/utils' -import { markRaw, toRaw } from 'vue' import { escapeTextLiteral } from 'ydoc-shared/ast/text' import type { LanguageServer } from 'ydoc-shared/languageServer' -import type { Path, StackItem, Uuid } from 'ydoc-shared/languageServerTypes' +import type { Path, Uuid } from 'ydoc-shared/languageServerTypes' import { Err, Ok, type Result } from 'ydoc-shared/util/data/result' +import { type ExternalId } from 'ydoc-shared/yjsModel' // === Constants === @@ -45,7 +45,6 @@ export interface UploadResult { export class Uploader { private checksum: Hash private uploadedBytes: bigint - private stackItem: StackItem private awareness: Awareness private projectFiles: ProjectFiles @@ -60,11 +59,10 @@ export class Uploader { private position: Vec2, private isOnLocalBackend: boolean, private disableDirectRead: boolean, - stackItem: StackItem, + private readonly method: ExternalId, ) { this.checksum = SHA3.create() this.uploadedBytes = BigInt(0) - this.stackItem = markRaw(toRaw(stackItem)) this.awareness = projectStore.awareness this.projectFiles = useProjectFiles(projectStore) } @@ -81,16 +79,17 @@ export class Uploader { position: Vec2, isOnLocalBackend: boolean, disableDirectRead: boolean, - stackItem: StackItem, + method: ExternalId, ): Uploader { - return new Uploader( - projectStore, - file, - position, - isOnLocalBackend, - disableDirectRead, - stackItem, - ) + return new Uploader(projectStore, file, position, isOnLocalBackend, disableDirectRead, method) + } + + private progressUpdate(sizePercentage: number) { + return { + sizePercentage, + position: this.position, + method: this.method, + } } /** Start the upload process */ @@ -111,11 +110,7 @@ export class Uploader { if (!dataDirExists.ok) return dataDirExists const name = await this.projectFiles.pickUniqueName(dataDirPath, this.file.name) if (!name.ok) return name - this.awareness.addOrUpdateUpload(name.value, { - sizePercentage: 0, - position: this.position, - stackItem: this.stackItem, - }) + this.awareness.addOrUpdateUpload(name.value, this.progressUpdate(0)) const remotePath: Path = { rootId, segments: [DATA_DIR_NAME, name.value] } const cleanup = this.cleanup.bind(this, name.value) const writableStream = new WritableStream({ @@ -131,11 +126,7 @@ export class Uploader { this.uploadedBytes += BigInt(chunk.length) const bytes = Number(this.uploadedBytes) const sizePercentage = Math.round((bytes / this.file.size) * 100) - this.awareness.addOrUpdateUpload(name.value, { - sizePercentage, - position: this.position, - stackItem: this.stackItem, - }) + this.awareness.addOrUpdateUpload(name.value, this.progressUpdate(sizePercentage)) }, close: cleanup, abort: async (reason: string) => { diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFunction/__tests__/widgetFunctionCallInfo.test.ts b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFunction/__tests__/widgetFunctionCallInfo.test.ts index 95584d69df8f..40e05890db11 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFunction/__tests__/widgetFunctionCallInfo.test.ts +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFunction/__tests__/widgetFunctionCallInfo.test.ts @@ -1,13 +1,13 @@ import { WidgetInput } from '@/providers/widgetRegistry' import { parseWithSpans } from '@/stores/graph/__tests__/graphDatabase.test' import type { NodeVisualizationConfiguration } from '@/stores/project/executionContext' +import { entryMethodPointer } from '@/stores/suggestionDatabase/entry' import { - entryMethodPointer, makeArgument, makeConstructor, makeMethod, makeStaticMethod, -} from '@/stores/suggestionDatabase/entry' +} from '@/stores/suggestionDatabase/mockSuggestion' import { assert } from '@/util/assert' import { Ast } from '@/util/ast' import { expect, test } from 'vitest' diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/__tests__/tableInputArgument.test.ts b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/__tests__/tableInputArgument.test.ts index 758357a8dd99..03818aa88f13 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/__tests__/tableInputArgument.test.ts +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetTableEditor/__tests__/tableInputArgument.test.ts @@ -10,7 +10,7 @@ import { import { MenuItem } from '@/components/shared/AgGridTableView.vue' import { WidgetInput } from '@/providers/widgetRegistry' import { SuggestionDb } from '@/stores/suggestionDatabase' -import { makeType } from '@/stores/suggestionDatabase/entry' +import { makeType } from '@/stores/suggestionDatabase/mockSuggestion' import { assert } from '@/util/assert' import { Ast } from '@/util/ast' import { GetContextMenuItems, GetMainMenuItems } from 'ag-grid-enterprise' diff --git a/app/gui/src/project-view/stores/awareness.ts b/app/gui/src/project-view/stores/awareness.ts index 99b38f0be721..bfe988019a1d 100644 --- a/app/gui/src/project-view/stores/awareness.ts +++ b/app/gui/src/project-view/stores/awareness.ts @@ -1,7 +1,7 @@ import { Vec2 } from '@/util/data/vec2' import { reactive } from 'vue' import { Awareness as YjsAwareness } from 'y-protocols/awareness' -import type { StackItem } from 'ydoc-shared/languageServerTypes' +import { type ExternalId } from 'ydoc-shared/yjsModel' import * as Y from 'yjs' // === Public types === @@ -10,7 +10,7 @@ export type FileName = string export interface UploadingFile { sizePercentage: number - stackItem: StackItem + method: ExternalId position: Vec2 } diff --git a/app/gui/src/project-view/stores/graph/__tests__/imports.test.ts b/app/gui/src/project-view/stores/graph/__tests__/imports.test.ts index c7f83afa5e10..c37e61d16013 100644 --- a/app/gui/src/project-view/stores/graph/__tests__/imports.test.ts +++ b/app/gui/src/project-view/stores/graph/__tests__/imports.test.ts @@ -16,7 +16,7 @@ import { makeModule, makeStaticMethod, makeType, -} from '@/stores/suggestionDatabase/entry' +} from '@/stores/suggestionDatabase/mockSuggestion' import { Ast } from '@/util/ast' import { unwrap } from '@/util/data/result' import { tryIdentifier, tryQualifiedName } from '@/util/qualifiedName' diff --git a/app/gui/src/project-view/stores/graph/graphDatabase.ts b/app/gui/src/project-view/stores/graph/graphDatabase.ts index e7f08f39b292..36d62a0f764a 100644 --- a/app/gui/src/project-view/stores/graph/graphDatabase.ts +++ b/app/gui/src/project-view/stores/graph/graphDatabase.ts @@ -27,7 +27,14 @@ import { } from '@/util/reactivity' import * as objects from 'enso-common/src/utilities/data/object' import * as set from 'lib0/set' -import { reactive, ref, shallowReactive, type Ref, type WatchStopHandle } from 'vue' +import { + reactive, + ref, + shallowReactive, + type DeepReadonly, + type Ref, + type WatchStopHandle, +} from 'vue' import { type SourceDocument } from 'ydoc-shared/ast/sourceDocument' import { methodPointerEquals, @@ -60,7 +67,7 @@ export class GraphDb { /** TODO: Add docs */ constructor( private suggestionDb: SuggestionDb, - private groups: Ref, + private groups: Ref>, private valuesRegistry: ComputedValueRegistry, ) {} diff --git a/app/gui/src/project-view/stores/graph/imports.ts b/app/gui/src/project-view/stores/graph/imports.ts index d2118ad3eb32..a57503645828 100644 --- a/app/gui/src/project-view/stores/graph/imports.ts +++ b/app/gui/src/project-view/stores/graph/imports.ts @@ -1,7 +1,7 @@ import { SuggestionDb } from '@/stores/suggestionDatabase' import { SuggestionKind, type SuggestionEntry } from '@/stores/suggestionDatabase/entry' import { Ast } from '@/util/ast' -import { MutableModule, parseIdent, parseIdents, parseQualifiedName } from '@/util/ast/abstract' +import { astToQualifiedName, MutableModule, parseIdent, parseIdents } from '@/util/ast/abstract' import { unwrap } from '@/util/data/result' import { qnLastSegment, @@ -24,7 +24,7 @@ export function recognizeImport(ast: Ast.Import): Import | null { const all = ast.all const hiding = ast.hiding const moduleAst = from ?? import_ - const module = moduleAst ? parseQualifiedName(moduleAst) : null + const module = moduleAst ? astToQualifiedName(moduleAst) : null if (!module) return null if (all) { const except = (hiding != null ? parseIdents(hiding) : []) ?? [] diff --git a/app/gui/src/project-view/stores/suggestionDatabase/__tests__/lsUpdate.test.ts b/app/gui/src/project-view/stores/suggestionDatabase/__tests__/lsUpdate.test.ts index 17e4b91bfbb7..2e7710cd9605 100644 --- a/app/gui/src/project-view/stores/suggestionDatabase/__tests__/lsUpdate.test.ts +++ b/app/gui/src/project-view/stores/suggestionDatabase/__tests__/lsUpdate.test.ts @@ -1,16 +1,25 @@ import { SuggestionDb, type Group } from '@/stores/suggestionDatabase' import { SuggestionKind, entryQn, type SuggestionEntry } from '@/stores/suggestionDatabase/entry' -import { applyUpdates } from '@/stores/suggestionDatabase/lsUpdate' +import { SuggestionUpdateProcessor } from '@/stores/suggestionDatabase/lsUpdate' import { unwrap } from '@/util/data/result' import { parseDocs } from '@/util/docParser' import { tryIdentifier, tryQualifiedName, type QualifiedName } from '@/util/qualifiedName' import { expect, test } from 'vitest' import * as lsTypes from 'ydoc-shared/languageServerTypes/suggestions' +import { type SuggestionsDatabaseUpdate } from 'ydoc-shared/languageServerTypes/suggestions' + +function applyUpdates( + db: SuggestionDb, + updates: SuggestionsDatabaseUpdate[], + { groups }: { groups: Group[] }, +) { + new SuggestionUpdateProcessor(groups).applyUpdates(db, updates) +} test('Adding suggestion database entries', () => { const test = new Fixture() const db = new SuggestionDb() - applyUpdates(db, test.addUpdatesForExpected(), test.groups) + applyUpdates(db, test.addUpdatesForExpected(), test.suggestionContext) test.check(db) }) @@ -24,23 +33,26 @@ test('Entry qualified names', () => { expect(entryQn(db.get(5)!)).toStrictEqual('Standard.Base.Type.static_method') expect(entryQn(db.get(6)!)).toStrictEqual('Standard.Base.function') expect(entryQn(db.get(7)!)).toStrictEqual('Standard.Base.local') + expect(entryQn(db.get(8)!)).toStrictEqual('local.Mock_Project.collapsed') }) test('Qualified name indexing', () => { const test = new Fixture() const db = new SuggestionDb() - applyUpdates(db, test.addUpdatesForExpected(), test.groups) - for (let i = 1; i <= 7; i++) { - const qName = entryQn(db.get(i)!) - expect(db.nameToId.lookup(qName)).toEqual(new Set([i])) - expect(db.nameToId.reverseLookup(i)).toEqual(new Set([qName])) + const addUpdates = test.addUpdatesForExpected() + applyUpdates(db, addUpdates, test.suggestionContext) + for (const { id } of addUpdates) { + const qName = entryQn(db.get(id)!) + expect(db.nameToId.lookup(qName)).toEqual(new Set([id])) + expect(db.nameToId.reverseLookup(id)).toEqual(new Set([qName])) } }) test('Parent-children indexing', () => { const test = new Fixture() const db = new SuggestionDb() - applyUpdates(db, test.addUpdatesForExpected(), test.groups) + const initialAddUpdates = test.addUpdatesForExpected() + applyUpdates(db, initialAddUpdates, test.suggestionContext) // Parent lookup. expect(db.childIdToParentId.lookup(1)).toEqual(new Set([])) expect(db.childIdToParentId.lookup(2)).toEqual(new Set([1])) @@ -49,6 +61,7 @@ test('Parent-children indexing', () => { expect(db.childIdToParentId.lookup(5)).toEqual(new Set([2])) expect(db.childIdToParentId.lookup(6)).toEqual(new Set([1])) expect(db.childIdToParentId.lookup(7)).toEqual(new Set([1])) + expect(db.childIdToParentId.lookup(8)).toEqual(new Set([])) // Children lookup. expect(db.childIdToParentId.reverseLookup(1)).toEqual(new Set([2, 6, 7])) @@ -58,12 +71,14 @@ test('Parent-children indexing', () => { expect(db.childIdToParentId.reverseLookup(5)).toEqual(new Set([])) expect(db.childIdToParentId.reverseLookup(6)).toEqual(new Set([])) expect(db.childIdToParentId.reverseLookup(7)).toEqual(new Set([])) + expect(db.childIdToParentId.reverseLookup(8)).toEqual(new Set([])) // Add new entry. + const newEntryId = initialAddUpdates[initialAddUpdates.length - 1]!.id + 1 const modifications: lsTypes.SuggestionsDatabaseUpdate[] = [ { type: 'Add', - id: 8, + id: newEntryId, suggestion: { type: 'method', module: 'Standard.Base', @@ -77,22 +92,22 @@ test('Parent-children indexing', () => { }, }, ] - applyUpdates(db, modifications, test.groups) - expect(db.childIdToParentId.lookup(8)).toEqual(new Set([2])) - expect(db.childIdToParentId.reverseLookup(8)).toEqual(new Set([])) - expect(db.childIdToParentId.reverseLookup(2)).toEqual(new Set([3, 4, 5, 8])) + applyUpdates(db, modifications, test.suggestionContext) + expect(db.childIdToParentId.lookup(newEntryId)).toEqual(new Set([2])) + expect(db.childIdToParentId.reverseLookup(newEntryId)).toEqual(new Set([])) + expect(db.childIdToParentId.reverseLookup(2)).toEqual(new Set([3, 4, 5, newEntryId])) // Remove entry. const modifications2: lsTypes.SuggestionsDatabaseUpdate[] = [{ type: 'Remove', id: 3 }] - applyUpdates(db, modifications2, test.groups) + applyUpdates(db, modifications2, test.suggestionContext) expect(db.childIdToParentId.lookup(3)).toEqual(new Set([])) - expect(db.childIdToParentId.reverseLookup(2)).toEqual(new Set([4, 5, 8])) + expect(db.childIdToParentId.reverseLookup(2)).toEqual(new Set([4, 5, newEntryId])) // Modify entry. Moving new method from `Standard.Base.Type` to `Standard.Base`. - db.get(8)!.memberOf = 'Standard.Base' as QualifiedName - expect(db.childIdToParentId.reverseLookup(1)).toEqual(new Set([2, 6, 7, 8])) - expect(db.childIdToParentId.lookup(8)).toEqual(new Set([1])) - expect(db.childIdToParentId.reverseLookup(8)).toEqual(new Set([])) + db.get(newEntryId)!.memberOf = 'Standard.Base' as QualifiedName + expect(db.childIdToParentId.reverseLookup(1)).toEqual(new Set([2, 6, 7, newEntryId])) + expect(db.childIdToParentId.lookup(newEntryId)).toEqual(new Set([1])) + expect(db.childIdToParentId.reverseLookup(newEntryId)).toEqual(new Set([])) expect(db.childIdToParentId.reverseLookup(2)).toEqual(new Set([4, 5])) }) @@ -137,7 +152,7 @@ test("Modifying suggestion entries' fields", () => { test.expectedStaticMethod.memberOf = unwrap(tryQualifiedName('Standard.Base2.Type')) test.expectedFunction.scope = scope2 - applyUpdates(db, modifications, test.groups) + applyUpdates(db, modifications, test.suggestionContext) test.check(db) }) @@ -166,7 +181,7 @@ test("Unsetting suggestion entries' fields", () => { test.expectedMethod.documentation = [] delete test.expectedMethod.groupIndex - applyUpdates(db, modifications, test.groups) + applyUpdates(db, modifications, test.suggestionContext) test.check(db) }) @@ -177,7 +192,7 @@ test('Removing entries from database', () => { { type: 'Remove', id: 6 }, ] const db = test.createDbWithExpected() - applyUpdates(db, update, test.groups) + applyUpdates(db, update, test.suggestionContext) expect(db.get(1)).toStrictEqual(test.expectedModule) expect(db.get(2)).toBeUndefined() expect(db.get(3)).toStrictEqual(test.expectedCon) @@ -185,6 +200,7 @@ test('Removing entries from database', () => { expect(db.get(5)).toStrictEqual(test.expectedStaticMethod) expect(db.get(6)).toBeUndefined() expect(db.get(7)).toStrictEqual(test.expectedLocal) + expect(db.get(8)).toStrictEqual(test.expectedLocalStaticMethod) }) test('Adding new argument', () => { @@ -205,7 +221,7 @@ test('Adding new argument', () => { test.expectedCon.arguments = [test.arg1, newArg] test.expectedStaticMethod.arguments = [test.arg1, newArg, test.arg2] - applyUpdates(db, modifications, test.groups) + applyUpdates(db, modifications, test.suggestionContext) test.check(db) }) @@ -250,7 +266,7 @@ test('Modifying arguments', () => { const db = test.createDbWithExpected() test.expectedStaticMethod.arguments = [newArg1, newArg2] - applyUpdates(db, modifications, test.groups) + applyUpdates(db, modifications, test.suggestionContext) test.check(db) }) @@ -264,15 +280,18 @@ test('Removing Arguments', () => { test.expectedMethod.arguments = [] test.expectedStaticMethod.arguments = [test.arg1] - applyUpdates(db, update, test.groups) + applyUpdates(db, update, test.suggestionContext) test.check(db) }) class Fixture { - groups: Group[] = [ - { name: 'Test1', project: unwrap(tryQualifiedName('Standard.Base')) }, - { name: 'Test2', project: unwrap(tryQualifiedName('Standard.Base')) }, - ] + suggestionContext = { + groups: [ + { name: 'Test1', project: unwrap(tryQualifiedName('Standard.Base')) }, + { name: 'Test2', project: unwrap(tryQualifiedName('Standard.Base')) }, + ], + currentProject: 'local.Mock_Project' as QualifiedName, + } arg1 = { name: 'a', reprType: 'Any', @@ -394,6 +413,29 @@ class Fixture { scope: this.scope, annotations: [], } + expectedLocalStaticMethod: SuggestionEntry = { + kind: SuggestionKind.Method, + arguments: [ + { + name: 'a', + reprType: 'Standard.Base.Any.Any', + isSuspended: false, + hasDefault: false, + defaultValue: null, + tagValues: null, + }, + ], + annotations: [], + name: unwrap(tryIdentifier('collapsed')), + definedIn: unwrap(tryQualifiedName('local.Mock_Project')), + documentation: [{ Tag: { tag: 'Icon', body: 'group' } }, { Paragraph: { body: '' } }], + iconName: 'group', + aliases: [], + isPrivate: false, + isUnstable: false, + memberOf: unwrap(tryQualifiedName('local.Mock_Project')), + returnType: 'Standard.Base.Any.Any', + } addUpdatesForExpected(): lsTypes.SuggestionsDatabaseUpdate[] { return [ @@ -490,6 +532,30 @@ class Fixture { documentation: this.localDocs, }, }, + { + type: 'Add', + id: 8, + suggestion: { + type: 'method', + module: 'local.Mock_Project.Main', + name: 'collapsed', + arguments: [ + { + name: 'a', + reprType: 'Standard.Base.Any.Any', + isSuspended: false, + hasDefault: false, + defaultValue: null, + tagValues: null, + }, + ], + selfType: 'local.Mock_Project.Main', + returnType: 'Standard.Base.Any.Any', + isStatic: true, + documentation: ' ICON group', + annotations: [], + }, + }, ] } @@ -502,6 +568,7 @@ class Fixture { db.set(5, structuredClone(this.expectedStaticMethod)) db.set(6, structuredClone(this.expectedFunction)) db.set(7, structuredClone(this.expectedLocal)) + db.set(8, structuredClone(this.expectedLocalStaticMethod)) return db } @@ -513,5 +580,6 @@ class Fixture { expect(db.get(5)).toStrictEqual(this.expectedStaticMethod) expect(db.get(6)).toStrictEqual(this.expectedFunction) expect(db.get(7)).toStrictEqual(this.expectedLocal) + expect(db.get(8)).toStrictEqual(this.expectedLocalStaticMethod) } } diff --git a/app/gui/src/project-view/stores/suggestionDatabase/documentation.ts b/app/gui/src/project-view/stores/suggestionDatabase/documentation.ts index c92631f03748..93c25504e4e5 100644 --- a/app/gui/src/project-view/stores/suggestionDatabase/documentation.ts +++ b/app/gui/src/project-view/stores/suggestionDatabase/documentation.ts @@ -4,6 +4,7 @@ import { isSome, type Opt } from '@/util/data/opt' import { parseDocs, type Doc } from '@/util/docParser' import type { Icon } from '@/util/iconName' import { type QualifiedName } from '@/util/qualifiedName' +import { type DeepReadonly } from 'vue' export interface DocumentationData { documentation: Doc.Section[] @@ -31,7 +32,7 @@ export function tagValue(doc: Doc.Section[], tag: string): Opt { export function getGroupIndex( groupName: string, entryModule: QualifiedName, - groups: Group[], + groups: DeepReadonly, ): Opt { let normalized: string if (groupName.indexOf('.') >= 0) { @@ -48,7 +49,7 @@ export function getGroupIndex( export function documentationData( documentation: Opt, definedIn: QualifiedName, - groups: Group[], + groups: DeepReadonly, ): DocumentationData { const parsed = documentation != null ? parseDocs(documentation) : [] const groupName = tagValue(parsed, 'Group') diff --git a/app/gui/src/project-view/stores/suggestionDatabase/entry.ts b/app/gui/src/project-view/stores/suggestionDatabase/entry.ts index 956b4d73e7e2..c9ee80d34293 100644 --- a/app/gui/src/project-view/stores/suggestionDatabase/entry.ts +++ b/app/gui/src/project-view/stores/suggestionDatabase/entry.ts @@ -1,21 +1,13 @@ -import { assert } from '@/util/assert' import type { Doc } from '@/util/docParser' import type { Icon } from '@/util/iconName' import type { IdentifierOrOperatorIdentifier, QualifiedName } from '@/util/qualifiedName' -import { - isIdentifierOrOperatorIdentifier, - isQualifiedName, - qnJoin, - qnLastSegment, - qnParent, - qnSegments, - qnSplit, -} from '@/util/qualifiedName' +import { qnJoin, qnParent, qnSegments } from '@/util/qualifiedName' import type { MethodPointer } from 'ydoc-shared/languageServerTypes' import type { SuggestionEntryArgument, SuggestionEntryScope, } from 'ydoc-shared/languageServerTypes/suggestions' + export type { SuggestionEntryArgument, SuggestionEntryScope, @@ -119,128 +111,6 @@ export function suggestionDocumentationUrl(entry: SuggestionEntry): string | und return segments.join('/') } -function makeSimpleEntry( - kind: SuggestionKind, - definedIn: QualifiedName, - name: IdentifierOrOperatorIdentifier, - returnType: QualifiedName, -): SuggestionEntry { - return { - kind, - definedIn, - name, - isPrivate: false, - isUnstable: false, - aliases: [], - arguments: [], - returnType, - documentation: [], - annotations: [], - } -} - -/** TODO: Add docs */ -export function makeModule(fqn: string): SuggestionEntry { - assert(isQualifiedName(fqn)) - return makeSimpleEntry(SuggestionKind.Module, fqn, qnLastSegment(fqn), fqn) -} - -/** TODO: Add docs */ -export function makeType(fqn: string): SuggestionEntry { - assert(isQualifiedName(fqn)) - const [definedIn, name] = qnSplit(fqn) - assert(definedIn != null) - return makeSimpleEntry(SuggestionKind.Type, definedIn, name, fqn) -} - -/** TODO: Add docs */ -export function makeConstructor(fqn: string): SuggestionEntry { - assert(isQualifiedName(fqn)) - const [type, name] = qnSplit(fqn) - assert(type != null) - const definedIn = qnParent(type) - assert(definedIn != null) - return { - memberOf: type, - ...makeSimpleEntry(SuggestionKind.Constructor, definedIn, name, type), - } -} - -/** TODO: Add docs */ -export function makeMethod(fqn: string, returnType: string = 'Any'): SuggestionEntry { - assert(isQualifiedName(fqn)) - assert(isQualifiedName(returnType)) - const [type, name] = qnSplit(fqn) - assert(type != null) - const definedIn = qnParent(type) - assert(definedIn != null) - return { - memberOf: type, - selfType: type, - ...makeSimpleEntry(SuggestionKind.Method, definedIn, name, returnType), - } -} - -/** TODO: Add docs */ -export function makeStaticMethod(fqn: string, returnType: string = 'Any'): SuggestionEntry { - assert(isQualifiedName(fqn)) - assert(isQualifiedName(returnType)) - const [type, name] = qnSplit(fqn) - assert(type != null) - const definedIn = qnParent(type) - assert(definedIn != null) - return { - memberOf: type, - ...makeSimpleEntry(SuggestionKind.Method, definedIn, name, returnType), - } -} - -/** TODO: Add docs */ -export function makeModuleMethod(fqn: string, returnType: string = 'Any'): SuggestionEntry { - assert(isQualifiedName(fqn)) - assert(isQualifiedName(returnType)) - const [definedIn, name] = qnSplit(fqn) - assert(definedIn != null) - return { - memberOf: definedIn, - ...makeSimpleEntry(SuggestionKind.Method, definedIn, name, returnType), - } -} - -/** TODO: Add docs */ -export function makeFunction( - definedIn: string, - name: string, - returnType: string = 'Any', -): SuggestionEntry { - assert(isQualifiedName(definedIn)) - assert(isIdentifierOrOperatorIdentifier(name)) - assert(isQualifiedName(returnType)) - return makeSimpleEntry(SuggestionKind.Function, definedIn, name, returnType) -} - -/** TODO: Add docs */ -export function makeLocal( - definedIn: string, - name: string, - returnType: string = 'Any', -): SuggestionEntry { - assert(isQualifiedName(definedIn)) - assert(isIdentifierOrOperatorIdentifier(name)) - assert(isQualifiedName(returnType)) - return makeSimpleEntry(SuggestionKind.Local, definedIn, name, returnType) -} - -/** TODO: Add docs */ -export function makeArgument(name: string, type: string = 'Any'): SuggestionEntryArgument { - return { - name, - reprType: type, - isSuspended: false, - hasDefault: false, - } -} - /** `true` if calling the function without providing a value for this argument will result in an error. */ export function isRequiredArgument(info: SuggestionEntryArgument) { return !!info.defaultValue?.startsWith('Missing_Argument.') diff --git a/app/gui/src/project-view/stores/suggestionDatabase/index.ts b/app/gui/src/project-view/stores/suggestionDatabase/index.ts index 868fb9ec2a53..290c6b4c6b6d 100644 --- a/app/gui/src/project-view/stores/suggestionDatabase/index.ts +++ b/app/gui/src/project-view/stores/suggestionDatabase/index.ts @@ -1,7 +1,7 @@ import { createContextStore } from '@/providers' import { type ProjectStore } from '@/stores/project' import { entryQn, type SuggestionEntry, type SuggestionId } from '@/stores/suggestionDatabase/entry' -import { applyUpdates, entryFromLs } from '@/stores/suggestionDatabase/lsUpdate' +import { SuggestionUpdateProcessor } from '@/stores/suggestionDatabase/lsUpdate' import { ReactiveDb, ReactiveIndex } from '@/util/database/reactiveDb' import { AsyncQueue } from '@/util/net' import { @@ -11,7 +11,7 @@ import { tryQualifiedName, type QualifiedName, } from '@/util/qualifiedName' -import { markRaw, proxyRefs, ref, type Ref } from 'vue' +import { markRaw, proxyRefs, readonly, ref } from 'vue' import { LanguageServer } from 'ydoc-shared/languageServer' import type { MethodPointer } from 'ydoc-shared/languageServerTypes' import * as lsTypes from 'ydoc-shared/languageServerTypes/suggestions' @@ -79,18 +79,17 @@ class Synchronizer { constructor( projectStore: ProjectStore, public entries: SuggestionDb, - public groups: Ref, + updateProcessor: Promise, ) { const lsRpc = projectStore.lsRpcConnection const initState = exponentialBackoff(() => lsRpc.acquireCapability('search/receivesSuggestionsDatabaseUpdates', {}), - ).then((capability) => { + ).then(async (capability) => { if (!capability.ok) { capability.error.log('Will not receive database updates') } - this.#setupUpdateHandler(lsRpc) - this.#loadGroups(lsRpc, projectStore.firstExecution) - return Synchronizer.loadDatabase(entries, lsRpc, groups.value) + this.#setupUpdateHandler(lsRpc, await updateProcessor) + return Synchronizer.loadDatabase(entries, lsRpc, await updateProcessor) }) this.queue = new AsyncQueue(initState) @@ -99,7 +98,7 @@ class Synchronizer { static async loadDatabase( entries: SuggestionDb, lsRpc: LanguageServer, - groups: Group[], + updateProcessor: SuggestionUpdateProcessor, ): Promise<{ currentVersion: number }> { const initialDb = await exponentialBackoff(() => lsRpc.getSuggestionsDatabase()) if (!initialDb.ok) { @@ -109,7 +108,7 @@ class Synchronizer { return { currentVersion: 0 } } for (const lsEntry of initialDb.value.entries) { - const entry = entryFromLs(lsEntry.suggestion, groups) + const entry = updateProcessor.entryFromLs(lsEntry.suggestion) if (!entry.ok) { entry.error.log() console.error(`Skipping entry ${lsEntry.id}, the suggestion database will be incomplete!`) @@ -120,7 +119,7 @@ class Synchronizer { return { currentVersion: initialDb.value.currentVersion } } - #setupUpdateHandler(lsRpc: LanguageServer) { + #setupUpdateHandler(lsRpc: LanguageServer, updateProcessor: SuggestionUpdateProcessor) { lsRpc.on('search/suggestionsDatabaseUpdates', (param) => { this.queue.pushTask(async ({ currentVersion }) => { // There are rare cases where the database is updated twice in quick succession, with the @@ -140,33 +139,30 @@ class Synchronizer { ) return { currentVersion } } else { - applyUpdates(this.entries, param.updates, this.groups.value) + updateProcessor.applyUpdates(this.entries, param.updates) return { currentVersion: param.currentVersion } } }) }) } +} - async #loadGroups(lsRpc: LanguageServer, firstExecution: Promise) { - this.queue.pushTask(async ({ currentVersion }) => { - await firstExecution - const groups = await exponentialBackoff(() => lsRpc.getComponentGroups()) - if (!groups.ok) { - if (!lsRpc.isDisposed) { - groups.error.log('Cannot read component groups. Continuing without groups') - } - return { currentVersion } - } - this.groups.value = groups.value.componentGroups.map( - (group): Group => ({ - name: group.name, - ...(group.color ? { color: group.color } : {}), - project: group.library as QualifiedName, - }), - ) - return { currentVersion } - }) +async function loadGroups(lsRpc: LanguageServer, firstExecution: Promise) { + await firstExecution + const groups = await exponentialBackoff(() => lsRpc.getComponentGroups()) + if (!groups.ok) { + if (!lsRpc.isDisposed) { + groups.error.log('Cannot read component groups. Continuing without groups') + } + return [] } + return groups.value.componentGroups.map( + (group): Group => ({ + name: group.name, + ...(group.color ? { color: group.color } : {}), + project: group.library as QualifiedName, + }), + ) } /** {@link useSuggestionDbStore} composable object */ @@ -177,23 +173,32 @@ export const [provideSuggestionDbStore, useSuggestionDbStore] = createContextSto const entries = new SuggestionDb() const groups = ref([]) + const updateProcessor = loadGroups( + projectStore.lsRpcConnection, + projectStore.firstExecution, + ).then((loadedGroups) => { + groups.value = loadedGroups + return new SuggestionUpdateProcessor(loadedGroups) + }) + /** Add an entry to the suggestion database. */ function mockSuggestion(entry: lsTypes.SuggestionEntry) { const id = Math.max(...entries.nameToId.reverse.keys()) + 1 - applyUpdates( - entries, - [ - { - type: 'Add', - id, - suggestion: entry, - }, - ], - groups.value, - ) + new SuggestionUpdateProcessor([]).applyUpdates(entries, [ + { + type: 'Add', + id, + suggestion: entry, + }, + ]) } - const _synchronizer = new Synchronizer(projectStore, entries, groups) - return proxyRefs({ entries: markRaw(entries), groups, _synchronizer, mockSuggestion }) + const _synchronizer = new Synchronizer(projectStore, entries, updateProcessor) + return proxyRefs({ + entries: markRaw(entries), + groups: readonly(groups), + _synchronizer, + mockSuggestion, + }) }, ) diff --git a/app/gui/src/project-view/stores/suggestionDatabase/lsUpdate.ts b/app/gui/src/project-view/stores/suggestionDatabase/lsUpdate.ts index acb7a3bbf5e2..ab7c01494f12 100644 --- a/app/gui/src/project-view/stores/suggestionDatabase/lsUpdate.ts +++ b/app/gui/src/project-view/stores/suggestionDatabase/lsUpdate.ts @@ -7,14 +7,11 @@ import { SuggestionKind, type SuggestionEntry, type SuggestionEntryArgument, - type SuggestionEntryScope, type Typename, } from '@/stores/suggestionDatabase/entry' import { assert, assertNever } from '@/util/assert' import { type Opt } from '@/util/data/opt' import { Err, Ok, withContext, type Result } from '@/util/data/result' -import type { Doc } from '@/util/docParser' -import type { Icon } from '@/util/iconName' import { normalizeQualifiedName, qnJoin, @@ -24,404 +21,402 @@ import { type IdentifierOrOperatorIdentifier, type QualifiedName, } from '@/util/qualifiedName' +import { type ToValue } from '@/util/reactivity' +import { toValue, type DeepReadonly } from 'vue' import * as lsTypes from 'ydoc-shared/languageServerTypes/suggestions' +import { + SuggestionArgumentUpdate, + SuggestionsDatabaseUpdate, +} from 'ydoc-shared/languageServerTypes/suggestions' -interface UnfinishedEntry { +interface UnfinishedEntry extends Partial { kind: SuggestionKind - definedIn?: QualifiedName - memberOf?: QualifiedName - isPrivate?: boolean - isUnstable?: boolean - name?: IdentifierOrOperatorIdentifier - aliases?: string[] - selfType?: Typename - arguments?: SuggestionEntryArgument[] - returnType?: Typename - parentType?: QualifiedName - reexportedIn?: QualifiedName - documentation?: Doc.Section[] - scope?: SuggestionEntryScope - iconName?: Icon - groupIndex?: number | undefined - annotations?: string[] } -function setLsName( - entry: UnfinishedEntry, - name: string, -): entry is UnfinishedEntry & { name: IdentifierOrOperatorIdentifier } { - const ident = tryIdentifierOrOperatorIdentifier(name) - if (!ident.ok) return false - entry.name = ident.value - return true -} +/** Interprets language server messages to create and update suggestion database entries. */ +export class SuggestionUpdateProcessor { + /** Constructor. */ + constructor(private readonly groups: ToValue>) {} -function setLsModule( - entry: UnfinishedEntry & { name: IdentifierOrOperatorIdentifier }, - module: string, -): entry is UnfinishedEntry & { name: IdentifierOrOperatorIdentifier; definedIn: QualifiedName } { - const qn = tryQualifiedName(module) - if (!qn.ok) return false - const normalizedQn = normalizeQualifiedName(qn.value) - entry.definedIn = normalizedQn - switch (entry.kind) { - case SuggestionKind.Module: - entry.name = qnLastSegment(normalizedQn) - entry.returnType = normalizedQn - break - case SuggestionKind.Type: - entry.returnType = qnJoin(normalizedQn, entry.name) - break + private setLsName( + entry: UnfinishedEntry, + name: string, + ): entry is UnfinishedEntry & { name: IdentifierOrOperatorIdentifier } { + const ident = tryIdentifierOrOperatorIdentifier(name) + if (!ident.ok) return false + entry.name = ident.value + return true } - return true -} -function setAsOwner(entry: UnfinishedEntry, type: string) { - const qn = tryQualifiedName(type) - if (qn.ok) { - entry.memberOf = normalizeQualifiedName(qn.value) - } else { - delete entry.memberOf + private setLsModule( + entry: UnfinishedEntry & { name: IdentifierOrOperatorIdentifier }, + module: string, + ): entry is UnfinishedEntry & { name: IdentifierOrOperatorIdentifier; definedIn: QualifiedName } { + const qn = tryQualifiedName(module) + if (!qn.ok) return false + const normalizedQn = normalizeQualifiedName(qn.value) + entry.definedIn = normalizedQn + switch (entry.kind) { + case SuggestionKind.Module: + entry.name = qnLastSegment(normalizedQn) + entry.returnType = normalizedQn + break + case SuggestionKind.Type: + entry.returnType = qnJoin(normalizedQn, entry.name) + break + } + return true } -} -function setLsSelfType(entry: UnfinishedEntry, selfType: Typename, isStaticParam?: boolean) { - const isStatic = isStaticParam ?? entry.selfType == null - if (!isStatic) entry.selfType = selfType - setAsOwner(entry, selfType) -} + private setAsOwner(entry: UnfinishedEntry, type: string) { + const qn = tryQualifiedName(type) + if (qn.ok) { + entry.memberOf = normalizeQualifiedName(qn.value) + } else { + delete entry.memberOf + } + } -function setLsReturnType( - entry: UnfinishedEntry, - returnType: Typename, -): asserts entry is UnfinishedEntry & { returnType: Typename } { - entry.returnType = returnType - if (entry.kind == SuggestionKind.Constructor) { - setAsOwner(entry, returnType) + private setLsSelfType(entry: UnfinishedEntry, selfType: Typename, isStaticParam?: boolean) { + const isStatic = isStaticParam ?? entry.selfType == null + if (!isStatic) entry.selfType = selfType + this.setAsOwner(entry, selfType) } -} -function setLsReexported( - entry: UnfinishedEntry, - reexported: string, -): entry is UnfinishedEntry & { reexprotedIn: QualifiedName } { - const qn = tryQualifiedName(reexported) - if (!qn.ok) return false - entry.reexportedIn = normalizeQualifiedName(qn.value) - return true -} + private setLsReturnType( + entry: UnfinishedEntry, + returnType: Typename, + ): asserts entry is UnfinishedEntry & { returnType: Typename } { + entry.returnType = returnType + if (entry.kind == SuggestionKind.Constructor) { + this.setAsOwner(entry, returnType) + } + } -function setLsParentType( - entry: UnfinishedEntry, - parentType: string, -): entry is UnfinishedEntry & { parentType: QualifiedName } { - const qn = tryQualifiedName(parentType) - if (!qn.ok) return false - entry.parentType = normalizeQualifiedName(qn.value) - return true -} + private setLsReexported( + entry: UnfinishedEntry, + reexported: string, + ): entry is UnfinishedEntry & { reexprotedIn: QualifiedName } { + const qn = tryQualifiedName(reexported) + if (!qn.ok) return false + entry.reexportedIn = normalizeQualifiedName(qn.value) + return true + } -function setLsDocumentation( - entry: UnfinishedEntry & { definedIn: QualifiedName }, - documentation: Opt, - groups: Group[], -): asserts entry is UnfinishedEntry & { definedIn: QualifiedName } & DocumentationData { - const data = documentationData(documentation, entry.definedIn, groups) - Object.assign(entry, data) - // Removing optional fields. I don't know a better way to do this. - if (data.groupIndex == null) delete entry.groupIndex - if (data.iconName == null) delete entry.iconName -} + private setLsParentType( + entry: UnfinishedEntry, + parentType: string, + ): entry is UnfinishedEntry & { parentType: QualifiedName } { + const qn = tryQualifiedName(parentType) + if (!qn.ok) return false + entry.parentType = normalizeQualifiedName(qn.value) + return true + } + + private setLsDocumentation( + entry: UnfinishedEntry & { definedIn: QualifiedName }, + documentation: Opt, + ): asserts entry is UnfinishedEntry & { definedIn: QualifiedName } & DocumentationData { + const data = documentationData(documentation, entry.definedIn, toValue(this.groups)) + Object.assign(entry, data) + // Removing optional fields. I don't know a better way to do this. + if (data.groupIndex == null) delete entry.groupIndex + if (data.iconName == null) delete entry.iconName + } -/** TODO: Add docs */ -export function entryFromLs( - lsEntry: lsTypes.SuggestionEntry, - groups: Group[], -): Result { - return withContext( - () => `when creating entry`, - () => { - switch (lsEntry.type) { - case 'function': { - const entry = { - kind: SuggestionKind.Function, - annotations: [], + /** Create a suggestion DB entry from data provided by the given language server. */ + entryFromLs(lsEntry: lsTypes.SuggestionEntry): Result { + return withContext( + () => `when creating entry`, + () => { + switch (lsEntry.type) { + case 'function': { + const entry = { + kind: SuggestionKind.Function, + annotations: [], + } + if (!this.setLsName(entry, lsEntry.name)) return Err('Invalid name') + if (!this.setLsModule(entry, lsEntry.module)) return Err('Invalid module name') + this.setLsReturnType(entry, lsEntry.returnType) + this.setLsDocumentation(entry, lsEntry.documentation) + return Ok({ + scope: lsEntry.scope, + arguments: lsEntry.arguments, + ...entry, + }) } - if (!setLsName(entry, lsEntry.name)) return Err('Invalid name') - if (!setLsModule(entry, lsEntry.module)) return Err('Invalid module name') - setLsReturnType(entry, lsEntry.returnType) - setLsDocumentation(entry, lsEntry.documentation, groups) - return Ok({ - scope: lsEntry.scope, - arguments: lsEntry.arguments, - ...entry, - }) - } - case 'module': { - const entry = { - kind: SuggestionKind.Module, - name: 'MODULE' as IdentifierOrOperatorIdentifier, - arguments: [], - returnType: '', - annotations: [], + case 'module': { + const entry = { + kind: SuggestionKind.Module, + name: 'MODULE' as IdentifierOrOperatorIdentifier, + arguments: [], + returnType: '', + annotations: [], + } + if (!this.setLsModule(entry, lsEntry.module)) return Err('Invalid module name') + if (lsEntry.reexport != null && !this.setLsReexported(entry, lsEntry.reexport)) + return Err('Invalid reexported module name') + this.setLsDocumentation(entry, lsEntry.documentation) + assert(entry.returnType !== '') // Should be overwriten + return Ok(entry) } - if (!setLsModule(entry, lsEntry.module)) return Err('Invalid module name') - if (lsEntry.reexport != null && !setLsReexported(entry, lsEntry.reexport)) - return Err('Invalid reexported module name') - setLsDocumentation(entry, lsEntry.documentation, groups) - assert(entry.returnType !== '') // Should be overwriten - return Ok(entry) - } - case 'type': { - const entry = { - kind: SuggestionKind.Type, - returnType: '', - annotations: [], + case 'type': { + const entry = { + kind: SuggestionKind.Type, + returnType: '', + annotations: [], + } + if (!this.setLsName(entry, lsEntry.name)) return Err('Invalid name') + if (!this.setLsModule(entry, lsEntry.module)) return Err('Invalid module name') + if (lsEntry.reexport != null && !this.setLsReexported(entry, lsEntry.reexport)) + return Err('Invalid reexported module name') + if (lsEntry.parentType != null && !this.setLsParentType(entry, lsEntry.parentType)) + return Err('Invalid parent type') + this.setLsDocumentation(entry, lsEntry.documentation) + assert(entry.returnType !== '') // Should be overwriten + return Ok({ + arguments: lsEntry.params, + ...entry, + }) } - if (!setLsName(entry, lsEntry.name)) return Err('Invalid name') - if (!setLsModule(entry, lsEntry.module)) return Err('Invalid module name') - if (lsEntry.reexport != null && !setLsReexported(entry, lsEntry.reexport)) - return Err('Invalid reexported module name') - if (lsEntry.parentType != null && !setLsParentType(entry, lsEntry.parentType)) - return Err('Invalid parent type') - setLsDocumentation(entry, lsEntry.documentation, groups) - assert(entry.returnType !== '') // Should be overwriten - return Ok({ - arguments: lsEntry.params, - ...entry, - }) - } - case 'constructor': { - const entry = { kind: SuggestionKind.Constructor } - if (!setLsName(entry, lsEntry.name)) return Err('Invalid name') - if (!setLsModule(entry, lsEntry.module)) return Err('Invalid module name') - if (lsEntry.reexport != null && !setLsReexported(entry, lsEntry.reexport)) - return Err('Invalid reexported module name') - setLsDocumentation(entry, lsEntry.documentation, groups) - setLsReturnType(entry, lsEntry.returnType) - return Ok({ - arguments: lsEntry.arguments, - annotations: lsEntry.annotations, - ...entry, - }) - } - case 'method': { - const entry = { kind: SuggestionKind.Method } - if (!setLsName(entry, lsEntry.name)) return Err('Invalid name') - if (!setLsModule(entry, lsEntry.module)) return Err('Invalid module name') - if (lsEntry.reexport != null && !setLsReexported(entry, lsEntry.reexport)) - return Err('Invalid reexported module name') - setLsDocumentation(entry, lsEntry.documentation, groups) - setLsSelfType(entry, lsEntry.selfType, lsEntry.isStatic) - setLsReturnType(entry, lsEntry.returnType) - return Ok({ - arguments: lsEntry.arguments, - annotations: lsEntry.annotations, - ...entry, - }) - } - case 'local': { - const entry = { - kind: SuggestionKind.Local, - arguments: [], - annotations: [], + case 'constructor': { + const entry = { kind: SuggestionKind.Constructor } + if (!this.setLsName(entry, lsEntry.name)) return Err('Invalid name') + if (!this.setLsModule(entry, lsEntry.module)) return Err('Invalid module name') + if (lsEntry.reexport != null && !this.setLsReexported(entry, lsEntry.reexport)) + return Err('Invalid reexported module name') + this.setLsDocumentation(entry, lsEntry.documentation) + this.setLsReturnType(entry, lsEntry.returnType) + return Ok({ + arguments: lsEntry.arguments, + annotations: lsEntry.annotations, + ...entry, + }) } - if (!setLsName(entry, lsEntry.name)) return Err('Invalid name') - if (!setLsModule(entry, lsEntry.module)) return Err('Invalid module name') - setLsReturnType(entry, lsEntry.returnType) - setLsDocumentation(entry, lsEntry.documentation, groups) - return Ok({ - scope: lsEntry.scope, - ...entry, - }) - } - default: - assertNever(lsEntry) - } - }, - ) -} - -function applyFieldUpdate( - name: K, - update: { [P in K]?: lsTypes.FieldUpdate }, - updater: (newValue: T) => R, -): Result> { - const field = update[name] - if (field == null) return Ok(null) - return withContext( - () => `when handling field "${name}" update`, - () => { - switch (field.tag) { - case 'Set': - if (field.value != null) { - return Ok(updater(field.value)) - } else { - return Err('Received "Set" update with no value') + case 'method': { + const entry = { kind: SuggestionKind.Method } + if (!this.setLsName(entry, lsEntry.name)) return Err('Invalid name') + if (!this.setLsModule(entry, lsEntry.module)) return Err('Invalid module name') + if (lsEntry.reexport != null && !this.setLsReexported(entry, lsEntry.reexport)) + return Err('Invalid reexported module name') + this.setLsDocumentation(entry, lsEntry.documentation) + this.setLsSelfType(entry, lsEntry.selfType, lsEntry.isStatic) + this.setLsReturnType(entry, lsEntry.returnType) + return Ok({ + arguments: lsEntry.arguments, + annotations: lsEntry.annotations, + ...entry, + }) } - case 'Remove': - return Err(`Received "Remove" for non-optional field`) - default: - return Err(`Received field update with unknown value`) - } - }, - ) -} + case 'local': { + const entry = { + kind: SuggestionKind.Local, + arguments: [], + annotations: [], + } + if (!this.setLsName(entry, lsEntry.name)) return Err('Invalid name') + if (!this.setLsModule(entry, lsEntry.module)) return Err('Invalid module name') + this.setLsReturnType(entry, lsEntry.returnType) + this.setLsDocumentation(entry, lsEntry.documentation) + return Ok({ + scope: lsEntry.scope, + ...entry, + }) + } + default: + assertNever(lsEntry) + } + }, + ) + } -function applyPropertyUpdate( - name: K, - obj: { [P in K]: T }, - update: { [P in K]?: lsTypes.FieldUpdate }, -): Result { - const apply = applyFieldUpdate(name, update, (newValue) => { - obj[name] = newValue - }) - if (!apply.ok) return apply - return Ok(undefined) -} + private applyFieldUpdate( + name: K, + update: { [P in K]?: lsTypes.FieldUpdate }, + updater: (newValue: T) => R, + ): Result> { + const field = update[name] + if (field == null) return Ok(null) + return withContext( + () => `when handling field "${name}" update`, + () => { + switch (field.tag) { + case 'Set': + if (field.value != null) { + return Ok(updater(field.value)) + } else { + return Err('Received "Set" update with no value') + } + case 'Remove': + return Err(`Received "Remove" for non-optional field`) + default: + return Err(`Received field update with unknown value`) + } + }, + ) + } -function applyOptPropertyUpdate( - name: K, - obj: { [P in K]?: T }, - update: { [P in K]?: lsTypes.FieldUpdate }, -) { - const field = update[name] - switch (field?.tag) { - case 'Set': - obj[name] = field.value - break - case 'Remove': - delete obj[name] - break + private applyPropertyUpdate( + name: K, + obj: { [P in K]: T }, + update: { [P in K]?: lsTypes.FieldUpdate }, + ): Result { + const apply = this.applyFieldUpdate(name, update, (newValue) => { + obj[name] = newValue + }) + if (!apply.ok) return apply + return Ok() } -} -function applyArgumentsUpdate( - args: SuggestionEntryArgument[], - update: lsTypes.SuggestionArgumentUpdate, -): Result { - switch (update.type) { - case 'Add': { - args.splice(update.index, 0, update.argument) - return Ok(undefined) - } - case 'Remove': { - args.splice(update.index, 1) - return Ok(undefined) - } - case 'Modify': { - return withContext( - () => `when modifying argument with index ${update.index}`, - () => { - const arg = args[update.index] - if (arg == null) return Err(`Wrong argument index ${update.index}`) - const nameUpdate = applyPropertyUpdate('name', arg, update) - if (!nameUpdate.ok) return nameUpdate - const typeUpdate = applyFieldUpdate('reprType', update, (type) => { - arg.reprType = type - }) - if (!typeUpdate.ok) return typeUpdate - const isSuspendedUpdate = applyPropertyUpdate('isSuspended', arg, update) - if (!isSuspendedUpdate.ok) return isSuspendedUpdate - const hasDefaultUpdate = applyPropertyUpdate('hasDefault', arg, update) - if (!hasDefaultUpdate.ok) return hasDefaultUpdate - applyOptPropertyUpdate('defaultValue', arg, update) - return Ok(undefined) - }, - ) + private applyOptPropertyUpdate( + name: K, + obj: { [P in K]?: T }, + update: { [P in K]?: lsTypes.FieldUpdate }, + ) { + const field = update[name] + switch (field?.tag) { + case 'Set': + obj[name] = field.value + break + case 'Remove': + delete obj[name] + break } } -} -/** TODO: Add docs */ -export function applyUpdate( - entries: SuggestionDb, - update: lsTypes.SuggestionsDatabaseUpdate, - groups: Group[], -): Result { - switch (update.type) { - case 'Add': { - return withContext( - () => `when adding new entry ${JSON.stringify(update)}`, - () => { - const newEntry = entryFromLs(update.suggestion, groups) - if (!newEntry.ok) return newEntry - entries.set(update.id, newEntry.value) - return Ok(undefined) - }, - ) - } - case 'Remove': { - if (!entries.delete(update.id)) { - return Err(`Received "Remove" suggestion database update for non-existing id ${update.id}.`) + private applyArgumentsUpdate( + args: SuggestionEntryArgument[], + update: lsTypes.SuggestionArgumentUpdate, + ): Result { + switch (update.type) { + case 'Add': { + args.splice(update.index, 0, update.argument) + return Ok() + } + case 'Remove': { + args.splice(update.index, 1) + return Ok() + } + case 'Modify': { + return withContext( + () => `when modifying argument with index ${update.index}`, + () => { + const arg = args[update.index] + if (arg == null) return Err(`Wrong argument index ${update.index}`) + return this.modifyArgument(arg, update) + }, + ) } - return Ok(undefined) } - case 'Modify': { - return withContext( - () => `when modifying entry to ${JSON.stringify(update)}`, - () => { - const entry = entries.get(update.id) - if (entry == null) { - return Err(`Entry with id ${update.id} does not exist.`) - } + } - for (const argumentUpdate of update.arguments ?? []) { - const updateResult = applyArgumentsUpdate(entry.arguments, argumentUpdate) - if (!updateResult.ok) return updateResult - } + private modifyArgument( + arg: SuggestionEntryArgument, + update: SuggestionArgumentUpdate.Modify, + ): Result { + const nameUpdate = this.applyPropertyUpdate('name', arg, update) + if (!nameUpdate.ok) return nameUpdate + const typeUpdate = this.applyFieldUpdate('reprType', update, (type) => { + arg.reprType = type + }) + if (!typeUpdate.ok) return typeUpdate + const isSuspendedUpdate = this.applyPropertyUpdate('isSuspended', arg, update) + if (!isSuspendedUpdate.ok) return isSuspendedUpdate + const hasDefaultUpdate = this.applyPropertyUpdate('hasDefault', arg, update) + if (!hasDefaultUpdate.ok) return hasDefaultUpdate + this.applyOptPropertyUpdate('defaultValue', arg, update) + return Ok() + } - const moduleUpdate = applyFieldUpdate('module', update, (module) => - setLsModule(entry, module), + private applyUpdate( + entries: SuggestionDb, + update: lsTypes.SuggestionsDatabaseUpdate, + ): Result { + switch (update.type) { + case 'Add': { + return withContext( + () => `when adding new entry ${JSON.stringify(update)}`, + () => { + const newEntry = this.entryFromLs(update.suggestion) + if (!newEntry.ok) return newEntry + entries.set(update.id, newEntry.value) + return Ok() + }, + ) + } + case 'Remove': { + if (!entries.delete(update.id)) { + return Err( + `Received "Remove" suggestion database update for non-existing id ${update.id}.`, ) - if (!moduleUpdate.ok) return moduleUpdate - if (moduleUpdate.value === false) return Err('Invalid module name') + } + return Ok() + } + case 'Modify': { + return withContext( + () => `when modifying entry to ${JSON.stringify(update)}`, + () => { + const entry = entries.get(update.id) + if (entry == null) return Err(`Entry with id ${update.id} does not exist.`) + return this.modifyEntry(entry, update) + }, + ) + } + } + } - const selfTypeUpdate = applyFieldUpdate('selfType', update, (selfType) => - setLsSelfType(entry, selfType), - ) - if (!selfTypeUpdate.ok) return selfTypeUpdate + private modifyEntry( + entry: SuggestionEntry, + update: SuggestionsDatabaseUpdate.Modify, + ): Result { + for (const argumentUpdate of update.arguments ?? []) { + const updateResult = this.applyArgumentsUpdate(entry.arguments, argumentUpdate) + if (!updateResult.ok) return updateResult + } - const returnTypeUpdate = applyFieldUpdate('returnType', update, (returnType) => { - setLsReturnType(entry, returnType) - }) - if (!returnTypeUpdate.ok) return returnTypeUpdate + const moduleUpdate = this.applyFieldUpdate('module', update, (module) => + this.setLsModule(entry, module), + ) + if (!moduleUpdate.ok) return moduleUpdate + if (moduleUpdate.value === false) return Err('Invalid module name') - if (update.documentation != null) - setLsDocumentation(entry, update.documentation.value, groups) + const selfTypeUpdate = this.applyFieldUpdate('selfType', update, (selfType) => + this.setLsSelfType(entry, selfType), + ) + if (!selfTypeUpdate.ok) return selfTypeUpdate - applyOptPropertyUpdate('scope', entry, update) + const returnTypeUpdate = this.applyFieldUpdate('returnType', update, (returnType) => { + this.setLsReturnType(entry, returnType) + }) + if (!returnTypeUpdate.ok) return returnTypeUpdate - if (update.reexport != null) { - if (update.reexport.value != null) { - const reexport = tryQualifiedName(update.reexport.value) - if (!reexport.ok) return reexport - entry.reexportedIn = reexport.value - } else { - delete entry.reexportedIn - } - } + if (update.documentation != null) this.setLsDocumentation(entry, update.documentation.value) + + this.applyOptPropertyUpdate('scope', entry, update) - return Ok(undefined) - }, - ) + if (update.reexport != null) { + if (update.reexport.value != null) { + const reexport = tryQualifiedName(update.reexport.value) + if (!reexport.ok) return reexport + entry.reexportedIn = reexport.value + } else { + delete entry.reexportedIn + } } + + return Ok() } -} -/** TODO: Add docs */ -export function applyUpdates( - entries: SuggestionDb, - updates: lsTypes.SuggestionsDatabaseUpdate[], - groups: Group[], -) { - for (const update of updates) { - const updateResult = applyUpdate(entries, update, groups) - if (!updateResult.ok) { - updateResult.error.log() - if (entries.get(update.id) != null) { - console.error(`Removing entry ${update.id}, because its state is unclear`) - entries.delete(update.id) + /** Update a suggestion database according to information provided by the language server. */ + applyUpdates(entries: SuggestionDb, updates: lsTypes.SuggestionsDatabaseUpdate[]) { + for (const update of updates) { + const updateResult = this.applyUpdate(entries, update) + if (!updateResult.ok) { + updateResult.error.log() + if (entries.get(update.id) != null) { + console.error(`Removing entry ${update.id}, because its state is unclear`) + entries.delete(update.id) + } } } } diff --git a/app/gui/src/project-view/stores/suggestionDatabase/mockSuggestion.ts b/app/gui/src/project-view/stores/suggestionDatabase/mockSuggestion.ts new file mode 100644 index 000000000000..9d27452f905e --- /dev/null +++ b/app/gui/src/project-view/stores/suggestionDatabase/mockSuggestion.ts @@ -0,0 +1,140 @@ +import { + type SuggestionEntry, + type SuggestionEntryArgument, +} from '@/stores/suggestionDatabase/entry' +import { SuggestionUpdateProcessor } from '@/stores/suggestionDatabase/lsUpdate' +import { ANY_TYPE_QN } from '@/util/ensoTypes' +import { isQualifiedName, qnParent, qnSplit } from '@/util/qualifiedName' +import * as lsTypes from 'ydoc-shared/languageServerTypes/suggestions' +import { assert } from 'ydoc-shared/util/assert' +import { unwrap } from 'ydoc-shared/util/data/result' + +function makeEntry(lsEntry: lsTypes.SuggestionEntry) { + return unwrap(new SuggestionUpdateProcessor([]).entryFromLs(lsEntry)) +} + +const EMPTY_SCOPE = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } + +/** Mock a module suggestion entry. */ +export function makeModule(fqn: string): SuggestionEntry { + return makeEntry({ + type: 'module', + module: fqn, + }) +} + +/** Mock a type suggestion entry. */ +export function makeType(fqn: string): SuggestionEntry { + assert(isQualifiedName(fqn)) + const [definedIn, name] = qnSplit(fqn) + assert(definedIn != null) + return makeEntry({ + type: 'type', + module: definedIn, + name, + params: [], + }) +} + +/** Mock a type constructor suggestion entry. */ +export function makeConstructor(fqn: string): SuggestionEntry { + assert(isQualifiedName(fqn)) + const [type, name] = qnSplit(fqn) + assert(type != null) + const definedIn = qnParent(type) + assert(definedIn != null) + return makeEntry({ + type: 'constructor', + name, + module: definedIn, + arguments: [], + returnType: type, + annotations: [], + }) +} + +/** Mock a type method suggestion entry. */ +export function makeMethod( + fqn: string, + returnType: string = ANY_TYPE_QN, + isStatic: boolean = false, +): SuggestionEntry { + assert(isQualifiedName(fqn)) + const [type, name] = qnSplit(fqn) + assert(type != null) + const definedIn = qnParent(type) + assert(definedIn != null) + return makeEntry({ + type: 'method', + name, + module: definedIn, + arguments: [], + selfType: type, + returnType, + isStatic, + annotations: [], + }) +} + +/** Mock a static type method suggestion entry. */ +export function makeStaticMethod(fqn: string, returnType: string = ANY_TYPE_QN): SuggestionEntry { + return makeMethod(fqn, returnType, true) +} + +/** Mock a module method suggestion entry. */ +export function makeModuleMethod(fqn: string, returnType: string = ANY_TYPE_QN): SuggestionEntry { + assert(isQualifiedName(fqn)) + const [module, name] = qnSplit(fqn) + assert(module != null) + return makeEntry({ + type: 'method', + name, + module, + arguments: [], + selfType: module, + returnType, + isStatic: true, + annotations: [], + }) +} + +/** Mock a function suggestion entry. */ +export function makeFunction( + definedIn: string, + name: string, + returnType: string = ANY_TYPE_QN, +): SuggestionEntry { + return makeEntry({ + type: 'function', + name, + module: definedIn, + arguments: [], + returnType, + scope: EMPTY_SCOPE, + }) +} + +/** Mock a local variable suggestion entry. */ +export function makeLocal( + definedIn: string, + name: string, + returnType: string = ANY_TYPE_QN, +): SuggestionEntry { + return makeEntry({ + type: 'local', + name, + module: definedIn, + returnType, + scope: EMPTY_SCOPE, + }) +} + +/** Mock a suggestion entry argument specification. */ +export function makeArgument(name: string, type: string = ANY_TYPE_QN): SuggestionEntryArgument { + return { + name, + reprType: type, + isSuspended: false, + hasDefault: false, + } +} diff --git a/app/gui/src/project-view/util/__tests__/callTree.test.ts b/app/gui/src/project-view/util/__tests__/callTree.test.ts index 73131960ca89..362f5f6e1af1 100644 --- a/app/gui/src/project-view/util/__tests__/callTree.test.ts +++ b/app/gui/src/project-view/util/__tests__/callTree.test.ts @@ -2,6 +2,7 @@ import * as widgetCfg from '@/providers/widgetRegistry/configuration' import { GraphDb } from '@/stores/graph/graphDatabase' import { ComputedValueRegistry, type ExpressionInfo } from '@/stores/project/computedValueRegistry' import { SuggestionDb } from '@/stores/suggestionDatabase' +import { type SuggestionEntry } from '@/stores/suggestionDatabase/entry' import { makeArgument, makeConstructor, @@ -9,8 +10,7 @@ import { makeModule, makeModuleMethod, makeType, - type SuggestionEntry, -} from '@/stores/suggestionDatabase/entry' +} from '@/stores/suggestionDatabase/mockSuggestion' import { Ast } from '@/util/ast' import type { AstId } from '@/util/ast/abstract' import { diff --git a/app/gui/src/project-view/util/ast/abstract.ts b/app/gui/src/project-view/util/ast/abstract.ts index 9dd919ce9b8f..cffd99e12fab 100644 --- a/app/gui/src/project-view/util/ast/abstract.ts +++ b/app/gui/src/project-view/util/ast/abstract.ts @@ -182,8 +182,8 @@ export function parseIdents(ast: Ast): IdentifierOrOperatorIdentifier[] | null { return unrollOprChain(ast, ',') } -/** TODO: Add docs */ -export function parseQualifiedName(ast: Ast): QualifiedName | null { +/** If the syntax tree represents a valid qualified name, return an equivalent {@link QualifiedName}. */ +export function astToQualifiedName(ast: Ast): QualifiedName | null { const idents = unrollPropertyAccess(ast) return idents && normalizeQualifiedName(qnFromSegments(idents)) } @@ -223,7 +223,7 @@ export function substituteQualifiedName( to: QualifiedName, ) { if (expr instanceof MutablePropertyAccess || expr instanceof MutableIdent) { - const qn = parseQualifiedName(expr) + const qn = astToQualifiedName(expr) if (qn === pattern) { expr.updateValue(() => parseExpression(to, expr.module)!) } else if (qn && qn.startsWith(pattern)) { diff --git a/app/gui/src/project-view/util/callTree.ts b/app/gui/src/project-view/util/callTree.ts index 087ded4b9163..551f7bfe3948 100644 --- a/app/gui/src/project-view/util/callTree.ts +++ b/app/gui/src/project-view/util/callTree.ts @@ -117,7 +117,7 @@ export class ArgumentPlaceholder extends Argument { /** TODO: Add docs */ get value(): WidgetInputValue { - return this.argInfo.defaultValue + return this.argInfo.defaultValue === null ? undefined : this.argInfo.defaultValue } /** TODO: Add docs */ diff --git a/app/gui/src/project-view/util/data/array.ts b/app/gui/src/project-view/util/data/array.ts index 987fcd76811f..fdf7c6d88c69 100644 --- a/app/gui/src/project-view/util/data/array.ts +++ b/app/gui/src/project-view/util/data/array.ts @@ -7,7 +7,7 @@ export type NonEmptyArray = [T, ...T[]] /** An equivalent of `Array.prototype.findIndex` method, but returns null instead of -1. */ export function findIndexOpt( - arr: T[], + arr: ReadonlyArray, pred: (elem: T, index: number) => boolean, ): number | null { const index = arr.findIndex(pred) diff --git a/app/gui/src/project-view/util/ensoTypes.ts b/app/gui/src/project-view/util/ensoTypes.ts new file mode 100644 index 000000000000..0cac09b91af6 --- /dev/null +++ b/app/gui/src/project-view/util/ensoTypes.ts @@ -0,0 +1,4 @@ +import { tryQualifiedName } from '@/util/qualifiedName' +import { unwrap } from 'ydoc-shared/util/data/result' + +export const ANY_TYPE_QN = unwrap(tryQualifiedName('Standard.Base.Any.Any')) diff --git a/app/ydoc-shared/src/languageServerTypes/suggestions.ts b/app/ydoc-shared/src/languageServerTypes/suggestions.ts index 44513d873a33..e75ec896844b 100644 --- a/app/ydoc-shared/src/languageServerTypes/suggestions.ts +++ b/app/ydoc-shared/src/languageServerTypes/suggestions.ts @@ -13,9 +13,9 @@ export interface SuggestionEntryArgument { /** Indicates whether the argument has default value. */ hasDefault: boolean /** Optional default value. */ - defaultValue?: string + defaultValue?: string | null /** Optional list of possible values that this argument takes. */ - tagValues?: string[] + tagValues?: string[] | null } export interface Position { @@ -218,11 +218,11 @@ export interface FieldUpdate { } export type SuggestionArgumentUpdate = - | suggestionArgumentUpdateVariant.Add - | suggestionArgumentUpdateVariant.Remove - | suggestionArgumentUpdateVariant.Modify + | SuggestionArgumentUpdate.Add + | SuggestionArgumentUpdate.Remove + | SuggestionArgumentUpdate.Modify -namespace suggestionArgumentUpdateVariant { +export namespace SuggestionArgumentUpdate { export interface Add { type: 'Add' /** The position of the argument. */ @@ -261,11 +261,11 @@ namespace suggestionArgumentUpdateVariant { } export type SuggestionsDatabaseUpdate = - | suggestionDatabaseUpdateVariant.Add - | suggestionDatabaseUpdateVariant.Remove - | suggestionDatabaseUpdateVariant.Modify + | SuggestionsDatabaseUpdate.Add + | SuggestionsDatabaseUpdate.Remove + | SuggestionsDatabaseUpdate.Modify -namespace suggestionDatabaseUpdateVariant { +export namespace SuggestionsDatabaseUpdate { export interface Add { type: 'Add' /** Suggestion entry id. */