mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 03:32:23 +03:00
Consider additional self types (#11451)
Fixes #11427 Before: https://github.com/user-attachments/assets/fc16cefd-f264-4410-bd30-1747c580da1a After: https://github.com/user-attachments/assets/e85b8fb3-35c4-4d18-a9a0-2aeb69201b6f
This commit is contained in:
parent
2bbd909705
commit
d3beac3a90
@ -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
|
||||
|
@ -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<MatchResult>) {
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
@ -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[]
|
||||
|
@ -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<string>,
|
||||
@ -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({
|
||||
|
Loading…
Reference in New Issue
Block a user