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
|
- [Changed the way of adding new column in Table Input Widget][11388]. The
|
||||||
"virtual column" is replaced with an explicit (+) button.
|
"virtual column" is replaced with an explicit (+) button.
|
||||||
- [New dropdown-based component menu][11398].
|
- [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].
|
- [Undo/redo buttons in the top bar][11433].
|
||||||
- [Size of Table Input Widget is preserved and restored after project
|
- [Size of Table Input Widget is preserved and restored after project
|
||||||
re-opening][11435]
|
re-opening][11435]
|
||||||
@ -28,6 +30,7 @@
|
|||||||
[11383]: https://github.com/enso-org/enso/pull/11383
|
[11383]: https://github.com/enso-org/enso/pull/11383
|
||||||
[11388]: https://github.com/enso-org/enso/pull/11388
|
[11388]: https://github.com/enso-org/enso/pull/11388
|
||||||
[11398]: https://github.com/enso-org/enso/pull/11398
|
[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
|
[11433]: https://github.com/enso-org/enso/pull/11433
|
||||||
[11435]: https://github.com/enso-org/enso/pull/11435
|
[11435]: https://github.com/enso-org/enso/pull/11435
|
||||||
[11446]: https://github.com/enso-org/enso/pull/11446
|
[11446]: https://github.com/enso-org/enso/pull/11446
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
makeStaticMethod,
|
makeStaticMethod,
|
||||||
SuggestionEntry,
|
SuggestionEntry,
|
||||||
} from '@/stores/suggestionDatabase/entry'
|
} from '@/stores/suggestionDatabase/entry'
|
||||||
import { qnLastSegment } from '@/util/qualifiedName'
|
import { qnLastSegment, QualifiedName } from '@/util/qualifiedName'
|
||||||
import { Opt } from 'ydoc-shared/util/data/opt'
|
import { Opt } from 'ydoc-shared/util/data/opt'
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
@ -24,7 +24,7 @@ test.each([
|
|||||||
makeStaticMethod('local.Project.Internalization.internalize'),
|
makeStaticMethod('local.Project.Internalization.internalize'),
|
||||||
])('$name entry is in the CB main view', (entry) => {
|
])('$name entry is in the CB main view', (entry) => {
|
||||||
const filtering = new Filtering({})
|
const filtering = new Filtering({})
|
||||||
expect(filtering.filter(entry)).not.toBeNull()
|
expect(filtering.filter(entry, [])).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
@ -36,7 +36,7 @@ test.each([
|
|||||||
makeStaticMethod('Standard.Base.Internal.Foo.bar'), // Internal method
|
makeStaticMethod('Standard.Base.Internal.Foo.bar'), // Internal method
|
||||||
])('$name entry is not in the CB main view', (entry) => {
|
])('$name entry is not in the CB main view', (entry) => {
|
||||||
const filtering = new Filtering({})
|
const filtering = new Filtering({})
|
||||||
expect(filtering.filter(entry)).toBeNull()
|
expect(filtering.filter(entry, [])).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('An Instance method is shown when self arg matches', () => {
|
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({
|
const filteringWithSelfType = new Filtering({
|
||||||
selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' },
|
selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' },
|
||||||
})
|
})
|
||||||
expect(filteringWithSelfType.filter(entry1)).not.toBeNull()
|
expect(filteringWithSelfType.filter(entry1, [])).not.toBeNull()
|
||||||
expect(filteringWithSelfType.filter(entry2)).toBeNull()
|
expect(filteringWithSelfType.filter(entry2, [])).toBeNull()
|
||||||
const filteringWithAnySelfType = new Filtering({
|
const filteringWithAnySelfType = new Filtering({
|
||||||
selfArg: { type: 'unknown' },
|
selfArg: { type: 'unknown' },
|
||||||
})
|
})
|
||||||
expect(filteringWithAnySelfType.filter(entry1)).not.toBeNull()
|
expect(filteringWithAnySelfType.filter(entry1, [])).not.toBeNull()
|
||||||
expect(filteringWithAnySelfType.filter(entry2)).not.toBeNull()
|
expect(filteringWithAnySelfType.filter(entry2, [])).not.toBeNull()
|
||||||
const filteringWithoutSelfType = new Filtering({ pattern: 'get' })
|
const filteringWithoutSelfType = new Filtering({ pattern: 'get' })
|
||||||
expect(filteringWithoutSelfType.filter(entry1)).toBeNull()
|
expect(filteringWithoutSelfType.filter(entry1, [])).toBeNull()
|
||||||
expect(filteringWithoutSelfType.filter(entry2)).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([
|
test.each([
|
||||||
@ -69,7 +87,7 @@ test.each([
|
|||||||
const filtering = new Filtering({
|
const filtering = new Filtering({
|
||||||
selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' },
|
selfArg: { type: 'known', typename: 'Standard.Base.Data.Vector.Vector' },
|
||||||
})
|
})
|
||||||
expect(filtering.filter(entry)).toBeNull()
|
expect(filtering.filter(entry, [])).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test.each`
|
test.each`
|
||||||
@ -84,7 +102,7 @@ test.each`
|
|||||||
`('$name is not matched by pattern $pattern', ({ name, pattern }) => {
|
`('$name is not matched by pattern $pattern', ({ name, pattern }) => {
|
||||||
const entry = makeModuleMethod(`local.Project.${name}`)
|
const entry = makeModuleMethod(`local.Project.${name}`)
|
||||||
const filtering = new Filtering({ pattern })
|
const filtering = new Filtering({ pattern })
|
||||||
expect(filtering.filter(entry)).toBeNull()
|
expect(filtering.filter(entry, [])).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
function matchedText(ownerName: string, name: string, matchResult: MatchResult) {
|
function matchedText(ownerName: string, name: string, matchResult: MatchResult) {
|
||||||
@ -200,7 +218,7 @@ test.each([
|
|||||||
...makeModuleMethod(`${module ?? 'local.Project'}.${name}`),
|
...makeModuleMethod(`${module ?? 'local.Project'}.${name}`),
|
||||||
aliases: aliases ?? [],
|
aliases: aliases ?? [],
|
||||||
}))
|
}))
|
||||||
const matchResults = Array.from(matchedSortedEntries, (entry) => filtering.filter(entry))
|
const matchResults = Array.from(matchedSortedEntries, (entry) => filtering.filter(entry, []))
|
||||||
// Checking matching entries
|
// Checking matching entries
|
||||||
function checkResult(entry: SuggestionEntry, result: Opt<MatchResult>) {
|
function checkResult(entry: SuggestionEntry, result: Opt<MatchResult>) {
|
||||||
expect(result, `Matching entry ${entryQn(entry)}`).not.toBeNull()
|
expect(result, `Matching entry ${entryQn(entry)}`).not.toBeNull()
|
||||||
@ -226,6 +244,6 @@ test.each([
|
|||||||
...makeModuleMethod(`${module ?? 'local.Project'}.${name}`),
|
...makeModuleMethod(`${module ?? 'local.Project'}.${name}`),
|
||||||
aliases: aliases ?? [],
|
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 { Range } from '@/util/data/range'
|
||||||
import { displayedIconOf } from '@/util/getIconName'
|
import { displayedIconOf } from '@/util/getIconName'
|
||||||
import type { Icon } from '@/util/iconName'
|
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 {
|
interface ComponentLabelInfo {
|
||||||
label: string
|
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. */
|
/** Create {@link Component} list from filtered suggestions. */
|
||||||
export function makeComponentList(db: SuggestionDb, filtering: Filtering): Component[] {
|
export function makeComponentList(db: SuggestionDb, filtering: Filtering): Component[] {
|
||||||
function* matchSuggestions() {
|
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()) {
|
for (const [id, entry] of db.entries()) {
|
||||||
const match = filtering.filter(entry)
|
const match = filtering.filter(entry, additionalSelfTypes)
|
||||||
if (isSome(match)) {
|
if (isSome(match)) {
|
||||||
yield { id, entry, match }
|
yield { id, entry, match }
|
||||||
}
|
}
|
||||||
@ -120,3 +131,16 @@ export function makeComponentList(db: SuggestionDb, filtering: Filtering): Compo
|
|||||||
const matched = Array.from(matchSuggestions()).sort(compareSuggestions)
|
const matched = Array.from(matchSuggestions()).sort(compareSuggestions)
|
||||||
return Array.from(matched, (info) => makeComponent(info))
|
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
|
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
|
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
|
else return entry.selfType != null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,11 +275,11 @@ export class Filtering {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** TODO: Add docs */
|
/** 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)
|
if (entry.isPrivate || entry.kind != SuggestionKind.Method || entry.memberOf == null)
|
||||||
return null
|
return null
|
||||||
if (this.selfArg == null && isInternal(entry)) 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 (this.pattern) {
|
||||||
if (entry.memberOf == null) return null
|
if (entry.memberOf == null) return null
|
||||||
const patternMatch = this.pattern.tryMatch(entry.name, entry.aliases, entry.memberOf)
|
const patternMatch = this.pattern.tryMatch(entry.name, entry.aliases, entry.memberOf)
|
||||||
|
@ -320,6 +320,7 @@ class Fixture {
|
|||||||
aliases: ['Test Type'],
|
aliases: ['Test Type'],
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
isUnstable: false,
|
isUnstable: false,
|
||||||
|
parentType: unwrap(tryQualifiedName('Standard.Base.Any.Any')),
|
||||||
reexportedIn: unwrap(tryQualifiedName('Standard.Base.Another.Module')),
|
reexportedIn: unwrap(tryQualifiedName('Standard.Base.Another.Module')),
|
||||||
annotations: [],
|
annotations: [],
|
||||||
}
|
}
|
||||||
@ -415,6 +416,7 @@ class Fixture {
|
|||||||
name: 'Type',
|
name: 'Type',
|
||||||
params: [this.arg1],
|
params: [this.arg1],
|
||||||
documentation: this.typeDocs,
|
documentation: this.typeDocs,
|
||||||
|
parentType: 'Standard.Base.Any.Any',
|
||||||
reexport: 'Standard.Base.Another.Module',
|
reexport: 'Standard.Base.Another.Module',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -59,6 +59,8 @@ export interface SuggestionEntry {
|
|||||||
arguments: SuggestionEntryArgument[]
|
arguments: SuggestionEntryArgument[]
|
||||||
/** A type returned by the suggested object. */
|
/** A type returned by the suggested object. */
|
||||||
returnType: Typename
|
returnType: Typename
|
||||||
|
/** Qualified name of the parent type. */
|
||||||
|
parentType?: QualifiedName
|
||||||
/** A least-nested module reexporting this entity. */
|
/** A least-nested module reexporting this entity. */
|
||||||
reexportedIn?: QualifiedName
|
reexportedIn?: QualifiedName
|
||||||
documentation: Doc.Section[]
|
documentation: Doc.Section[]
|
||||||
|
@ -37,6 +37,7 @@ interface UnfinishedEntry {
|
|||||||
selfType?: Typename
|
selfType?: Typename
|
||||||
arguments?: SuggestionEntryArgument[]
|
arguments?: SuggestionEntryArgument[]
|
||||||
returnType?: Typename
|
returnType?: Typename
|
||||||
|
parentType?: QualifiedName
|
||||||
reexportedIn?: QualifiedName
|
reexportedIn?: QualifiedName
|
||||||
documentation?: Doc.Section[]
|
documentation?: Doc.Section[]
|
||||||
scope?: SuggestionEntryScope
|
scope?: SuggestionEntryScope
|
||||||
@ -110,6 +111,16 @@ function setLsReexported(
|
|||||||
return true
|
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(
|
function setLsDocumentation(
|
||||||
entry: UnfinishedEntry & { definedIn: QualifiedName },
|
entry: UnfinishedEntry & { definedIn: QualifiedName },
|
||||||
documentation: Opt<string>,
|
documentation: Opt<string>,
|
||||||
@ -171,6 +182,8 @@ export function entryFromLs(
|
|||||||
if (!setLsModule(entry, lsEntry.module)) return Err('Invalid module name')
|
if (!setLsModule(entry, lsEntry.module)) return Err('Invalid module name')
|
||||||
if (lsEntry.reexport != null && !setLsReexported(entry, lsEntry.reexport))
|
if (lsEntry.reexport != null && !setLsReexported(entry, lsEntry.reexport))
|
||||||
return Err('Invalid reexported module name')
|
return Err('Invalid reexported module name')
|
||||||
|
if (lsEntry.parentType != null && !setLsParentType(entry, lsEntry.parentType))
|
||||||
|
return Err('Invalid parent type')
|
||||||
setLsDocumentation(entry, lsEntry.documentation, groups)
|
setLsDocumentation(entry, lsEntry.documentation, groups)
|
||||||
assert(entry.returnType !== '') // Should be overwriten
|
assert(entry.returnType !== '') // Should be overwriten
|
||||||
return Ok({
|
return Ok({
|
||||||
|
Loading…
Reference in New Issue
Block a user