From 00bde82663c206f10336917f4fd4931493e8cdf3 Mon Sep 17 00:00:00 2001 From: Kaz Wesley Date: Thu, 23 Jan 2025 13:48:58 -0800 Subject: [PATCH] Abstract project names (#12106) Use abstract project references in retained state. Fixes #11815. --- CHANGELOG.md | 2 + app/gui/src/project-view/ProjectView.vue | 12 +- .../components/ComponentBrowser.vue | 16 +- .../__tests__/component.test.ts | 40 +-- .../__tests__/filtering.test.ts | 67 +++-- .../components/ComponentBrowser/component.ts | 22 +- .../components/ComponentBrowser/filtering.ts | 59 ++--- .../components/ComponentBrowser/input.ts | 32 ++- .../components/DocumentationPanel.vue | 9 +- .../DocumentationPanel/DocsList.vue | 2 +- .../components/DocumentationPanel/ir.ts | 10 +- .../components/FunctionSignatureEditor.vue | 19 +- .../project-view/components/GraphEditor.vue | 10 +- .../components/GraphEditor/GraphNode.vue | 2 +- .../GraphEditor/GraphNodeMessage.vue | 12 +- .../GraphEditor/widgets/WidgetCheckbox.vue | 17 +- .../GraphEditor/widgets/WidgetFileBrowser.vue | 10 +- .../GraphEditor/widgets/WidgetFunction.vue | 6 +- .../__tests__/widgetFunctionCallInfo.test.ts | 40 +-- .../WidgetFunction/widgetFunctionCallInfo.ts | 34 ++- .../GraphEditor/widgets/WidgetFunctionDef.vue | 2 +- .../widgets/WidgetFunctionName.vue | 12 +- .../GraphEditor/widgets/WidgetSelection.vue | 9 +- .../GraphEditor/widgets/WidgetTableEditor.vue | 11 +- .../__tests__/tableInputArgument.test.ts | 16 +- .../WidgetTableEditor/tableInputArgument.ts | 16 +- .../composables/stackNavigator.ts | 11 +- .../graph/__tests__/graphDatabase.test.ts | 8 +- .../stores/graph/__tests__/imports.test.ts | 98 ++++--- .../stores/graph/graphDatabase.ts | 59 +++-- .../src/project-view/stores/graph/imports.ts | 223 ++++++++-------- .../src/project-view/stores/graph/index.ts | 55 ++-- app/gui/src/project-view/stores/persisted.ts | 2 +- .../stores/project/computedValueRegistry.ts | 60 ++++- .../stores/project/executionContext.ts | 126 +++++---- .../src/project-view/stores/project/index.ts | 70 +++-- .../src/project-view/stores/projectNames.ts | 96 +++++++ .../__tests__/documentation.test.ts | 21 +- .../__tests__/lsUpdate.test.ts | 199 +++++++++------ .../suggestionDatabase/documentation.ts | 8 +- .../stores/suggestionDatabase/entry.ts | 49 ++-- .../stores/suggestionDatabase/index.ts | 75 +++--- .../stores/suggestionDatabase/lsUpdate.ts | 241 ++++++++++-------- .../suggestionDatabase/mockSuggestion.ts | 164 +++++++----- .../util/__tests__/callTree.test.ts | 85 +++--- .../util/__tests__/projectPath.test.ts | 48 ++++ .../util/__tests__/qualifiedName.test.ts | 24 +- app/gui/src/project-view/util/ast/abstract.ts | 23 +- app/gui/src/project-view/util/ensoTypes.ts | 5 + app/gui/src/project-view/util/getIconName.ts | 4 +- .../src/project-view/util/methodPointer.ts | 76 ++++++ app/gui/src/project-view/util/projectPath.ts | 99 +++++++ .../src/project-view/util/qualifiedName.ts | 30 +-- app/ydoc-shared/src/languageServerTypes.ts | 18 +- 54 files changed, 1525 insertions(+), 939 deletions(-) create mode 100644 app/gui/src/project-view/stores/projectNames.ts create mode 100644 app/gui/src/project-view/util/__tests__/projectPath.test.ts create mode 100644 app/gui/src/project-view/util/methodPointer.ts create mode 100644 app/gui/src/project-view/util/projectPath.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 858d5cc1a8e5..a44b9e4e30a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - [Redo stack is no longer lost when interacting with text literals][11908]. - [Fixed bug when clicking header in Table Editor Widget didn't start editing it][12064] +- [Fixed bugs occurring after renaming project from within graph editor][12106]. [11889]: https://github.com/enso-org/enso/pull/11889 [11836]: https://github.com/enso-org/enso/pull/11836 @@ -18,6 +19,7 @@ [11902]: https://github.com/enso-org/enso/pull/11902 [11908]: https://github.com/enso-org/enso/pull/11908 [12064]: https://github.com/enso-org/enso/pull/12064 +[12106]: https://github.com/enso-org/enso/pull/12106 #### Enso Standard Library diff --git a/app/gui/src/project-view/ProjectView.vue b/app/gui/src/project-view/ProjectView.vue index d9f818b13f16..b4c39b75af0a 100644 --- a/app/gui/src/project-view/ProjectView.vue +++ b/app/gui/src/project-view/ProjectView.vue @@ -4,9 +4,10 @@ import GraphEditor from '@/components/GraphEditor.vue' import { provideBackend } from '@/providers/backend' import { provideEventLogger } from '@/providers/eventLogging' import { provideVisibility } from '@/providers/visibility' -import { LsUrls, provideProjectStore } from '@/stores/project' +import { type LsUrls, provideProjectStore } from '@/stores/project' +import { provideProjectNames } from '@/stores/projectNames' import { provideSettings } from '@/stores/settings' -import { Opt } from '@/util/data/opt' +import { type Opt } from '@/util/data/opt' import { useEventListener } from '@vueuse/core' import { markRaw, onActivated, onDeactivated, ref, toRaw, toRef, watch } from 'vue' @@ -49,7 +50,12 @@ watch( useEventListener(window, 'beforeunload', () => logger.send('ide_project_closed')) -provideProjectStore(props) +const projectNames = provideProjectNames( + toRef(props, 'projectNamespace'), + props.projectName, + toRef(props, 'projectDisplayedName'), +) +provideProjectStore(props, projectNames) provideSettings() const visible = ref(false) diff --git a/app/gui/src/project-view/components/ComponentBrowser.vue b/app/gui/src/project-view/components/ComponentBrowser.vue index 097ea1418282..0065355fd1c7 100644 --- a/app/gui/src/project-view/components/ComponentBrowser.vue +++ b/app/gui/src/project-view/components/ComponentBrowser.vue @@ -15,6 +15,7 @@ import { injectInteractionHandler, type Interaction } from '@/providers/interact import { useGraphStore } from '@/stores/graph' import type { RequiredImport } from '@/stores/graph/imports' import { useProjectStore } from '@/stores/project' +import { injectProjectNames } from '@/stores/projectNames' import { useSuggestionDbStore } from '@/stores/suggestionDatabase' import { type Typename } from '@/stores/suggestionDatabase/entry' import type { VisualizationDataSource } from '@/stores/visualization' @@ -50,6 +51,7 @@ const projectStore = useProjectStore() const suggestionDbStore = useSuggestionDbStore() const graphStore = useGraphStore() const interaction = injectInteractionHandler() +const projectNames = injectProjectNames() const props = defineProps<{ nodePosition: Vec2 @@ -177,7 +179,7 @@ const input = useComponentBrowserInput() const currentFiltering = computed(() => { if (input.mode.mode === 'componentBrowsing') { - const currentModule = projectStore.modulePath + const currentModule = projectStore.moduleProjectPath return new Filtering(input.mode.filter, currentModule?.ok ? currentModule.value : undefined) } else { return undefined @@ -260,11 +262,11 @@ const previewedCode = debouncedGetter(() => input.code, 200) const previewedSuggestionReturnType = computed(() => { const id = input.mode.mode === 'codeEditing' ? input.mode.appliedSuggestion : undefined const appliedEntry = id != null ? suggestionDbStore.entries.get(id) : undefined - if (appliedEntry != null) return appliedEntry.returnType - else if (props.usage.type === 'editNode') { - return graphStore.db.getNodeMainSuggestion(props.usage.node)?.returnType - } - return undefined + const entry = + appliedEntry ? appliedEntry + : props.usage.type === 'editNode' ? graphStore.db.getNodeMainSuggestion(props.usage.node) + : undefined + return entry?.returnType(projectNames) }) const previewDataSource = computed(() => { @@ -316,7 +318,7 @@ function applySuggestion(component: Opt = null) { function acceptInput() { const appliedReturnType = input.mode.mode === 'codeEditing' && input.mode.appliedSuggestion != null ? - suggestionDbStore.entries.get(input.mode.appliedSuggestion)?.returnType + suggestionDbStore.entries.get(input.mode.appliedSuggestion)?.returnType(projectNames) : undefined emit('accepted', input.code.trim(), input.importsToAdd(), appliedReturnType) interaction.ended(cbOpen) 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 31dc12c48c73..2833e56be932 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 @@ -22,7 +22,7 @@ test.each([ [makeStaticMethod('Standard.Base.Data.Vector.new'), 'Vector.new'], [makeMethod('Standard.Base.Data.Vector.get'), 'get'], [makeConstructor('Standard.Table.Join_Kind.Join_Kind.Inner'), 'Join_Kind.Inner'], - [makeModuleMethod('local.Project.main'), 'Project.main'], + [makeModuleMethod('local.Mock_Project.main'), 'Main.main'], ])("$name Component's label is valid", (suggestion, expected) => { expect(labelOfEntry(suggestion, { score: 0 }).label).toBe(expected) }) @@ -31,42 +31,42 @@ test('Suggestions are ordered properly', () => { const sortedEntries: MatchedSuggestion[] = [ { id: 100, - entry: makeModuleMethod('local.Project.Z.best_score'), + entry: makeModuleMethod('local.Mock_Project.Z.best_score'), match: { score: 0 }, }, { id: 90, - entry: { ...makeModuleMethod('local.Project.Z.b'), groupIndex: 0 }, + entry: { ...makeModuleMethod('local.Mock_Project.Z.b'), groupIndex: 0 }, match: { score: 50 }, }, { id: 91, - entry: { ...makeModuleMethod('local.Project.Z.a'), groupIndex: 0 }, + entry: { ...makeModuleMethod('local.Mock_Project.Z.a'), groupIndex: 0 }, match: { score: 50 }, }, { id: 89, - entry: { ...makeModuleMethod('local.Project.A.foo'), groupIndex: 1 }, + entry: { ...makeModuleMethod('local.Mock_Project.A.foo'), groupIndex: 1 }, match: { score: 50 }, }, { id: 88, - entry: { ...makeModuleMethod('local.Project.B.another_module'), groupIndex: 1 }, + entry: { ...makeModuleMethod('local.Mock_Project.B.another_module'), groupIndex: 1 }, match: { score: 50 }, }, { id: 87, - entry: { ...makeModule('local.Project.A'), groupIndex: 1 }, + entry: { ...makeModule('local.Mock_Project.A'), groupIndex: 1 }, match: { score: 50 }, }, { id: 50, - entry: makeModuleMethod('local.Project.Z.module_content'), + entry: makeModuleMethod('local.Mock_Project.Z.module_content'), match: { score: 50 }, }, { id: 49, - entry: makeModule('local.Project.Z.Module'), + entry: makeModule('local.Mock_Project.Z.Module'), match: { score: 50 }, }, ] @@ -82,16 +82,16 @@ test('Suggestions are ordered properly', () => { test.each` name | aliases | highlighted - ${'foo_bar'} | ${[]} | ${'Project.<_bar>'} - ${'foo_xyz_barabc'} | ${[]} | ${'Project._xyz<_bar>abc'} - ${'fooabc_barabc'} | ${[]} | ${'Project.abc<_bar>abc'} - ${'bar'} | ${['foo_bar', 'foo']} | ${'Project.bar (<_bar>)'} - ${'bar'} | ${['foo', 'foo_xyz_barabc']} | ${'Project.bar (_xyz<_bar>abc)'} - ${'bar'} | ${['foo', 'fooabc_barabc']} | ${'Project.bar (abc<_bar>abc)'} - ${'xyz_foo_abc_bar_xyz'} | ${[]} | ${'Project.xyz__abc<_bar>_xyz'} - ${'xyz_fooabc_abc_barabc_xyz'} | ${[]} | ${'Project.xyz_abc_abc<_bar>abc_xyz'} - ${'bar'} | ${['xyz_foo_abc_bar_xyz']} | ${'Project.bar (xyz__abc<_bar>_xyz)'} - ${'bar'} | ${['xyz_fooabc_abc_barabc_xyz']} | ${'Project.bar (xyz_abc_abc<_bar>abc_xyz)'} + ${'foo_bar'} | ${[]} | ${'Main.<_bar>'} + ${'foo_xyz_barabc'} | ${[]} | ${'Main._xyz<_bar>abc'} + ${'fooabc_barabc'} | ${[]} | ${'Main.abc<_bar>abc'} + ${'bar'} | ${['foo_bar', 'foo']} | ${'Main.bar (<_bar>)'} + ${'bar'} | ${['foo', 'foo_xyz_barabc']} | ${'Main.bar (_xyz<_bar>abc)'} + ${'bar'} | ${['foo', 'fooabc_barabc']} | ${'Main.bar (abc<_bar>abc)'} + ${'xyz_foo_abc_bar_xyz'} | ${[]} | ${'Main.xyz__abc<_bar>_xyz'} + ${'xyz_fooabc_abc_barabc_xyz'} | ${[]} | ${'Main.xyz_abc_abc<_bar>abc_xyz'} + ${'bar'} | ${['xyz_foo_abc_bar_xyz']} | ${'Main.bar (xyz__abc<_bar>_xyz)'} + ${'bar'} | ${['xyz_fooabc_abc_barabc_xyz']} | ${'Main.bar (xyz_abc_abc<_bar>abc_xyz)'} `('Matched ranges of $highlighted are correct', ({ name, aliases, highlighted }) => { function replaceMatches(component: Component) { if (!component.matchedRanges) return component.label @@ -104,7 +104,7 @@ test.each` } const pattern = 'foo_bar' - const entry = makeModuleMethod(`local.Project.${name}`, { aliases: aliases ?? [] }) + const entry = makeModuleMethod(`local.Mock_Project.${name}`, { aliases: aliases ?? [] }) const filtering = new Filtering({ pattern }) const match = filtering.filter(entry, []) expect(match).not.toBeNull() 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 4c6c6269ed25..01bed515c68a 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,5 +1,5 @@ import { Filtering, type MatchResult } from '@/components/ComponentBrowser/filtering' -import { SuggestionEntry } from '@/stores/suggestionDatabase/entry' +import { type SuggestionEntry } from '@/stores/suggestionDatabase/entry' import { makeConstructor, makeFunction, @@ -9,9 +9,12 @@ import { makeModuleMethod, makeStaticMethod, } from '@/stores/suggestionDatabase/mockSuggestion' -import { qnLastSegment, QualifiedName } from '@/util/qualifiedName' +import { assert } from '@/util/assert' +import { parseAbsoluteProjectPath } from '@/util/projectPath' +import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName' import { expect, test } from 'vitest' -import { Opt } from 'ydoc-shared/util/data/opt' +import { type Opt } from 'ydoc-shared/util/data/opt' +import { unwrap } from 'ydoc-shared/util/data/result' test.each([ makeModuleMethod('Standard.Base.Data.read', { group: 'Standard.Base.MockGroup1' }), @@ -37,11 +40,19 @@ test.each([ expect(filtering.filter(entry, [])).toBeNull() }) +function stdPath(path: string) { + assert(path.startsWith('Standard.')) + return parseAbsoluteProjectPath(unwrap(tryQualifiedName(path))) +} + test('An Instance method is shown when self arg matches', () => { const entry1 = makeMethod('Standard.Base.Data.Vector.Vector.get') const entry2 = makeMethod('Standard.Base.Data.Table.get') const filteringWithSelfType = new Filtering({ - selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' }, + selfArg: { + type: 'known', + typename: stdPath('Standard.Base.Data.Vector.Vector'), + }, }) expect(filteringWithSelfType.filter(entry1, [])).not.toBeNull() expect(filteringWithSelfType.filter(entry2, [])).toBeNull() @@ -59,7 +70,10 @@ test('`Any` type methods taken into account when filtering', () => { const entry1 = makeMethod('Standard.Base.Data.Vector.Vector.get') const entry2 = makeMethod('Standard.Base.Any.Any.to_string') const filtering = new Filtering({ - selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' }, + selfArg: { + type: 'known', + typename: stdPath('Standard.Base.Data.Vector.Vector'), + }, }) expect(filtering.filter(entry1, [])).not.toBeNull() expect(filtering.filter(entry2, [])).not.toBeNull() @@ -72,9 +86,9 @@ test('`Any` type methods taken into account when filtering', () => { test('Additional self types are taken into account when filtering', () => { const entry1 = makeMethod('Standard.Base.Data.Numbers.Float.abs') const entry2 = makeMethod('Standard.Base.Data.Numbers.Number.sqrt') - const additionalSelfType = 'Standard.Base.Data.Numbers.Number' as QualifiedName + const additionalSelfType = stdPath('Standard.Base.Data.Numbers.Number') const filtering = new Filtering({ - selfArg: { type: 'known', typename: 'Standard.Base.Data.Numbers.Float' }, + selfArg: { type: 'known', typename: stdPath('Standard.Base.Data.Numbers.Float') }, }) expect(filtering.filter(entry1, [additionalSelfType])).not.toBeNull() expect(filtering.filter(entry2, [additionalSelfType])).not.toBeNull() @@ -97,7 +111,10 @@ test.each([ makeMethod('Standard.Base.Data.Vector.Vector2.get'), ])('$name is filtered out when Vector self type is specified', (entry) => { const filtering = new Filtering({ - selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' }, + selfArg: { + type: 'known', + typename: stdPath('Standard.Base.Data.Vector.Vector'), + }, }) expect(filtering.filter(entry, [])).toBeNull() }) @@ -203,14 +220,14 @@ test.each([ ], }, { - pattern: 'pr.foo', + pattern: 'ma.foo', matchedSorted: [ - { module: 'local.Pr', name: 'foo' }, // exact match - { module: 'local.Project', name: 'foo' }, // name exact match and owner name start match - { module: 'local.Pr', name: 'foobar' }, // module exact match and name start match - { module: 'local.Pr', name: 'bar', aliases: ['baz', 'foo'] }, // exact alias match - { module: 'local.Project', name: 'bar', aliases: ['baz', 'foo'] }, // exact alias match, but nonexact owner match - { module: 'local.Project', name: 'bar', aliases: ['bazbar', 'foobar'] }, // alias start match + { module: 'local.Project.Ma', name: 'foo' }, // exact match + { module: 'local.Project.Main', name: 'foo' }, // name exact match and owner name start match + { module: 'local.Project.Ma', name: 'foobar' }, // module exact match and name start match + { module: 'local.Project.Ma', name: 'bar', aliases: ['baz', 'foo'] }, // exact alias match + { module: 'local.Project.Main', name: 'bar', aliases: ['baz', 'foo'] }, // exact alias match, but nonexact owner match + { module: 'local.Project.Main', name: 'bar', aliases: ['bazbar', 'foobar'] }, // alias start match { name: 'bar_foo' }, // name word exact match { name: 'baz_foobar' }, // name word start match { name: 'bar', aliases: ['bar_foo'] }, // alias word exact match @@ -220,22 +237,25 @@ test.each([ ], notMatched: [ { module: 'local.Project.Data', name: 'foo' }, - { module: 'local.Ploject', name: 'foo' }, { module: 'local.Pr', name: 'bar' }, ], }, ])('Matching pattern $pattern', ({ pattern, matchedSorted, notMatched }) => { const filtering = new Filtering({ pattern }) - const matchedSortedEntries = Array.from(matchedSorted, ({ name, aliases, module }) => + const matchedSortedEntries = matchedSorted.map(({ name, aliases, module }) => makeModuleMethod(`${module ?? 'local.Project'}.${name}`, { aliases: aliases ?? [] }), ) - const matchResults = Array.from(matchedSortedEntries, (entry) => filtering.filter(entry, [])) + const matchResults = matchedSortedEntries.map((entry) => filtering.filter(entry, [])) // Checking matching entries function checkResult(entry: SuggestionEntry, result: Opt) { - expect(result, `Matching entry ${entry.definitionPath}`).not.toBeNull() + expect(result, `Matching entry ${JSON.stringify(entry.definitionPath)}`).not.toBeNull() expect( matchedText( - 'memberOf' in entry && entry.memberOf ? qnLastSegment(entry.memberOf) : '', + 'memberOf' in entry ? + entry.memberOf.path ? + qnLastSegment(entry.memberOf.path) + : 'Main' + : '', entry.name, result!, ) @@ -257,10 +277,9 @@ test.each([ // Checking non-matching entries for (const { module, name, aliases } of notMatched) { - const entry = { - ...makeModuleMethod(`${module ?? 'local.Project'}.${name}`), + const entry = makeModuleMethod(`${module ?? 'local.Project'}.${name}`, { aliases: aliases ?? [], - } - expect(filtering.filter(entry, []), entry.definitionPath).toBeNull() + }) + expect(filtering.filter(entry, []), JSON.stringify(entry.definitionPath)).toBeNull() } }) diff --git a/app/gui/src/project-view/components/ComponentBrowser/component.ts b/app/gui/src/project-view/components/ComponentBrowser/component.ts index 20ec455dea59..61652e694371 100644 --- a/app/gui/src/project-view/components/ComponentBrowser/component.ts +++ b/app/gui/src/project-view/components/ComponentBrowser/component.ts @@ -1,6 +1,7 @@ import { Filtering, type MatchResult } from '@/components/ComponentBrowser/filtering' import { SuggestionDb } from '@/stores/suggestionDatabase' import { + entryDisplayPath, entryIsStatic, SuggestionKind, type SuggestionEntry, @@ -10,8 +11,8 @@ import { compareOpt } from '@/util/compare' import { isSome } from '@/util/data/opt' import { Range } from '@/util/data/range' import { displayedIconOf } from '@/util/getIconName' -import type { Icon } from '@/util/iconMetadata/iconName' -import { qnJoin, qnLastSegment, tryQualifiedName, type QualifiedName } from '@/util/qualifiedName' +import { type Icon } from '@/util/iconMetadata/iconName' +import { type ProjectPath } from '@/util/projectPath' interface ComponentLabelInfo { label: string @@ -33,8 +34,8 @@ export interface Component extends ComponentLabel { /** @returns the displayed label of given suggestion entry with information of highlighted ranges. */ export function labelOfEntry(entry: SuggestionEntry, match: MatchResult): ComponentLabelInfo { - if (entryIsStatic(entry) && entry.memberOf) { - const label = qnJoin(qnLastSegment(entry.memberOf), entry.name) + if (entryIsStatic(entry)) { + const label = entryDisplayPath(entry) if ((!match.ownerNameRanges && !match.nameRanges) || match.matchedAlias) { return { label, @@ -86,7 +87,9 @@ export function compareSuggestions(a: MatchedSuggestion, b: MatchedSuggestion): const kindCompare = +(a.entry.kind === SuggestionKind.Module) - +(b.entry.kind === SuggestionKind.Module) if (kindCompare !== 0) return kindCompare - const moduleCompare = a.entry.definedIn.localeCompare(b.entry.definedIn) + const moduleCompare = + (a.entry.definedIn.project ?? '').localeCompare(b.entry.definedIn.project ?? '') || + (a.entry.definedIn.path ?? '').localeCompare(b.entry.definedIn.path ?? '') if (moduleCompare !== 0) return moduleCompare return a.id - b.id } @@ -110,13 +113,10 @@ export function makeComponent({ id, entry, match }: ComponentInfo): Component { /** Create {@link Component} list from filtered suggestions. */ export function makeComponentList(db: SuggestionDb, filtering: Filtering): Component[] { function* matchSuggestions() { - const additionalSelfTypes: QualifiedName[] = [] + const additionalSelfTypes: ProjectPath[] = [] if (filtering.selfArg?.type === 'known') { - const maybeName = tryQualifiedName(filtering.selfArg.typename) - if (maybeName.ok) { - const entry = db.getEntryByQualifiedName(maybeName.value) - if (entry) additionalSelfTypes.push(...db.ancestors(entry)) - } + const entry = db.getEntryByProjectPath(filtering.selfArg.typename) + if (entry) additionalSelfTypes.push(...db.ancestors(entry)) } for (const [id, entry] of db.entries()) { diff --git a/app/gui/src/project-view/components/ComponentBrowser/filtering.ts b/app/gui/src/project-view/components/ComponentBrowser/filtering.ts index e16c44066436..8f34e60ce2ec 100644 --- a/app/gui/src/project-view/components/ComponentBrowser/filtering.ts +++ b/app/gui/src/project-view/components/ComponentBrowser/filtering.ts @@ -1,18 +1,14 @@ -import { - type SuggestionEntry, - SuggestionKind, - type Typename, -} from '@/stores/suggestionDatabase/entry' -import type { Opt } from '@/util/data/opt' +import { SuggestionKind, type SuggestionEntry } from '@/stores/suggestionDatabase/entry' import { Range } from '@/util/data/range' -import { ANY_TYPE_QN } from '@/util/ensoTypes' -import { qnIsTopElement, qnLastSegment, type QualifiedName } from '@/util/qualifiedName' +import { ANY_TYPE } from '@/util/ensoTypes' +import { type ProjectPath } from '@/util/projectPath' +import { qnLastSegment } from '@/util/qualifiedName' import escapeStringRegexp from '@/util/regexp' export type SelfArg = | { type: 'known' - typename: Typename + typename: ProjectPath } | { type: 'unknown' } @@ -181,10 +177,12 @@ class FilteringWithPattern { return null } - tryMatch(name: string, aliases: string[], memberOf: QualifiedName): MatchResult | null { + tryMatch(name: string, aliases: string[], memberOf: ProjectPath): MatchResult | null { const nameMatch: (NameMatchResult & { alias?: string }) | null = this.nameFilter.tryMatch(name) ?? this.firstMatchingAlias(aliases) - const ownerNameMatch = this.ownerNameFilter.tryMatch(qnLastSegment(memberOf)) + const ownerNameMatch = this.ownerNameFilter.tryMatch( + memberOf.path ? qnLastSegment(memberOf.path) : 'Main', + ) if (!nameMatch && !ownerNameMatch) return null if (this.bothFiltersMustMatch && (!nameMatch || !ownerNameMatch)) return null @@ -235,28 +233,28 @@ class FilteringWithPattern { * name is preferred before alias. See `FilteringWithPattern.tryMatch` implementation for details. */ export class Filtering { - pattern?: FilteringWithPattern - selfArg?: SelfArg - currentModule?: QualifiedName + pattern: FilteringWithPattern | undefined + selfArg: SelfArg | undefined /** TODO: Add docs */ - constructor(filter: Filter, currentModule: Opt = undefined) { + constructor( + filter: Filter, + public currentModule: ProjectPath | undefined = undefined, + ) { const { pattern, selfArg } = filter - if (pattern) { - this.pattern = new FilteringWithPattern(pattern) - } - if (selfArg != null) this.selfArg = selfArg - if (currentModule != null) this.currentModule = currentModule + this.pattern = pattern != null ? new FilteringWithPattern(pattern) : undefined + this.selfArg = selfArg } - private selfTypeMatches(entry: SuggestionEntry, additionalSelfTypes: QualifiedName[]): boolean { + private selfTypeMatches(entry: SuggestionEntry, additionalSelfTypes: ProjectPath[]): boolean { if (this.selfArg == null) return entry.kind !== SuggestionKind.Method || entry.selfType == null if (entry.kind !== SuggestionKind.Method || entry.selfType == null) return false + const entrySelfType = entry.selfType return ( this.selfArg.type !== 'known' || - entry.selfType === this.selfArg.typename || - entry.selfType === ANY_TYPE_QN || - additionalSelfTypes.some((t) => entry.selfType === t) + entrySelfType.equals(this.selfArg.typename) || + entrySelfType.equals(ANY_TYPE) || + additionalSelfTypes.some((t) => entrySelfType.equals(t)) ) } @@ -267,19 +265,18 @@ export class Filtering { private mainViewFilter(entry: SuggestionEntry): MatchResult | null { const hasGroup = entry.groupIndex != null - const isInTopModule = qnIsTopElement(entry.definedIn) + const isInTopModule = entry.definedIn.isTopElement() if (hasGroup || isInTopModule) return { score: 0 } else return null } private isLocal(entry: SuggestionEntry): boolean { - return this.currentModule != null && entry.definedIn === this.currentModule + return this.currentModule != null && entry.definedIn.equals(this.currentModule) } /** TODO: Add docs */ - filter(entry: SuggestionEntry, additionalSelfTypes: QualifiedName[]): MatchResult | null { - if (entry.isPrivate || entry.kind != SuggestionKind.Method || entry.memberOf == null) - return null + filter(entry: SuggestionEntry, additionalSelfTypes: ProjectPath[]): MatchResult | null { + if (entry.isPrivate || entry.kind != SuggestionKind.Method) return null if (this.selfArg == null && isInternal(entry)) return null if (!this.selfTypeMatches(entry, additionalSelfTypes)) return null if (this.pattern) { @@ -297,6 +294,6 @@ function isInternal(entry: SuggestionEntry): boolean { return isInternalModulePath(entry.definedIn) } -function isInternalModulePath(path: string): boolean { - return /Standard[.].*Internal(?:[._]|$)/.test(path) +function isInternalModulePath({ project, path }: ProjectPath): boolean { + return !!project && project.startsWith('Standard.') && !!path && /Internal(?:[._]|$)/.test(path) } diff --git a/app/gui/src/project-view/components/ComponentBrowser/input.ts b/app/gui/src/project-view/components/ComponentBrowser/input.ts index 540eb4d59a12..5e66c14bf430 100644 --- a/app/gui/src/project-view/components/ComponentBrowser/input.ts +++ b/app/gui/src/project-view/components/ComponentBrowser/input.ts @@ -11,10 +11,10 @@ import { } from '@/stores/suggestionDatabase/entry' import { isIdentifier, type AstId, type Identifier } from '@/util/ast/abstract' import { Err, Ok, type Result } from '@/util/data/result' -import { ANY_TYPE_QN } from '@/util/ensoTypes' -import { qnJoin, qnLastSegment, type QualifiedName } from '@/util/qualifiedName' +import { type ProjectPath } from '@/util/projectPath' +import { qnJoin, qnLastSegment } from '@/util/qualifiedName' import { useToast } from '@/util/toast' -import { computed, proxyRefs, readonly, ref, type ComputedRef } from 'vue' +import { computed, proxyRefs, readonly, ref, shallowRef, type ComputedRef } from 'vue' /** Information how the component browser is used, needed for proper input initializing. */ export type Usage = @@ -51,7 +51,7 @@ export function useComponentBrowserInput( const text = ref('') const cbUsage = ref() const selection = ref({ start: 0, end: 0 }) - const imports = ref([]) + const imports = shallowRef([]) const processingAIPrompt = ref(false) const toastError = useToast.error() const sourceNodeIdentifier = ref() @@ -127,9 +127,7 @@ export function useComponentBrowserInput( const definition = graphDb.getIdentDefiningNode(sourceNodeIdentifier.value) if (definition == null) return null const typename = graphDb.getExpressionInfo(definition)?.typename - return typename != null && typename !== ANY_TYPE_QN ? - { type: 'known', typename } - : { type: 'unknown' } + return typename ? { type: 'known', typename } : { type: 'unknown' } }) /** Apply given suggested entry to the input. */ @@ -142,7 +140,7 @@ export function useComponentBrowserInput( text.value = newText selection.value = { start: newCursorPos, end: newCursorPos } if (requiredImport) { - const [importId] = suggestionDb.nameToId.lookup(requiredImport) + const importId = suggestionDb.findByProjectPath(requiredImport) if (importId) { const requiredEntry = suggestionDb.get(importId) if (requiredEntry) { @@ -161,7 +159,7 @@ export function useComponentBrowserInput( function inputAfterApplyingSuggestion(entry: SuggestionEntry): { newText: string - requiredImport: QualifiedName | undefined + requiredImport: ProjectPath | undefined } { if (sourceNodeIdentifier.value) { return { @@ -170,9 +168,17 @@ export function useComponentBrowserInput( } } else { // Perhaps we will add cases for Type/Con imports, but they are not displayed as suggestion ATM. - const owner = entryIsStatic(entry) ? entry.memberOf : undefined + const owner = entryIsStatic(entry) ? entry.memberOf.normalized() : undefined return { - newText: (owner ? qnJoin(qnLastSegment(owner), entry.name) : entry.name) + ' ', + newText: + (owner ? + qnJoin( + owner.path ? qnLastSegment(owner.path) + : owner.project ? qnLastSegment(owner.project) + : ('Main' as Identifier), + entry.name, + ) + : entry.name) + ' ', requiredImport: owner, } } @@ -188,7 +194,9 @@ export function useComponentBrowserInput( for (const anImport of imports.value) { const alreadyAdded = finalImports.some((existing) => requiredImportEquals(existing, anImport)) const importedIdent = - anImport.kind == 'Qualified' ? qnLastSegment(anImport.module) : anImport.import + anImport.kind == 'Qualified' ? + qnLastSegment(anImport.module.path ?? anImport.module.project ?? ('Main' as Identifier)) + : anImport.import const noLongerNeeded = !text.value.includes(importedIdent) if (!noLongerNeeded && !alreadyAdded) { finalImports.push(anImport) diff --git a/app/gui/src/project-view/components/DocumentationPanel.vue b/app/gui/src/project-view/components/DocumentationPanel.vue index 3340e37d6fad..f4e2db88e443 100644 --- a/app/gui/src/project-view/components/DocumentationPanel.vue +++ b/app/gui/src/project-view/components/DocumentationPanel.vue @@ -11,6 +11,7 @@ import type { Docs, FunctionDocs, Sections, TypeDocs } from '@/components/Docume import { lookupDocumentation, placeholder } from '@/components/DocumentationPanel/ir' import SvgButton from '@/components/SvgButton.vue' import { groupColorStyle } from '@/composables/nodeColors' +import { injectProjectNames } from '@/stores/projectNames' import { useSuggestionDbStore } from '@/stores/suggestionDatabase' import type { SuggestionId } from '@/stores/suggestionDatabase/entry' import { suggestionDocumentationUrl } from '@/stores/suggestionDatabase/entry' @@ -55,9 +56,11 @@ const types = computed(() => { const isPlaceholder = computed(() => documentation.value.kind === 'Placeholder') +const projectNames = injectProjectNames() + const name = computed>(() => { const docs = documentation.value - return docs.kind === 'Placeholder' ? null : docs.name + return docs.kind === 'Placeholder' ? null : projectNames.printProjectPath(docs.name) }) // === Breadcrumbs === @@ -106,8 +109,8 @@ function handleBreadcrumbClick(index: number) { if (name.value) { const qName = qnSlice(name.value, 0, index + 2) if (qName.ok) { - const [id] = db.entries.nameToId.lookup(qName.value) - if (id) { + const id = db.entries.findByProjectPath(projectNames.parseProjectPath(qName.value)) + if (id != null) { historyStack.record(id) } } diff --git a/app/gui/src/project-view/components/DocumentationPanel/DocsList.vue b/app/gui/src/project-view/components/DocumentationPanel/DocsList.vue index af8e1c26c6c9..fd2328b4dd78 100644 --- a/app/gui/src/project-view/components/DocumentationPanel/DocsList.vue +++ b/app/gui/src/project-view/components/DocumentationPanel/DocsList.vue @@ -56,7 +56,7 @@ const annotations = computed>(() => { class="link clickable" @click.stop="emit('linkClicked', item.id)" > - {{ qnSplit(item.name)[1] }} + {{ qnSplit(item.name.path ?? item.name.project!)[1] }} {{ ' ' + argumentsList(item.arguments) }} diff --git a/app/gui/src/project-view/components/DocumentationPanel/ir.ts b/app/gui/src/project-view/components/DocumentationPanel/ir.ts index 893b245b817b..027765dd4ec4 100644 --- a/app/gui/src/project-view/components/DocumentationPanel/ir.ts +++ b/app/gui/src/project-view/components/DocumentationPanel/ir.ts @@ -2,7 +2,7 @@ import type { SuggestionDb } from '@/stores/suggestionDatabase' import type { SuggestionEntry, SuggestionId } from '@/stores/suggestionDatabase/entry' import { SuggestionKind } from '@/stores/suggestionDatabase/entry' import type { Doc } from '@/util/docParser' -import type { QualifiedName } from '@/util/qualifiedName' +import { type ProjectPath } from '@/util/projectPath' import * as iter from 'enso-common/src/utilities/data/iter' import type { SuggestionEntryArgument } from 'ydoc-shared/languageServerTypes/suggestions' @@ -20,7 +20,7 @@ export interface Placeholder { export interface FunctionDocs { kind: 'Function' id: SuggestionId - name: QualifiedName + name: ProjectPath arguments: SuggestionEntryArgument[] sections: Sections } @@ -28,7 +28,7 @@ export interface FunctionDocs { export interface TypeDocs { kind: 'Type' id: SuggestionId - name: QualifiedName + name: ProjectPath arguments: SuggestionEntryArgument[] sections: Sections methods: FunctionDocs[] @@ -38,7 +38,7 @@ export interface TypeDocs { export interface ModuleDocs { kind: 'Module' id: SuggestionId - name: QualifiedName + name: ProjectPath sections: Sections types: TypeDocs[] methods: FunctionDocs[] @@ -47,7 +47,7 @@ export interface ModuleDocs { export interface LocalDocs { kind: 'Local' id: SuggestionId - name: QualifiedName + name: ProjectPath sections: Sections } diff --git a/app/gui/src/project-view/components/FunctionSignatureEditor.vue b/app/gui/src/project-view/components/FunctionSignatureEditor.vue index 777fdc44044c..21449ecacd49 100644 --- a/app/gui/src/project-view/components/FunctionSignatureEditor.vue +++ b/app/gui/src/project-view/components/FunctionSignatureEditor.vue @@ -1,17 +1,18 @@ diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue index 22c56841c7cf..69ec7d1f9dae 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetCheckbox.vue @@ -3,11 +3,12 @@ import { ArgumentNameShownKey } from '@/components/GraphEditor/widgets/WidgetArg import CheckboxWidget from '@/components/widgets/CheckboxWidget.vue' import { Score, WidgetInput, defineWidget, widgetProps } from '@/providers/widgetRegistry' import { useGraphStore } from '@/stores/graph' -import { requiredImportsByFQN } from '@/stores/graph/imports' +import { requiredImportsByProjectPath } from '@/stores/graph/imports' import { useSuggestionDbStore } from '@/stores/suggestionDatabase' import { assert } from '@/util/assert' import { Ast } from '@/util/ast' import { ArgumentInfoKey } from '@/util/callTree' +import { ProjectPath } from '@/util/projectPath' import { type Identifier, type QualifiedName } from '@/util/qualifiedName' import { computed } from 'vue' @@ -16,16 +17,22 @@ const graph = useGraphStore() const suggestionDb = useSuggestionDbStore() const trueImport = computed(() => - requiredImportsByFQN( + requiredImportsByProjectPath( suggestionDb.entries, - 'Standard.Base.Data.Boolean.Boolean.True' as QualifiedName, + ProjectPath.create( + 'Standard.Base' as QualifiedName, + 'Data.Boolean.Boolean.True' as QualifiedName, + ), true, ), ) const falseImport = computed(() => - requiredImportsByFQN( + requiredImportsByProjectPath( suggestionDb.entries, - 'Standard.Base.Data.Boolean.Boolean.False' as QualifiedName, + ProjectPath.create( + 'Standard.Base' as QualifiedName, + 'Data.Boolean.Boolean.False' as QualifiedName, + ), true, ), ) diff --git a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser.vue b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser.vue index 854bf534ce50..72be7a33aec8 100644 --- a/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser.vue +++ b/app/gui/src/project-view/components/GraphEditor/widgets/WidgetFileBrowser.vue @@ -10,8 +10,9 @@ import type { RequiredImport } from '@/stores/graph/imports' import { Ast } from '@/util/ast' import { Pattern } from '@/util/ast/match' import { ArgumentInfoKey } from '@/util/callTree' +import { ProjectPath, printAbsoluteProjectPath } from '@/util/projectPath' import { computed } from 'vue' -import { TextLiteral } from 'ydoc-shared/ast' +import { TextLiteral, type QualifiedName } from 'ydoc-shared/ast' const props = defineProps(widgetProps(widgetDefinition)) const graph = useGraphStore() @@ -128,8 +129,11 @@ const innerWidgetInput = computed(() => {