Skip to content

Commit

Permalink
Literals and operators in Component Browser (#12420)
Browse files Browse the repository at this point in the history
  • Loading branch information
farmaazon authored Mar 7, 2025
1 parent 3f943a2 commit 80a7d4f
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 50 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
- ["Insert link" button added to documentation panel][12365]
- [Cloud File Browser, when opened first time after opening project, shows and
highlights the currently set file][12184]
- [It's easier to write numeric/text nodes in Component Browser][12420]. When
typing digits only, any names containing digits are not the best match
anymore. Also unclosed text literals will be automatically closed.

[11889]: https://github.com/enso-org/enso/pull/11889
[11836]: https://github.com/enso-org/enso/pull/11836
Expand All @@ -56,6 +59,7 @@
[12386]: https://github.com/enso-org/enso/pull/12386
[12365]: https://github.com/enso-org/enso/pull/12365
[12184]: https://github.com/enso-org/enso/pull/12184
[12420]: https://github.com/enso-org/enso/pull/12420

#### Enso Standard Library

Expand Down
57 changes: 29 additions & 28 deletions app/gui/src/project-view/components/ComponentBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { componentBrowserBindings, listBindings } from '@/bindings'
import { type Component } from '@/components/ComponentBrowser/component'
import ComponentEditor from '@/components/ComponentBrowser/ComponentEditor.vue'
import ComponentList from '@/components/ComponentBrowser/ComponentList.vue'
import { Filtering } from '@/components/ComponentBrowser/filtering'
import { useComponentBrowserInput, type Usage } from '@/components/ComponentBrowser/input'
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
import SvgButton from '@/components/SvgButton.vue'
Expand All @@ -15,7 +14,6 @@ import { injectNodeColors } from '@/providers/graphNodeColors'
import { injectInteractionHandler, type Interaction } from '@/providers/interactionHandler'
import { useGraphStore } from '@/stores/graph'
import type { RequiredImport } from '@/stores/graph/imports'
import { useProjectStore } from '@/stores/project'
import { injectProjectNames } from '@/stores/projectNames'
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
import { type Typename } from '@/stores/suggestionDatabase/entry'
Expand All @@ -29,6 +27,8 @@ import { debouncedGetter } from '@/util/reactivity'
import type { ComponentInstance } from 'vue'
import { computed, onMounted, onUnmounted, ref, toValue, watch, watchEffect } from 'vue'
import type { SuggestionId } from 'ydoc-shared/languageServerTypes/suggestions'
import { Range } from 'ydoc-shared/util/data/range'
import { Ok } from 'ydoc-shared/util/data/result'
import type { VisualizationIdentifier } from 'ydoc-shared/yjsModel'
// Difference in position between the component browser and a node for the input of the component browser to
Expand All @@ -47,7 +47,6 @@ const EDGE_Y_OFFSET = -8
const cssComponentEditorPadding = `${COMPONENT_EDITOR_PADDING}px`
const projectStore = useProjectStore()
const suggestionDbStore = useSuggestionDbStore()
const graphStore = useGraphStore()
const interaction = injectInteractionHandler()
Expand Down Expand Up @@ -177,15 +176,6 @@ const selectedSuggestion = computed(() => {
const input = useComponentBrowserInput()
const currentFiltering = computed(() => {
if (input.mode.mode === 'componentBrowsing') {
const currentModule = projectStore.moduleProjectPath
return new Filtering(input.mode.filter, currentModule?.ok ? currentModule.value : undefined)
} else {
return undefined
}
})
onUnmounted(() => {
graphStore.cbEditedEdge = undefined
})
Expand Down Expand Up @@ -285,19 +275,26 @@ watch(
// === Accepting Entry ===
function acceptSuggestion(component: Opt<Component> = null) {
const suggestionId = component?.suggestionId ?? selectedSuggestionId.value
if (suggestionId == null) return acceptInput()
const result = input.applySuggestion(suggestionId)
if (result.ok) acceptInput()
else result.error.log('Cannot apply suggestion')
function applyComponent(component: Opt<Component> = null) {
component ??= selected.value
if (component == null) {
input.switchToCodeEditMode()
return Ok()
}
if (component.suggestionId != null) {
return input.applySuggestion(component.suggestionId)
} else {
// Component without suggestion database entry, for example "literal" component.
input.content = { text: component.label, selection: Range.emptyAt(component.label.length) }
input.switchToCodeEditMode()
return Ok()
}
}
function applySuggestion(component: Opt<Component> = null) {
const suggestionId = component?.suggestionId ?? selectedSuggestionId.value
if (suggestionId == null) return input.switchToCodeEditMode()
const result = input.applySuggestion(suggestionId)
if (!result.ok) result.error.log('Cannot apply suggestion')
function acceptComponent(component: Opt<Component> = null) {
const result = applyComponent(component)
if (result.ok) acceptInput()
else result.error.log('Cannot apply suggestion')
}
function acceptInput() {
Expand All @@ -314,11 +311,14 @@ function acceptInput() {
const outsideComponentBrowsing = computed(() => input.mode.mode != 'componentBrowsing')
const actions = registerHandlers({
'componentBrowser.editSuggestion': {
action: applySuggestion,
action: () => {
const result = applyComponent()
if (!result.ok) result.error.log('Cannot apply component')
},
disabled: outsideComponentBrowsing,
},
'componentBrowser.acceptSuggestion': {
action: acceptSuggestion,
action: acceptComponent,
disabled: outsideComponentBrowsing,
},
'componentBrowser.acceptInputAsCode': {
Expand Down Expand Up @@ -427,10 +427,11 @@ const listsHandler = listBindings.handler({
/>
</div>
<ComponentList
v-if="input.mode.mode === 'componentBrowsing' && currentFiltering"
v-if="input.mode.mode === 'componentBrowsing'"
ref="componentList"
:filtering="currentFiltering"
@acceptSuggestion="acceptSuggestion($event)"
:filter="input.mode.filter"
:literal="input.mode.literal"
@acceptSuggestion="acceptComponent($event)"
@update:selectedComponent="selected = $event"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
<script setup lang="ts">
import { makeComponentList, type Component } from '@/components/ComponentBrowser/component'
import { makeComponentLists, type Component } from '@/components/ComponentBrowser/component'
import ComponentEntry from '@/components/ComponentBrowser/ComponentEntry.vue'
import type { Filtering } from '@/components/ComponentBrowser/filtering'
import { Filter, Filtering } from '@/components/ComponentBrowser/filtering'
import SvgIcon from '@/components/SvgIcon.vue'
import VirtualizedList from '@/components/VirtualizedList.vue'
import { groupColorStyle } from '@/composables/nodeColors'
import { useProjectStore } from '@/stores/project'
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
import { Ast } from '@/util/ast'
import { tryGetIndex } from '@/util/data/array'
import { computed, ref, toRef, watch } from 'vue'
import * as map from 'lib0/map'
import { computed, ref, watch } from 'vue'
import type { ComponentExposed } from 'vue-component-type-helpers'
const ITEM_SIZE = 24
const SCROLL_TO_SELECTION_MARGIN = ITEM_SIZE / 2
const MOUSE_SELECTION_DEBOUNCE = 200
const props = defineProps<{
filtering: Filtering
filter: Filter
literal?: Ast.Ast | undefined
}>()
const emit = defineEmits<{
acceptSuggestion: [suggestion: Component]
'update:selectedComponent': [selected: Component | null]
}>()
const projectStore = useProjectStore()
const root = ref<HTMLElement>()
const groupsPanel = ref<ComponentExposed<typeof VirtualizedList>>()
const componentsPanel = ref<ComponentExposed<typeof VirtualizedList>>()
Expand All @@ -42,18 +47,34 @@ const displayedSelectedComponentIndex = computed({
},
})
watch(toRef(props, 'filtering'), () => (displayedSelectedComponentIndex.value = 0))
const filtering = computed(() => {
const currentModule = projectStore.moduleProjectPath
return new Filtering(props.filter, currentModule?.ok ? currentModule.value : undefined)
})
watch(filtering, () => (displayedSelectedComponentIndex.value = 0))
watch(selectedGroupIndex, () => (selectedComponentIndex.value = 0))
const suggestionDbStore = useSuggestionDbStore()
const components = computed(() => makeComponentList(suggestionDbStore.entries, props.filtering))
const components = computed(() => {
const lists = makeComponentLists(suggestionDbStore.entries, filtering.value)
if (props.literal != null) {
map
.setIfUndefined(lists, 'all', (): Component[] => [])
.unshift({
label: props.literal.code(),
icon: props.literal instanceof Ast.TextLiteral ? 'text_input' : 'input_number',
})
}
return lists
})
const currentGroups = computed(() => {
return Array.from(components.value.entries(), ([id, components]) => ({
id,
...(id === 'all' ? { name: 'all' }
: id === 'suggestions' ? { name: 'suggestions' }
: (suggestionDbStore.groups[id] ?? { name: 'unknown' })),
...(props.filtering.pattern != null ? { displayedNumber: components.length } : {}),
...(filtering.value?.pattern != null ? { displayedNumber: components.length } : {}),
}))
})
const displayedGroupId = computed(() =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useComponentBrowserInput } from '@/components/ComponentBrowser/input'
import { GraphDb, NodeId } from '@/stores/graph/graphDatabase'
import { ComputedValueRegistry } from '@/stores/project/computedValueRegistry'
import { SuggestionDb } from '@/stores/suggestionDatabase'
import { unwrap } from '@/util/data/result'
import { parseAbsoluteProjectPathRaw } from '@/util/projectPath'
import { expect, test } from 'vitest'
import { parseExpression } from 'ydoc-shared/ast'
import { assert, assertUnreachable } from 'ydoc-shared/util/assert'
import { Range } from 'ydoc-shared/util/data/range'

const aiMock = { query: assertUnreachable }
const operator1Id = '3d0e9b96-3ca0-4c35-a820-7d3a1649de55' as NodeId
const operator2Id = '5eb16101-dd2b-4034-a6e2-476e8bfa1f2b' as NodeId

function mockGraphDb() {
const computedValueRegistryMock = ComputedValueRegistry.Mock()
computedValueRegistryMock.db.set(operator1Id, {
typename: unwrap(parseAbsoluteProjectPathRaw('Standard.Base.Number')),
rawTypename: 'Standard.Base.Number',
methodCall: undefined,
payload: { type: 'Value' },
profilingInfo: [],
})
const db = GraphDb.Mock(computedValueRegistryMock)
db.mockNode('operator1', operator1Id, 'Data.read')
db.mockNode('operator2', operator2Id)
return db
}

test.each`
inputContent | expectedLiteral
${'read'} | ${undefined}
${'operator1'} | ${undefined}
${'12 + 14'} | ${undefined}
${'12'} | ${'12'}
${'12.6'} | ${'12.6'}
${'-12'} | ${'-12'}
${'-12.6'} | ${'-12.6'}
${'- 12.6'} | ${undefined /* in Enso this is partial OprApp, not negation */}
${'"text"'} | ${'"text"'}
${"'text'"} | ${"'text'"}
${"'text"} | ${"'text'"}
${"'''text"} | ${"'''text"}
`('Reading literal from $inputContent', ({ inputContent, expectedLiteral }) => {
console.log(parseExpression(inputContent))
const input = useComponentBrowserInput(mockGraphDb(), new SuggestionDb(), aiMock)
input.reset({ type: 'newNode' })
input.content = { text: inputContent, selection: Range.empty }
assert(input.mode.mode === 'componentBrowsing')
expect(input.mode.literal?.code()).toBe(expectedLiteral)
})

test.each`
inputContent | source | expectedCode
${'read'} | ${'operator1'} | ${'operator1.read'}
${'read'} | ${'operator2'} | ${'operator2.read'}
${'read "file"'} | ${'operator2'} | ${'operator2.read "file"'}
${'+ 2'} | ${'operator1'} | ${'operator1 + 2'}
${'+ 3'} | ${'operator2'} | ${'operator2 + 3'}
${'+2'} | ${'operator1'} | ${'operator1+2'}
${'-2'} | ${'operator1'} | ${'operator1-2'}
`(
'Code generated by CB from $inputContent with source node $sourceNode',
({ inputContent, source, expectedCode }) => {
const db = mockGraphDb()
const input = useComponentBrowserInput(db, new SuggestionDb(), aiMock)
const sourcePort = db.getNodeFirstOutputPort(db.getIdentDefiningNode(source))
assert(sourcePort != null)
input.reset({ type: 'newNode', sourcePort })
input.content = { text: inputContent, selection: Range.empty }
expect(input.code).toBe(expectedCode)
},
)
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface ComponentLabel {

/** A model of component suggestion displayed in the Component Browser. */
export interface Component extends ComponentLabel {
suggestionId: SuggestionId
suggestionId?: SuggestionId
icon: Icon
group?: number | undefined
}
Expand Down Expand Up @@ -112,7 +112,7 @@ export function makeComponent({ id, entry, match }: ComponentInfo): Component {
}

/** Create {@link Component} list for each displayed group from filtered suggestions. */
export function makeComponentList(
export function makeComponentLists(
db: SuggestionDb,
filtering: Filtering,
): Map<GroupId, Component[]> {
Expand Down
Loading

0 comments on commit 80a7d4f

Please sign in to comment.