diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c4aa501e..fa788b0827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - [Changed the way of adding new column in Table Input Widget][11388]. The "virtual column" is replaced with an explicit (+) button. - [New dropdown-based component menu][11398]. +- [Methods defined on Standard.Base.Any type are now visible on all + components][11451]. - [Undo/redo buttons in the top bar][11433]. - [Size of Table Input Widget is preserved and restored after project re-opening][11435] @@ -28,6 +30,7 @@ [11383]: https://github.com/enso-org/enso/pull/11383 [11388]: https://github.com/enso-org/enso/pull/11388 [11398]: https://github.com/enso-org/enso/pull/11398 +[11451]: https://github.com/enso-org/enso/pull/11451 [11433]: https://github.com/enso-org/enso/pull/11433 [11435]: https://github.com/enso-org/enso/pull/11435 [11446]: https://github.com/enso-org/enso/pull/11446 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 c59d789a29..8780dca65d 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 @@ -12,7 +12,7 @@ import { makeStaticMethod, SuggestionEntry, } from '@/stores/suggestionDatabase/entry' -import { qnLastSegment } from '@/util/qualifiedName' +import { qnLastSegment, QualifiedName } from '@/util/qualifiedName' import { Opt } from 'ydoc-shared/util/data/opt' test.each([ @@ -24,7 +24,7 @@ test.each([ makeStaticMethod('local.Project.Internalization.internalize'), ])('$name entry is in the CB main view', (entry) => { const filtering = new Filtering({}) - expect(filtering.filter(entry)).not.toBeNull() + expect(filtering.filter(entry, [])).not.toBeNull() }) test.each([ @@ -36,7 +36,7 @@ test.each([ makeStaticMethod('Standard.Base.Internal.Foo.bar'), // Internal method ])('$name entry is not in the CB main view', (entry) => { const filtering = new Filtering({}) - expect(filtering.filter(entry)).toBeNull() + expect(filtering.filter(entry, [])).toBeNull() }) test('An Instance method is shown when self arg matches', () => { @@ -45,16 +45,34 @@ test('An Instance method is shown when self arg matches', () => { const filteringWithSelfType = new Filtering({ selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' }, }) - expect(filteringWithSelfType.filter(entry1)).not.toBeNull() - expect(filteringWithSelfType.filter(entry2)).toBeNull() + expect(filteringWithSelfType.filter(entry1, [])).not.toBeNull() + expect(filteringWithSelfType.filter(entry2, [])).toBeNull() const filteringWithAnySelfType = new Filtering({ selfArg: { type: 'unknown' }, }) - expect(filteringWithAnySelfType.filter(entry1)).not.toBeNull() - expect(filteringWithAnySelfType.filter(entry2)).not.toBeNull() + expect(filteringWithAnySelfType.filter(entry1, [])).not.toBeNull() + expect(filteringWithAnySelfType.filter(entry2, [])).not.toBeNull() const filteringWithoutSelfType = new Filtering({ pattern: 'get' }) - expect(filteringWithoutSelfType.filter(entry1)).toBeNull() - expect(filteringWithoutSelfType.filter(entry2)).toBeNull() + expect(filteringWithoutSelfType.filter(entry1, [])).toBeNull() + expect(filteringWithoutSelfType.filter(entry2, [])).toBeNull() +}) + +test('Additional self types are taken into account when filtering', () => { + const entry1 = makeMethod('Standard.Base.Data.Vector.Vector.get') + const entry2 = makeMethod('Standard.Base.Any.Any.to_string') + const additionalSelfType = 'Standard.Base.Any.Any' as QualifiedName + const filtering = new Filtering({ + selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' }, + }) + expect(filtering.filter(entry1, [additionalSelfType])).not.toBeNull() + expect(filtering.filter(entry2, [additionalSelfType])).not.toBeNull() + expect(filtering.filter(entry2, [])).toBeNull() + + const filteringWithoutSelfType = new Filtering({}) + expect(filteringWithoutSelfType.filter(entry1, [additionalSelfType])).toBeNull() + expect(filteringWithoutSelfType.filter(entry2, [additionalSelfType])).toBeNull() + expect(filteringWithoutSelfType.filter(entry1, [])).toBeNull() + expect(filteringWithoutSelfType.filter(entry2, [])).toBeNull() }) test.each([ @@ -69,7 +87,7 @@ test.each([ const filtering = new Filtering({ selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' }, }) - expect(filtering.filter(entry)).toBeNull() + expect(filtering.filter(entry, [])).toBeNull() }) test.each` @@ -84,7 +102,7 @@ test.each` `('$name is not matched by pattern $pattern', ({ name, pattern }) => { const entry = makeModuleMethod(`local.Project.${name}`) const filtering = new Filtering({ pattern }) - expect(filtering.filter(entry)).toBeNull() + expect(filtering.filter(entry, [])).toBeNull() }) function matchedText(ownerName: string, name: string, matchResult: MatchResult) { @@ -200,7 +218,7 @@ test.each([ ...makeModuleMethod(`${module ?? 'local.Project'}.${name}`), aliases: aliases ?? [], })) - const matchResults = Array.from(matchedSortedEntries, (entry) => filtering.filter(entry)) + const matchResults = Array.from(matchedSortedEntries, (entry) => filtering.filter(entry, [])) // Checking matching entries function checkResult(entry: SuggestionEntry, result: Opt) { expect(result, `Matching entry ${entryQn(entry)}`).not.toBeNull() @@ -226,6 +244,6 @@ test.each([ ...makeModuleMethod(`${module ?? 'local.Project'}.${name}`), aliases: aliases ?? [], } - expect(filtering.filter(entry), entryQn(entry)).toBeNull() + expect(filtering.filter(entry, []), entryQn(entry)).toBeNull() } }) diff --git a/app/gui/src/project-view/components/ComponentBrowser/component.ts b/app/gui/src/project-view/components/ComponentBrowser/component.ts index 42f6341c88..7dfb429c58 100644 --- a/app/gui/src/project-view/components/ComponentBrowser/component.ts +++ b/app/gui/src/project-view/components/ComponentBrowser/component.ts @@ -10,7 +10,8 @@ import { isSome } from '@/util/data/opt' import { Range } from '@/util/data/range' import { displayedIconOf } from '@/util/getIconName' import type { Icon } from '@/util/iconName' -import { qnLastSegmentIndex } from '@/util/qualifiedName' +import { qnLastSegmentIndex, QualifiedName, tryQualifiedName } from '@/util/qualifiedName' +import { unwrap } from 'ydoc-shared/util/data/result' interface ComponentLabelInfo { label: string @@ -107,11 +108,21 @@ 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] + if (filtering.selfArg?.type === 'known') { + const maybeName = tryQualifiedName(filtering.selfArg.typename) + if (maybeName.ok) populateAdditionalSelfTypes(db, additionalSelfTypes, maybeName.value) + } + for (const [id, entry] of db.entries()) { - const match = filtering.filter(entry) + const match = filtering.filter(entry, additionalSelfTypes) if (isSome(match)) { yield { id, entry, match } } @@ -120,3 +131,16 @@ export function makeComponentList(db: SuggestionDb, filtering: Filtering): Compo const matched = Array.from(matchSuggestions()).sort(compareSuggestions) return Array.from(matched, (info) => makeComponent(info)) } + +/** + * Type can inherit methods from `parentType`, and it can do that recursively. + * In practice, these hierarchies are at most two levels deep. + */ +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) { + list.push(entry.parentType) + entry = db.getEntryByQualifiedName(entry.parentType) + } +} diff --git a/app/gui/src/project-view/components/ComponentBrowser/filtering.ts b/app/gui/src/project-view/components/ComponentBrowser/filtering.ts index 94b1bb2219..bcea401376 100644 --- a/app/gui/src/project-view/components/ComponentBrowser/filtering.ts +++ b/app/gui/src/project-view/components/ComponentBrowser/filtering.ts @@ -248,9 +248,13 @@ export class Filtering { if (currentModule != null) this.currentModule = currentModule } - private selfTypeMatches(entry: SuggestionEntry): boolean { + private selfTypeMatches(entry: SuggestionEntry, additionalSelfTypes: QualifiedName[]): boolean { if (this.selfArg == null) return entry.selfType == null - else if (this.selfArg.type == 'known') return entry.selfType === this.selfArg.typename + else if (this.selfArg.type == 'known') + return ( + entry.selfType === this.selfArg.typename || + additionalSelfTypes.some((t) => entry.selfType === t) + ) else return entry.selfType != null } @@ -271,11 +275,11 @@ export class Filtering { } /** TODO: Add docs */ - filter(entry: SuggestionEntry): MatchResult | null { + filter(entry: SuggestionEntry, additionalSelfTypes: QualifiedName[]): MatchResult | null { if (entry.isPrivate || entry.kind != SuggestionKind.Method || entry.memberOf == null) return null if (this.selfArg == null && isInternal(entry)) return null - if (!this.selfTypeMatches(entry)) return null + if (!this.selfTypeMatches(entry, additionalSelfTypes)) return null if (this.pattern) { if (entry.memberOf == null) return null const patternMatch = this.pattern.tryMatch(entry.name, entry.aliases, entry.memberOf) 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 ab1c5fae99..17e4b91bfb 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 @@ -320,6 +320,7 @@ class Fixture { aliases: ['Test Type'], isPrivate: false, isUnstable: false, + parentType: unwrap(tryQualifiedName('Standard.Base.Any.Any')), reexportedIn: unwrap(tryQualifiedName('Standard.Base.Another.Module')), annotations: [], } @@ -415,6 +416,7 @@ class Fixture { name: 'Type', params: [this.arg1], documentation: this.typeDocs, + parentType: 'Standard.Base.Any.Any', reexport: 'Standard.Base.Another.Module', }, }, diff --git a/app/gui/src/project-view/stores/suggestionDatabase/entry.ts b/app/gui/src/project-view/stores/suggestionDatabase/entry.ts index 1b66640151..76c7597160 100644 --- a/app/gui/src/project-view/stores/suggestionDatabase/entry.ts +++ b/app/gui/src/project-view/stores/suggestionDatabase/entry.ts @@ -59,6 +59,8 @@ export interface SuggestionEntry { arguments: SuggestionEntryArgument[] /** A type returned by the suggested object. */ returnType: Typename + /** Qualified name of the parent type. */ + parentType?: QualifiedName /** A least-nested module reexporting this entity. */ reexportedIn?: QualifiedName documentation: Doc.Section[] diff --git a/app/gui/src/project-view/stores/suggestionDatabase/lsUpdate.ts b/app/gui/src/project-view/stores/suggestionDatabase/lsUpdate.ts index 182fe17ee6..acb7a3bbf5 100644 --- a/app/gui/src/project-view/stores/suggestionDatabase/lsUpdate.ts +++ b/app/gui/src/project-view/stores/suggestionDatabase/lsUpdate.ts @@ -37,6 +37,7 @@ interface UnfinishedEntry { selfType?: Typename arguments?: SuggestionEntryArgument[] returnType?: Typename + parentType?: QualifiedName reexportedIn?: QualifiedName documentation?: Doc.Section[] scope?: SuggestionEntryScope @@ -110,6 +111,16 @@ function setLsReexported( return true } +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 +} + function setLsDocumentation( entry: UnfinishedEntry & { definedIn: QualifiedName }, documentation: Opt, @@ -171,6 +182,8 @@ export function entryFromLs( 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({