Skip to content

Commit 15fcf14

Browse files
authored
Visible intersection types handling (#13266)
Closes #13042 - Methods of “visible” intersection type are now visible in CB (without type casting inserted) - Refactoring of CB filtering and managing of types in general https://github.com/user-attachments/assets/e78d2477-5af7-4c76-a9c0-9ebca6eb82d0
1 parent 78a0db4 commit 15fcf14

32 files changed

+324
-209
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
- [Add keyboard shortcuts for formatting documentation][13134]
1919
- [New right-side panel][13135], unified between tabs.
2020
- [Allow selecting expected types for arguments of grouped components.][13161]
21+
- [Methods for ‘intersection’ types are now visible in Component
22+
Browser.][13266]
2123
- [Allow marking grouped component arguments as required or providing a default
2224
value.][13254]
2325

@@ -35,6 +37,7 @@
3537
[13134]: https://github.com/enso-org/enso/pull/13134
3638
[13135]: https://github.com/enso-org/enso/pull/13135
3739
[13161]: https://github.com/enso-org/enso/pull/13161
40+
[13266]: https://github.com/enso-org/enso/pull/13266
3841
[13254]: https://github.com/enso-org/enso/pull/13254
3942

4043
#### Enso Standard Library

app/gui/src/project-view/components/CodeEditor/CodeEditorTooltip.vue

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import { injectCurrentProject } from '$/components/WithCurrentProject.vue'
23
import { type NodeId } from '@/stores/graph'
34
import { type GraphDb } from '@/stores/graph/graphDatabase'
45
import { type SuggestionDbStore } from '@/stores/suggestionDatabase'
@@ -11,16 +12,24 @@ const { nodeId, syntax, graphDb, suggestionDbStore } = defineProps<{
1112
suggestionDbStore: SuggestionDbStore
1213
}>()
1314
15+
const { names: projectNames } = injectCurrentProject().storesRefs
16+
1417
const expressionInfo = computed(() => nodeId && graphDb.getExpressionInfo(nodeId))
15-
const typeName = computed(
16-
() => expressionInfo.value && (expressionInfo.value.typename ?? 'Unknown'),
17-
)
18+
const typeName = computed(() => {
19+
const type = expressionInfo.value?.typeInfo?.primaryType
20+
if (type == null || projectNames.value == null) return 'Unknown'
21+
return projectNames.value.printProjectPath(type)
22+
})
1823
const executionTimeMs = computed(
1924
() =>
2025
expressionInfo.value?.profilingInfo[0] &&
2126
(expressionInfo.value.profilingInfo[0].ExecutionTime.nanoTime / 1_000_000).toFixed(3),
2227
)
2328
const method = computed(() => expressionInfo.value?.methodCall?.methodPointer)
29+
const methodPath = computed(() => {
30+
if (method.value == null || projectNames.value == null) return 'Unknown'
31+
return projectNames.value.printProjectPath(method.value.definedOnType) + '.' + method.value.name
32+
})
2433
const group = computed(() => {
2534
const id = method.value && suggestionDbStore.entries.findByMethodPointer(method.value)
2635
if (id == null) return
@@ -42,6 +51,6 @@ const group = computed(() => {
4251
<div v-if="typeName">Type: {{ typeName }}</div>
4352
<div v-if="executionTimeMs != null">Execution Time: {{ executionTimeMs }}ms</div>
4453
<div>Syntax: {{ syntax }}</div>
45-
<div v-if="method">Method: {{ method.module }}.{{ method.name }}</div>
54+
<div v-if="methodPath">Method: {{ methodPath }}</div>
4655
<div v-if="group" :style="{ color: group.color }">Group: {{ group.name }}</div>
4756
</template>

app/gui/src/project-view/components/ComponentBrowser.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { tryGetIndex } from '@/util/data/array'
2525
import type { Opt } from '@/util/data/opt'
2626
import { Rect } from '@/util/data/rect'
2727
import { Vec2 } from '@/util/data/vec2'
28+
import { parseAbsoluteProjectPathRaw } from '@/util/projectPath'
2829
import { debouncedGetter } from '@/util/reactivity'
2930
import type { ComponentInstance } from 'vue'
3031
import { computed, onMounted, onUnmounted, ref, toValue, watch, watchEffect } from 'vue'
@@ -247,7 +248,11 @@ const previewedSuggestionReturnType = computed(() => {
247248
appliedEntry ? appliedEntry
248249
: props.usage.type === 'editNode' ? graphStore.db.getNodeMainSuggestion(props.usage.node)
249250
: undefined
250-
return entry?.returnType(projectNames)
251+
const returnType = entry?.returnType(projectNames)
252+
if (returnType == null) return undefined
253+
const parsed = parseAbsoluteProjectPathRaw(returnType)
254+
if (parsed.ok) return parsed.value
255+
return undefined
251256
})
252257
253258
const previewDataSource = computed<VisualizationDataSource | undefined>(() => {

app/gui/src/project-view/components/ComponentBrowser/ComponentEditorLabel.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type DisplayedAdditionalTypes =
1313
1414
const additionalTypes = computed<DisplayedAdditionalTypes>(() => {
1515
if (props.selfArg?.type === 'known') {
16-
const additionalTypes = props.selfArg.additionalTypes.flatMap((type) =>
16+
const additionalTypes = props.selfArg.typeInfo?.hiddenTypes.flatMap((type) =>
1717
type.path ? qnLastSegment(type.path) : [],
1818
)
1919
if (additionalTypes.length === 0) return null
@@ -28,8 +28,8 @@ const additionalTypes = computed<DisplayedAdditionalTypes>(() => {
2828
2929
const label = computed(() => {
3030
if (props.selfArg == null) return 'Input'
31-
if (props.selfArg.type === 'known' && props.selfArg.typename.path) {
32-
return qnLastSegment(props.selfArg.typename.path)
31+
if (props.selfArg.type === 'known' && props.selfArg.typeInfo?.primaryType.path) {
32+
return qnLastSegment(props.selfArg.typeInfo.primaryType.path)
3333
}
3434
3535
return undefined

app/gui/src/project-view/components/ComponentBrowser/__tests__/component.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type MatchedSuggestion,
77
} from '@/components/ComponentBrowser/component'
88
import { Filtering } from '@/components/ComponentBrowser/filtering'
9+
import { TypeInfo } from '@/stores/project/computedValueRegistry'
910
import {
1011
makeConstructor,
1112
makeMethod,
@@ -14,7 +15,7 @@ import {
1415
makeStaticMethod,
1516
} from '@/stores/suggestionDatabase/mockSuggestion'
1617
import { allRanges } from '@/util/data/range'
17-
import { ProjectPath } from '@/util/projectPath'
18+
import { ProjectPath, stdPath } from '@/util/projectPath'
1819
import { QualifiedName } from '@/util/qualifiedName'
1920
import shuffleSeed from 'shuffle-seed'
2021
import { expect, test } from 'vitest'
@@ -137,13 +138,15 @@ test.each`
137138
'Matched ranges of $highlighted with additional type are correct',
138139
({ name, aliases, highlighted }) => {
139140
const pattern = 'foo_bar'
140-
const entry = makeMethod(`local.Mock_Project.${name}`, { aliases: aliases ?? [] })
141+
const entry = makeMethod(`Standard.Base.${name}`, { aliases: aliases ?? [] })
141142
const filtering = new Filtering({
142143
pattern,
143144
selfArg: {
144145
type: 'known',
145-
typename: ProjectPath.create(undefined, 'Column' as QualifiedName),
146-
additionalTypes: [ProjectPath.create(undefined, 'Table' as QualifiedName)],
146+
typeInfo: TypeInfo.fromParsedTypes(
147+
[stdPath('Standard.Base.Column')],
148+
[stdPath('Standard.Base.Table')],
149+
)!,
147150
ancestors: [],
148151
},
149152
})

app/gui/src/project-view/components/ComponentBrowser/__tests__/filtering.test.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Filtering, type MatchResult } from '@/components/ComponentBrowser/filtering'
2+
import { TypeInfo } from '@/stores/project/computedValueRegistry'
23
import { type SuggestionEntry } from '@/stores/suggestionDatabase/entry'
34
import {
45
makeConstructor,
@@ -9,12 +10,10 @@ import {
910
makeModuleMethod,
1011
makeStaticMethod,
1112
} from '@/stores/suggestionDatabase/mockSuggestion'
12-
import { assert } from '@/util/assert'
13-
import { parseAbsoluteProjectPathRaw } from '@/util/projectPath'
13+
import { stdPath } from '@/util/projectPath'
1414
import { qnLastSegment } from '@/util/qualifiedName'
1515
import { expect, test } from 'vitest'
1616
import { type Opt } from 'ydoc-shared/util/data/opt'
17-
import { unwrap } from 'ydoc-shared/util/data/result'
1817

1918
test.each([
2019
makeModuleMethod('Standard.Base.Data.read', { group: 'Standard.Base.MockGroup1' }),
@@ -40,24 +39,32 @@ test.each([
4039
expect(filtering.filter(entry)).toBeNull()
4140
})
4241

43-
function stdPath(path: string) {
44-
assert(path.startsWith('Standard.'))
45-
return unwrap(parseAbsoluteProjectPathRaw(path))
46-
}
42+
test.each`
43+
visibleTypes | entry1Matched | entry2Matched
44+
${['Standard.Base.Data.Table']} | ${false} | ${true}
45+
${['Standard.Base.Data.Vector.Vector']} | ${true} | ${false}
46+
${['Standard.Base.Data.Vector.Vector', 'Standard.Base.Data.Table']} | ${true} | ${true}
47+
${['Standard.Base.Data.Table', 'Standard.Base.Data.Vector.Vector']} | ${true} | ${true}
48+
`(
49+
`Visible types are taken into account when filtering: $visibleTypes`,
50+
({ visibleTypes, entry1Matched, entry2Matched }) => {
51+
const entry1 = makeMethod('Standard.Base.Data.Vector.Vector.get')
52+
const entry2 = makeMethod('Standard.Base.Data.Table.get')
53+
const filtering = new Filtering({
54+
selfArg: {
55+
type: 'known',
56+
typeInfo: TypeInfo.fromParsedTypes(visibleTypes.map(stdPath), [])!,
57+
ancestors: [],
58+
},
59+
})
60+
expect(filtering.filter(entry1)).toEqual(entry1Matched ? { score: 0 } : null)
61+
expect(filtering.filter(entry2)).toEqual(entry2Matched ? { score: 0 } : null)
62+
},
63+
)
4764

48-
test('An Instance method is shown when self arg matches', () => {
65+
test('Filtering methods with no self type information', () => {
4966
const entry1 = makeMethod('Standard.Base.Data.Vector.Vector.get')
5067
const entry2 = makeMethod('Standard.Base.Data.Table.get')
51-
const filteringWithSelfType = new Filtering({
52-
selfArg: {
53-
type: 'known',
54-
typename: stdPath('Standard.Base.Data.Vector.Vector'),
55-
additionalTypes: [],
56-
ancestors: [],
57-
},
58-
})
59-
expect(filteringWithSelfType.filter(entry1)).not.toBeNull()
60-
expect(filteringWithSelfType.filter(entry2)).toBeNull()
6168
const filteringWithAnySelfType = new Filtering({
6269
selfArg: { type: 'unknown' },
6370
})
@@ -74,8 +81,7 @@ test('`Any` type methods taken into account when filtering', () => {
7481
const filtering = new Filtering({
7582
selfArg: {
7683
type: 'known',
77-
typename: stdPath('Standard.Base.Data.Vector.Vector'),
78-
additionalTypes: [],
84+
typeInfo: TypeInfo.fromParsedTypes([stdPath('Standard.Base.Data.Vector.Vector')], [])!,
7985
ancestors: [],
8086
},
8187
})
@@ -87,20 +93,25 @@ test('`Any` type methods taken into account when filtering', () => {
8793
expect(filteringWithoutSelfType.filter(entry2)).toBeNull()
8894
})
8995

90-
test('Additional self types and ancestors are taken into account when filtering', () => {
96+
test('Hidden self types and ancestors are taken into account when filtering', () => {
9197
const entry1 = makeMethod('Standard.Base.Data.Numbers.Float.abs')
9298
const entry2 = makeMethod('Standard.Base.Data.Numbers.Number.sqrt')
93-
const additionalSelfType = stdPath('Standard.Base.Data.Numbers.Number')
94-
const filteringWithAdditionalSelfType = new Filtering({
99+
const hiddenSelfType = 'Standard.Base.Data.Numbers.Number'
100+
const filteringWithHiddenSelfType = new Filtering({
95101
selfArg: {
96102
type: 'known',
97-
typename: stdPath('Standard.Base.Data.Numbers.Float'),
98-
additionalTypes: [additionalSelfType],
103+
typeInfo: TypeInfo.fromParsedTypes(
104+
[stdPath('Standard.Base.Data.Numbers.Float')],
105+
[stdPath(hiddenSelfType)],
106+
)!,
99107
ancestors: [],
100108
},
101109
})
102-
expect(filteringWithAdditionalSelfType.filter(entry1)).not.toBeNull()
103-
expect(filteringWithAdditionalSelfType.filter(entry2)).not.toBeNull()
110+
expect(filteringWithHiddenSelfType.filter(entry1)).toEqual({ score: 0 })
111+
expect(filteringWithHiddenSelfType.filter(entry2)).toEqual({
112+
score: 1,
113+
fromType: stdPath(hiddenSelfType),
114+
})
104115

105116
const filteringWithoutSelfType = new Filtering({})
106117
expect(filteringWithoutSelfType.filter(entry1)).toBeNull()
@@ -109,13 +120,15 @@ test('Additional self types and ancestors are taken into account when filtering'
109120
const filteringWithAncestors = new Filtering({
110121
selfArg: {
111122
type: 'known',
112-
typename: stdPath('Standard.Base.Data.Numbers.Float'),
113-
additionalTypes: [],
114-
ancestors: [additionalSelfType],
123+
typeInfo: TypeInfo.fromParsedTypes([stdPath('Standard.Base.Data.Numbers.Float')], [])!,
124+
ancestors: [stdPath(hiddenSelfType)],
115125
},
116126
})
117-
expect(filteringWithAncestors.filter(entry1)).not.toBeNull()
118-
expect(filteringWithAncestors.filter(entry2)).not.toBeNull()
127+
expect(filteringWithAncestors.filter(entry1)).toEqual({ score: 0 })
128+
expect(filteringWithAncestors.filter(entry2)).toEqual({
129+
score: 1,
130+
fromType: undefined,
131+
})
119132
})
120133

121134
test.each([
@@ -130,8 +143,7 @@ test.each([
130143
const filtering = new Filtering({
131144
selfArg: {
132145
type: 'known',
133-
typename: stdPath('Standard.Base.Data.Vector.Vector'),
134-
additionalTypes: [],
146+
typeInfo: TypeInfo.fromParsedTypes([stdPath('Standard.Base.Data.Vector.Vector')], [])!,
135147
ancestors: [],
136148
},
137149
})

app/gui/src/project-view/components/ComponentBrowser/__tests__/input.test.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { useComponentBrowserInput } from '@/components/ComponentBrowser/input'
22
import { GraphDb, NodeId } from '@/stores/graph/graphDatabase'
3-
import { ComputedValueRegistry } from '@/stores/project/computedValueRegistry'
3+
import { ComputedValueRegistry, TypeInfo } from '@/stores/project/computedValueRegistry'
44
import { SuggestionDb } from '@/stores/suggestionDatabase'
55
import { makeMethod, makeType } from '@/stores/suggestionDatabase/mockSuggestion'
6-
import { unwrap } from '@/util/data/result'
7-
import { parseAbsoluteProjectPathRaw } from '@/util/projectPath'
6+
import { stdPath } from '@/util/projectPath'
87
import { expect, test } from 'vitest'
98
import { assert, assertUnreachable } from 'ydoc-shared/util/assert'
109
import { Range } from 'ydoc-shared/util/data/range'
@@ -16,12 +15,10 @@ const operator2Id = '5eb16101-dd2b-4034-a6e2-476e8bfa1f2b' as NodeId
1615
function mockGraphDb() {
1716
const computedValueRegistryMock = ComputedValueRegistry.Mock()
1817
computedValueRegistryMock.db.set(operator1Id, {
19-
typename: unwrap(parseAbsoluteProjectPathRaw('Standard.Base.Number')),
20-
rawTypename: 'Standard.Base.Number',
18+
typeInfo: TypeInfo.fromParsedTypes([stdPath('Standard.Base.Number')], [])!,
2119
methodCall: undefined,
2220
payload: { type: 'Value' },
2321
profilingInfo: [],
24-
hiddenTypes: [],
2522
evaluationId: 1,
2623
})
2724
const db = GraphDb.Mock(computedValueRegistryMock)

app/gui/src/project-view/components/ComponentBrowser/filtering.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { TypeInfo } from '@/stores/project/computedValueRegistry'
12
import { SuggestionKind, type SuggestionEntry } from '@/stores/suggestionDatabase/entry'
23
import { ANY_TYPE } from '@/util/ensoTypes'
34
import { type ProjectPath } from '@/util/projectPath'
@@ -8,12 +9,8 @@ import { Range } from 'ydoc-shared/util/data/range'
89
export type SelfArg =
910
| {
1011
type: 'known'
11-
/** Type of the self argument. */
12-
typename: ProjectPath
13-
/** Ancestors of the type of the self argument. Does not include `Any` type. */
12+
typeInfo: TypeInfo
1413
ancestors: ProjectPath[]
15-
/** Additional (or ‘hidden’) types of the self argument. E.g. `Column` for single-column table.*/
16-
additionalTypes: ProjectPath[]
1714
}
1815
| { type: 'unknown' }
1916

@@ -282,13 +279,14 @@ export class Filtering {
282279
if (entry.kind !== SuggestionKind.Method || entry.selfType == null) return null
283280
if (this.selfArg.type !== 'known') return exactMatch()
284281
const entrySelfType = entry.selfType
285-
if (entrySelfType.equals(this.selfArg.typename)) return exactMatch()
286-
const { additionalTypes, ancestors } = this.selfArg
287-
const additionalType = additionalTypes.find((t) => entrySelfType.equals(t))
288-
const matchedAncestor = ancestors.find((t) => entrySelfType.equals(t))
289-
if (entrySelfType.equals(ANY_TYPE) || additionalType != null || matchedAncestor != null)
290-
// Matched ancestor are not added to `fromType`.
291-
return { score: DIFFERENT_TYPE_PENALTY, fromType: additionalType }
282+
const visibleTypes = this.selfArg.typeInfo.visibleTypes
283+
const visibleTypeMatch = visibleTypes?.find((ty) => entrySelfType.equals(ty))
284+
if (visibleTypeMatch != null) return exactMatch()
285+
const hiddenTypeMatch = this.selfArg.typeInfo?.hiddenTypes.find((t) => entrySelfType.equals(t))
286+
const matchedAncestor = this.selfArg.ancestors.find((t) => entrySelfType.equals(t))
287+
if (entrySelfType.equals(ANY_TYPE) || hiddenTypeMatch != null || matchedAncestor != null)
288+
// Matched ancestor are not added to `fromType`, because type casting is not needed.
289+
return { score: DIFFERENT_TYPE_PENALTY, fromType: hiddenTypeMatch }
292290
return null
293291
}
294292

@@ -322,7 +320,8 @@ export class Filtering {
322320
const selfTypeMatch = this.selfTypeMatches(entry)
323321
if (selfTypeMatch == null) return null
324322
if (this.pattern) {
325-
const additionalSelfTypes = this.selfArg?.type === 'known' ? this.selfArg.additionalTypes : []
323+
const additionalSelfTypes =
324+
this.selfArg?.type === 'known' ? this.selfArg.typeInfo.hiddenTypes : []
326325
const patternMatch = this.pattern.tryMatch(
327326
entry.name,
328327
entry.aliasesAndMacros,

app/gui/src/project-view/components/ComponentBrowser/input.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,9 @@ export function useComponentBrowserInput(
140140
const definition = graphDb.getIdentDefiningNode(sourceNodeIdentifier.value)
141141
if (definition == null) return null
142142
const info = graphDb.getExpressionInfo(definition)
143-
if (info == null) return { type: 'unknown' }
144-
const { typename, hiddenTypes } = info
145-
const additionalTypes = [...hiddenTypes]
146-
const ancestors = []
147-
if (typename != null) {
148-
const entry = suggestionDb.getEntryByProjectPath(typename)
149-
if (entry) ancestors.push(...suggestionDb.ancestors(entry))
150-
}
151-
return typename ? { type: 'known', typename, additionalTypes, ancestors } : { type: 'unknown' }
143+
if (info == null || info.typeInfo == null) return { type: 'unknown' }
144+
const ancestors = [...info.typeInfo.ancestors(suggestionDb)]
145+
return { type: 'known', typeInfo: info.typeInfo, ancestors }
152146
})
153147

154148
/** Apply given suggested entry to the input. */
@@ -183,7 +177,7 @@ export function useComponentBrowserInput(
183177
requiredImport: ProjectPath | undefined
184178
} {
185179
if (sourceNodeIdentifier.value && sourceNodeType.value?.type === 'known') {
186-
const sourceType = sourceNodeType.value.typename
180+
const sourceType = sourceNodeType.value.typeInfo.primaryType
187181
if (
188182
entryHasOwner(entry) &&
189183
!sourceType.equals(entry.memberOf) &&

app/gui/src/project-view/components/GraphEditor/GraphNode.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ const {
185185
nodeRect,
186186
scale,
187187
isFocused: isOnlyOneSelected,
188-
typename: () => expressionInfo.value?.rawTypename,
188+
typename: () => expressionInfo.value?.typeInfo?.primaryType,
189189
dataSource: () => ({ type: 'node', nodeId: props.node.rootExpr.externalId }) as const,
190190
emit,
191191
})

0 commit comments

Comments
 (0)