Ilya Bogdanov 2024-11-04 19:08:59 +04:00 committed by GitHub
parent 2bbd909705
commit d3beac3a90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 85 additions and 19 deletions

View File

@ -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

View File

@ -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()
} }
}) })

View File

@ -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 dont 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)
}
}

View File

@ -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)

View File

@ -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',
}, },
}, },

View File

@ -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[]

View File

@ -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({