mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 18:34:03 +03:00
Change "Override Execution Context" button to "Record" button on nodes (#9188)
- Close #9164 - Fix appearance of Record/Record Once icon in top menu - Change icon for overriding execution context to record icon - Unconditionally show per-node record icon if it is set - Remove the ability to override the execution context to disabled - Fix the icon for nodes with an overridden execution context always being the Enso icon # Important Notes None
This commit is contained in:
parent
d2f6b1026a
commit
6c2b2383d3
@ -6,6 +6,7 @@ import { mockDataHandler, mockLSHandler } from '../mock/engine'
|
||||
import '../src/assets/main.css'
|
||||
import { provideGuiConfig } from '../src/providers/guiConfig'
|
||||
import { provideVisualizationConfig } from '../src/providers/visualizationConfig'
|
||||
import { initializePrefixes } from '../src/util/ast/node'
|
||||
import { Vec2 } from '../src/util/data/vec2'
|
||||
import { MockTransport, MockWebSocket } from '../src/util/net'
|
||||
import MockApp from './MockApp.vue'
|
||||
@ -56,4 +57,7 @@ provideVisualizationConfig._mock(
|
||||
},
|
||||
app,
|
||||
)
|
||||
initializeFFI().then(() => app.mount('#app'))
|
||||
initializeFFI().then(() => {
|
||||
initializePrefixes()
|
||||
app.mount('#app')
|
||||
})
|
||||
|
@ -677,11 +677,13 @@
|
||||
</symbol>
|
||||
|
||||
<symbol id="record" viewBox="0 0 16 16" width="16" height="16" fill="none">
|
||||
<path d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z" fill="currentColor"></path>
|
||||
<circle cx="8" cy="8" r="7" fill="currentColor"></circle>
|
||||
</symbol>
|
||||
|
||||
<symbol id="record_once" viewBox="0 0 16 16" width="16" height="16" fill="none">
|
||||
<path d="M1.33333 2H.66667C.29848 2 0 2.38376 0 2.85714V13.1429C0 13.6162.29848 14 .66667 14H1.33333C1.70152 14 2 13.6162 2 13.1429V2.85714C2 2.38376 1.70152 2 1.33333 2ZM8 13C10.7614 13 13 10.7614 13 8 13 5.23858 10.7614 3 8 3 5.23858 3 3 5.23858 3 8 3 10.7614 5.23858 13 8 13ZM15.3333 2H14.6667C14.2985 2 14 2.38376 14 2.85714V13.1429C14 13.6162 14.2985 14 14.6667 14H15.3333C15.7015 14 16 13.6162 16 13.1429V2.85714C16 2.38376 15.7015 2 15.3333 2Z" fill="currentColor"></path>
|
||||
<rect x="14" y="3.33334" width="2" height="9.33333" rx="0.666667" fill="currentColor"></rect>
|
||||
<circle cx="8.00001" cy="8.00001" r="4.66667" fill="currentColor"></circle>
|
||||
<rect y="3.33334" width="2" height="9.33333" rx="0.666667" fill="currentColor"></rect>
|
||||
</symbol>
|
||||
|
||||
<symbol id="right_side_panel" viewBox="0 0 16 16" width="16" height="16" fill="none">
|
||||
@ -709,7 +711,7 @@
|
||||
</symbol>
|
||||
|
||||
<symbol id="scissors" viewBox="0 0 16 16" width="16" height="16" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7699 12.8559C14.5352 13.2977 15.5137 13.0355 15.9555 12.2703C16.066 12.0789 16.0005 11.8343 15.8091 11.7238L15.4786 11.533L10.3136 8.55098L8.31356 9.70568L13.4394 12.6651L13.444 12.6677L13.7699 12.8559ZM5.76183 10.0243L4.9631 10.4855C5.07499 10.6039 5.17484 10.7358 5.26034 10.8807C5.96159 12.0689 5.43125 13.6806 4.0758 14.4806C2.72034 15.2805 1.05304 14.9658 0.351788 13.7776C-0.349466 12.5894 0.180871 10.9777 1.53633 10.1777C1.55931 10.1641 1.58239 10.1509 1.60554 10.138L1.57329 10.1332L2.86875 9.38524L5.31365 7.97368L2.82655 6.53775L1.53108 5.7898L1.56334 5.78501C1.54018 5.77209 1.51711 5.75884 1.49412 5.74528C0.138666 4.94531 -0.391672 3.33358 0.309582 2.14538C1.01084 0.957185 2.67813 0.642461 4.03359 1.44243C5.38905 2.2424 5.91938 3.85412 5.21813 5.04232C5.13264 5.18718 5.03279 5.31906 4.92089 5.43751L7.31365 6.81897L13.4816 3.25791L13.4862 3.25527L13.8121 3.0671C14.5774 2.62527 15.5559 2.88747 15.9978 3.65274C16.1082 3.84406 16.0427 4.08869 15.8513 4.19915L15.5208 4.38996L11.9577 6.44716L11.9576 6.44708L5.76178 10.0243L5.76183 10.0243ZM3.92633 4.27992C4.1153 3.95974 4.07828 3.21056 3.27119 2.73423C2.4641 2.2579 1.79035 2.5876 1.60138 2.90778C1.41242 3.22796 1.44943 3.97715 2.25652 4.45348C3.06361 4.92981 3.73736 4.60011 3.92633 4.27992ZM3.96854 11.6431C4.1575 11.9632 4.12049 12.7124 3.3134 13.1888C2.50631 13.6651 1.83255 13.3354 1.64359 13.0152C1.45462 12.695 1.49164 11.9458 2.29873 11.4695C3.10582 10.9932 3.77957 11.3229 3.96854 11.6431Z" fill="currentColor"></path>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7699 12.8559C14.5352 13.2977 15.5137 13.0355 15.9555 12.2703C16.066 12.0789 16.0004 11.8343 15.8091 11.7238L15.4786 11.533L10.3136 8.55098L8.31355 9.70568L13.4394 12.6651L13.444 12.6677L13.7699 12.8559ZM5.76182 10.0243L4.96309 10.4855C5.07499 10.6039 5.17484 10.7358 5.26033 10.8807C5.96158 12.0689 5.43125 13.6806 4.07579 14.4806C2.72033 15.2805 1.05303 14.9658 0.35178 13.7776C-0.349474 12.5894 0.180864 10.9777 1.53632 10.1777C1.5593 10.1641 1.58238 10.1509 1.60553 10.138L1.57328 10.1332L2.86874 9.38524L5.31364 7.97368L2.82654 6.53775L1.53107 5.7898L1.56333 5.78501C1.54017 5.77208 1.5171 5.75884 1.49412 5.74528C0.138658 4.94531 -0.391679 3.33358 0.309574 2.14538C1.01083 0.957183 2.67812 0.642459 4.03358 1.44243C5.38904 2.24239 5.91938 3.85412 5.21812 5.04232C5.13263 5.18718 5.03278 5.31905 4.92089 5.43751L7.31365 6.81897L13.4816 3.25791L13.4862 3.25527L13.8121 3.0671C14.5774 2.62527 15.5559 2.88747 15.9977 3.65274C16.1082 3.84405 16.0427 4.08869 15.8513 4.19915L15.5208 4.38996L11.9577 6.44716L11.9576 6.44708L5.76177 10.0243L5.76182 10.0243ZM3.92632 4.27992C4.11529 3.95974 4.07827 3.21056 3.27118 2.73423C2.46409 2.2579 1.79034 2.5876 1.60138 2.90778C1.41241 3.22796 1.44942 3.97714 2.25651 4.45348C3.0636 4.92981 3.73736 4.6001 3.92632 4.27992ZM3.96853 11.6431C4.15749 11.9632 4.12048 12.7124 3.31339 13.1888C2.5063 13.6651 1.83255 13.3354 1.64358 13.0152C1.45462 12.695 1.49163 11.9458 2.29872 11.4695C3.10581 10.9932 3.77956 11.3229 3.96853 11.6431Z" fill="currentColor"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol id="select_column" viewBox="0 0 16 16" width="16" height="16" fill="none">
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
@ -3,14 +3,14 @@ import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import ToggleIcon from '@/components/ToggleIcon.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isOutputContextEnabledGlobally: boolean
|
||||
isOutputContextOverridden: boolean
|
||||
isRecordingEnabledGlobally: boolean
|
||||
isRecordingOverridden: boolean
|
||||
isDocsVisible: boolean
|
||||
isVisualizationVisible: boolean
|
||||
isFullMenuVisible: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
'update:isOutputContextOverridden': [isOutputContextOverridden: boolean]
|
||||
'update:isRecordingOverridden': [isRecordingOverridden: boolean]
|
||||
'update:isDocsVisible': [isDocsVisible: boolean]
|
||||
'update:isVisualizationVisible': [isVisualizationVisible: boolean]
|
||||
startEditing: []
|
||||
@ -56,16 +56,12 @@ const emit = defineEmits<{
|
||||
@click.stop="emit('startEditing')"
|
||||
/>
|
||||
<ToggleIcon
|
||||
:icon="props.isOutputContextEnabledGlobally ? 'no_auto_replay' : 'auto_replay'"
|
||||
icon="record"
|
||||
class="icon-container button slot7"
|
||||
:class="{ 'output-context-overridden': props.isOutputContextOverridden }"
|
||||
:alt="`${
|
||||
props.isOutputContextEnabledGlobally != props.isOutputContextOverridden ?
|
||||
'Disable'
|
||||
: 'Enable'
|
||||
} output context`"
|
||||
:modelValue="props.isOutputContextOverridden"
|
||||
@update:modelValue="emit('update:isOutputContextOverridden', $event)"
|
||||
:class="{ 'recording-overridden': props.isRecordingOverridden }"
|
||||
:alt="`${props.isRecordingOverridden ? 'Disable' : 'Enable'} recording`"
|
||||
:modelValue="props.isRecordingOverridden"
|
||||
@update:modelValue="emit('update:isRecordingOverridden', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -155,7 +151,7 @@ const emit = defineEmits<{
|
||||
opacity: 10%;
|
||||
}
|
||||
|
||||
.output-context-overridden {
|
||||
.recording-overridden {
|
||||
opacity: 100%;
|
||||
color: red;
|
||||
}
|
||||
@ -211,7 +207,7 @@ const emit = defineEmits<{
|
||||
.slot7 {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
left: 9px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.slot8 {
|
||||
|
@ -113,7 +113,7 @@ watchEffect(() => {
|
||||
const astSpan = ast.span()
|
||||
let foundNode: NodeId | undefined
|
||||
for (const [id, node] of graphStore.db.nodeIdToNode.entries()) {
|
||||
const rootSpan = graphStore.moduleSource.getSpan(node.rootSpan.id)
|
||||
const rootSpan = graphStore.moduleSource.getSpan(node.rootExpr.id)
|
||||
if (rootSpan && rangeEncloses(rootSpan, astSpan)) {
|
||||
foundNode = id
|
||||
break
|
||||
|
@ -456,7 +456,7 @@ export function useComponentBrowserInput(
|
||||
}
|
||||
break
|
||||
case 'editNode':
|
||||
code.value = graphDb.nodeIdToNode.get(usage.node)?.rootSpan.code() ?? ''
|
||||
code.value = graphDb.nodeIdToNode.get(usage.node)?.innerExpr.code() ?? ''
|
||||
selection.value = { start: usage.cursorPos, end: usage.cursorPos }
|
||||
break
|
||||
}
|
||||
|
@ -495,8 +495,8 @@ function copyNodeContent() {
|
||||
const id = nodeSelection.selected.values().next().value
|
||||
const node = graphStore.db.nodeIdToNode.get(id)
|
||||
if (!node) return
|
||||
const content = node.rootSpan.code()
|
||||
const nodeMetadata = node.rootSpan.nodeMetadata
|
||||
const content = node.innerExpr.code()
|
||||
const nodeMetadata = node.rootExpr.nodeMetadata
|
||||
const metadata = {
|
||||
position: nodeMetadata.get('position'),
|
||||
visualization: nodeMetadata.get('visualization'),
|
||||
|
@ -16,7 +16,7 @@ import { asNodeId } from '@/stores/graph/graphDatabase'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { Ast } from '@/util/ast'
|
||||
import type { AstId } from '@/util/ast/abstract'
|
||||
import { Prefixes } from '@/util/ast/prefixes'
|
||||
import { prefixes } from '@/util/ast/node'
|
||||
import type { Opt } from '@/util/data/opt'
|
||||
import { Rect } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
@ -28,16 +28,6 @@ import { computed, onUnmounted, ref, watch, watchEffect } from 'vue'
|
||||
const MAXIMUM_CLICK_LENGTH_MS = 300
|
||||
const MAXIMUM_CLICK_DISTANCE_SQ = 50
|
||||
|
||||
const prefixes = Prefixes.FromLines({
|
||||
enableOutputContext:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
disableOutputContext:
|
||||
'Standard.Base.Runtime.with_disabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
// Currently unused; included as PoC.
|
||||
skip: 'SKIP __',
|
||||
freeze: 'FREEZE __',
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
node: Node
|
||||
edited: boolean
|
||||
@ -71,12 +61,11 @@ const outputPortsSet = computed(() => {
|
||||
return bindings
|
||||
})
|
||||
|
||||
const nodeId = computed(() => asNodeId(props.node.rootSpan.id))
|
||||
const externalId = computed(() => props.node.rootSpan.externalId)
|
||||
const nodeId = computed(() => asNodeId(props.node.rootExpr.id))
|
||||
const potentialSelfArgumentId = computed(() => props.node.primarySubject)
|
||||
const connectedSelfArgumentId = computed(() =>
|
||||
props.node.primarySubject && graph.isConnectedTarget(props.node.primarySubject) ?
|
||||
props.node.primarySubject
|
||||
potentialSelfArgumentId.value && graph.isConnectedTarget(potentialSelfArgumentId.value) ?
|
||||
potentialSelfArgumentId.value
|
||||
: undefined,
|
||||
)
|
||||
|
||||
@ -118,9 +107,11 @@ const isOnlyOneSelected = computed(
|
||||
|
||||
const menuVisible = isOnlyOneSelected
|
||||
const menuFull = ref(false)
|
||||
|
||||
watch(menuVisible, (visible) => {
|
||||
if (!visible) menuFull.value = false
|
||||
})
|
||||
|
||||
function openFullMenu() {
|
||||
menuFull.value = true
|
||||
nodeSelection?.setSelection(new Set([nodeId.value]))
|
||||
@ -139,15 +130,16 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
const bgStyleVariables = computed(() => {
|
||||
const { x: width, y: height } = nodeSize.value
|
||||
return {
|
||||
'--node-width': `${nodeSize.value.x}px`,
|
||||
'--node-height': `${nodeSize.value.y}px`,
|
||||
'--node-width': `${width}px`,
|
||||
'--node-height': `${height}px`,
|
||||
}
|
||||
})
|
||||
|
||||
const transform = computed(() => {
|
||||
let pos = props.node.position
|
||||
return `translate(${pos.x}px, ${pos.y}px)`
|
||||
const { x, y } = props.node.position
|
||||
return `translate(${x}px, ${y}px)`
|
||||
})
|
||||
|
||||
const startEpochMs = ref(0)
|
||||
@ -186,62 +178,30 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
}
|
||||
})
|
||||
|
||||
const matches = computed(() => prefixes.extractMatches(props.node.rootSpan))
|
||||
const displayedExpression = computed(() => props.node.rootSpan.module.get(matches.value.innerExpr))
|
||||
|
||||
const isOutputContextOverridden = computed({
|
||||
const isRecordingOverridden = computed({
|
||||
get() {
|
||||
const override =
|
||||
matches.value.matches.enableOutputContext ?? matches.value.matches.disableOutputContext
|
||||
const overrideEnabled = matches.value.matches.enableOutputContext != null
|
||||
// An override is only counted as enabled if it is currently in effect. This requires:
|
||||
// - that an override exists
|
||||
if (!override) return false
|
||||
// - that it is setting the "enabled" value to a non-default value
|
||||
else if (overrideEnabled === projectStore.isOutputContextEnabled) return false
|
||||
// - and that it applies to the current execution context.
|
||||
else {
|
||||
const module = props.node.rootSpan.module
|
||||
const contextWithoutQuotes = module
|
||||
.get(override[0])
|
||||
?.code()
|
||||
.replace(/^['"]|['"]$/g, '')
|
||||
return contextWithoutQuotes === projectStore.executionMode
|
||||
}
|
||||
return props.node.prefixes.enableRecording != null
|
||||
},
|
||||
set(shouldOverride) {
|
||||
const module = projectStore.module
|
||||
if (!module) return
|
||||
const edit = props.node.rootSpan.module.edit()
|
||||
const replacementText =
|
||||
shouldOverride ? [Ast.TextLiteral.new(projectStore.executionMode, edit)] : undefined
|
||||
const replacements =
|
||||
projectStore.isOutputContextEnabled ?
|
||||
{
|
||||
enableOutputContext: undefined,
|
||||
disableOutputContext: replacementText,
|
||||
}
|
||||
: {
|
||||
enableOutputContext: replacementText,
|
||||
disableOutputContext: undefined,
|
||||
}
|
||||
prefixes.modify(edit.getVersion(props.node.rootSpan), replacements)
|
||||
const edit = props.node.rootExpr.module.edit()
|
||||
const replacement =
|
||||
shouldOverride && !projectStore.isRecordingEnabled ?
|
||||
[Ast.TextLiteral.new(projectStore.executionMode, edit)]
|
||||
: undefined
|
||||
prefixes.modify(edit.getVersion(props.node.rootExpr), { enableRecording: replacement })
|
||||
graph.commitEdit(edit)
|
||||
},
|
||||
})
|
||||
|
||||
// FIXME [sb]: https://github.com/enso-org/enso/issues/8442
|
||||
// This does not take into account `displayedExpression`.
|
||||
const expressionInfo = computed(() => graph.db.getExpressionInfo(externalId.value))
|
||||
const expressionInfo = computed(() => graph.db.getExpressionInfo(props.node.innerExpr.externalId))
|
||||
const outputPortLabel = computed(() => expressionInfo.value?.typename ?? 'Unknown')
|
||||
const executionState = computed(() => expressionInfo.value?.payload.type ?? 'Unknown')
|
||||
const suggestionEntry = computed(() => graph.db.nodeMainSuggestion.lookup(nodeId.value))
|
||||
const color = computed(() => graph.db.getNodeColorStyle(nodeId.value))
|
||||
const icon = computed(() => {
|
||||
const expressionInfo = graph.db.getExpressionInfo(externalId.value)
|
||||
return displayedIconOf(
|
||||
suggestionEntry.value,
|
||||
expressionInfo?.methodCall?.methodPointer,
|
||||
expressionInfo.value?.methodCall?.methodPointer,
|
||||
outputPortLabel.value,
|
||||
)
|
||||
})
|
||||
@ -259,7 +219,7 @@ const nodeEditHandler = nodeEditBindings.handler({
|
||||
})
|
||||
|
||||
function startEditingNode(position: Vec2 | undefined) {
|
||||
let sourceOffset = props.node.rootSpan.code().length
|
||||
let sourceOffset = props.node.rootExpr.code().length
|
||||
if (position != null) {
|
||||
let domNode, domOffset
|
||||
if ((document as any).caretPositionFromPoint) {
|
||||
@ -404,11 +364,18 @@ const documentation = computed<string | undefined>({
|
||||
<div class="binding" @pointerdown.stop>
|
||||
{{ node.pattern?.code() ?? '' }}
|
||||
</div>
|
||||
<button
|
||||
v-if="!menuVisible && isRecordingOverridden"
|
||||
class="overrideRecordButton"
|
||||
@click="isRecordingOverridden = false"
|
||||
>
|
||||
<SvgIcon name="record" />
|
||||
</button>
|
||||
<CircularMenu
|
||||
v-if="menuVisible"
|
||||
v-model:isOutputContextOverridden="isOutputContextOverridden"
|
||||
v-model:isRecordingOverridden="isRecordingOverridden"
|
||||
v-model:isDocsVisible="isDocsVisible"
|
||||
:isOutputContextEnabledGlobally="projectStore.isOutputContextEnabled"
|
||||
:isRecordingEnabledGlobally="projectStore.isRecordingEnabled"
|
||||
:isVisualizationVisible="isVisualizationVisible"
|
||||
:isFullMenuVisible="menuVisible && menuFull"
|
||||
@update:isVisualizationVisible="emit('update:visualizationVisible', $event)"
|
||||
@ -423,9 +390,9 @@ const documentation = computed<string | undefined>({
|
||||
:scale="navigator?.scale ?? 1"
|
||||
:nodePosition="props.node.position"
|
||||
:isCircularMenuVisible="menuVisible"
|
||||
:currentType="node.vis?.identifier"
|
||||
:currentType="props.node.vis?.identifier"
|
||||
:isFullscreen="isVisualizationFullscreen"
|
||||
:dataSource="{ type: 'node', nodeId: externalId }"
|
||||
:dataSource="{ type: 'node', nodeId: props.node.rootExpr.externalId }"
|
||||
:typename="expressionInfo?.typename"
|
||||
:width="visualizationWidth"
|
||||
:isFocused="isOnlyOneSelected"
|
||||
@ -452,7 +419,7 @@ const documentation = computed<string | undefined>({
|
||||
@pointerup.stop
|
||||
>
|
||||
<NodeWidgetTree
|
||||
:ast="displayedExpression"
|
||||
:ast="props.node.innerExpr"
|
||||
:nodeId="nodeId"
|
||||
:icon="icon"
|
||||
:connectedSelfArgumentId="connectedSelfArgumentId"
|
||||
@ -729,4 +696,19 @@ const documentation = computed<string | undefined>({
|
||||
.GraphNode:has(.selection:hover) .statuses {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.overrideRecordButton {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
background: var(--color-app-bg);
|
||||
border-radius: var(--radius-full);
|
||||
color: red;
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
right: 100%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
@ -2,12 +2,14 @@ import { prepareCollapsedInfo } from '@/components/GraphEditor/collapsing'
|
||||
import { GraphDb, type NodeId } from '@/stores/graph/graphDatabase'
|
||||
import { assert } from '@/util/assert'
|
||||
import { Ast, RawAst } from '@/util/ast'
|
||||
import { initializePrefixes } from '@/util/ast/node'
|
||||
import { unwrap } from '@/util/data/result'
|
||||
import { tryIdentifier } from '@/util/qualifiedName'
|
||||
import { initializeFFI } from 'shared/ast/ffi'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
await initializeFFI()
|
||||
initializePrefixes()
|
||||
|
||||
function setupGraphDb(code: string, graphDb: GraphDb) {
|
||||
const { root, toRaw, getSpan } = Ast.parseExtended(code)
|
||||
@ -121,7 +123,7 @@ test.each(testCases)('Collapsing nodes, $description', (testCase) => {
|
||||
const nodePatternToId = new Map<string, NodeId>()
|
||||
for (const code of testCase.initialNodes) {
|
||||
const [pattern, expr] = code.split(/\s*=\s*/)
|
||||
const [id, _] = nodes.find(([_id, node]) => node.rootSpan.code() == expr)!
|
||||
const [id, _] = nodes.find(([_id, node]) => node.innerExpr.code() == expr)!
|
||||
nodeCodeToId.set(code, id)
|
||||
if (pattern != null) nodePatternToId.set(pattern, id)
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ export function performCollapse(
|
||||
|
||||
// Insert a new function.
|
||||
const collapsedNodeIds = collapsed
|
||||
.map((ast) => asNodeId(nodeFromAst(ast)?.rootSpan.id ?? ast.id))
|
||||
.map((ast) => asNodeId(nodeFromAst(ast)?.rootExpr.id ?? ast.id))
|
||||
.reverse()
|
||||
let outputNodeId: NodeId | undefined
|
||||
const outputIdentifier = info.extracted.output?.identifier
|
||||
|
@ -8,7 +8,7 @@ const emit = defineEmits<{ recordOnce: []; 'update:recordMode': [enabled: boolea
|
||||
|
||||
<template>
|
||||
<div class="RecordControl" @pointerdown.stop @pointerup.stop @click.stop>
|
||||
<div class="control left-end">
|
||||
<div class="control left-end" @click.stop="() => emit('update:recordMode', !props.recordMode)">
|
||||
<ToggleIcon
|
||||
icon="record"
|
||||
class="button"
|
||||
@ -17,14 +17,8 @@ const emit = defineEmits<{ recordOnce: []; 'update:recordMode': [enabled: boolea
|
||||
@update:modelValue="emit('update:recordMode', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="control right-end">
|
||||
<SvgIcon
|
||||
alt="Record once"
|
||||
class="button"
|
||||
name="record_once"
|
||||
draggable="false"
|
||||
@click.stop="() => emit('recordOnce')"
|
||||
/>
|
||||
<div class="control right-end" @click.stop="() => emit('recordOnce')">
|
||||
<SvgIcon alt="Record once" class="button" name="record_once" draggable="false" :scale="1.5" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -41,14 +35,26 @@ const emit = defineEmits<{ recordOnce: []; 'update:recordMode': [enabled: boolea
|
||||
background: var(--color-frame-bg);
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
padding: 8px 8px;
|
||||
width: 42px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left-end {
|
||||
border-radius: var(--radius-full) 0 0 var(--radius-full);
|
||||
|
||||
.button {
|
||||
margin: 0 4px 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.right-end {
|
||||
border-radius: 0 var(--radius-full) var(--radius-full) 0;
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toggledOn {
|
||||
|
@ -8,11 +8,22 @@ import icons from '@/assets/icons.svg'
|
||||
import type { URLString } from '@/util/data/urlString'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
|
||||
const props = defineProps<{ name: Icon | URLString; width?: number; height?: number }>()
|
||||
const props = defineProps<{
|
||||
name: Icon | URLString
|
||||
width?: number
|
||||
height?: number
|
||||
scale?: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg :style="{ '--width': `${width ?? 16}px`, '--height': `${height ?? 16}px` }">
|
||||
<svg
|
||||
:style="{
|
||||
'--width': `${props.width ?? 16}px`,
|
||||
'--height': `${props.height ?? 16}px`,
|
||||
'--scale': props.scale ?? 1,
|
||||
}"
|
||||
>
|
||||
<use :href="props.name.includes(':') ? props.name : `${icons}#${props.name}`"></use>
|
||||
</svg>
|
||||
</template>
|
||||
@ -23,5 +34,7 @@ svg {
|
||||
min-width: var(--width);
|
||||
height: var(--height);
|
||||
min-height: var(--height);
|
||||
transform: scale(var(--scale));
|
||||
transform-origin: top left;
|
||||
}
|
||||
</style>
|
||||
|
@ -105,7 +105,9 @@ class MockPointerEvent extends MouseEvent {
|
||||
readonly pointerId: number
|
||||
constructor(type: string, options: MouseEventInit & { currentTarget?: Element | undefined }) {
|
||||
super(type, options)
|
||||
vi.spyOn(this, 'currentTarget', 'get').mockReturnValue(options.currentTarget ?? null)
|
||||
vi.spyOn<MouseEvent, 'currentTarget'>(this, 'currentTarget', 'get').mockReturnValue(
|
||||
options.currentTarget ?? null,
|
||||
)
|
||||
this.pointerId = 4
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { initializePrefixes } from '@/util/ast/node'
|
||||
import { baseConfig, configValue, mergeConfig } from '@/util/config'
|
||||
import { urlParams } from '@/util/urlParams'
|
||||
import { isOnLinux } from 'enso-common/src/detect'
|
||||
@ -52,6 +53,7 @@ export interface StringConfig {
|
||||
|
||||
async function runApp(config: StringConfig | null, accessToken: string | null, metadata?: object) {
|
||||
await initializeFFI()
|
||||
initializePrefixes()
|
||||
running = true
|
||||
const { mountProjectApp } = await import('./createApp')
|
||||
if (!running) return
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { asNodeId, GraphDb } from '@/stores/graph/graphDatabase'
|
||||
import { Ast, RawAst } from '@/util/ast'
|
||||
import { initializePrefixes } from '@/util/ast/node'
|
||||
import assert from 'assert'
|
||||
import type { AstId } from 'shared/ast'
|
||||
import { initializeFFI } from 'shared/ast/ffi'
|
||||
import { IdMap, type ExternalId } from 'shared/yjsModel'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
await initializeFFI()
|
||||
initializePrefixes()
|
||||
|
||||
/**
|
||||
* Create a predictable fake UUID which contains given number in decimal at the end.
|
||||
@ -46,8 +49,10 @@ test('Reading graph from definition', () => {
|
||||
assert(rawFunc?.type === RawAst.Tree.Type.Function)
|
||||
db.readFunctionAst(func, rawFunc, code, getSpan, new Set())
|
||||
|
||||
const idFromExternal = new Map()
|
||||
ast.visitRecursiveAst((ast) => idFromExternal.set(ast.externalId, ast.id))
|
||||
const idFromExternal = new Map<ExternalId, AstId>()
|
||||
ast.visitRecursiveAst((ast) => {
|
||||
idFromExternal.set(ast.externalId, ast.id)
|
||||
})
|
||||
const id = (x: number) => idFromExternal.get(eid(x))!
|
||||
|
||||
expect(Array.from(db.nodeIdToNode.keys())).toEqual([id(4), id(8), id(12)])
|
||||
|
@ -140,7 +140,7 @@ export class GraphDb {
|
||||
|
||||
private nodeIdToExprIds = new ReactiveIndex(this.nodeIdToNode, (id, entry) => {
|
||||
const exprs: AstId[] = []
|
||||
entry.rootSpan.visitRecursiveAst((ast) => void exprs.push(ast.id))
|
||||
entry.innerExpr.visitRecursiveAst((ast) => void exprs.push(ast.id))
|
||||
return Array.from(exprs, (expr) => [id, expr])
|
||||
})
|
||||
|
||||
@ -187,8 +187,8 @@ export class GraphDb {
|
||||
return Array.from(ports, (port) => [id, port])
|
||||
})
|
||||
|
||||
nodeMainSuggestion = new ReactiveMapping(this.nodeIdToNode, (id, _entry) => {
|
||||
const expressionInfo = this.getExpressionInfo(id)
|
||||
nodeMainSuggestion = new ReactiveMapping(this.nodeIdToNode, (id, entry) => {
|
||||
const expressionInfo = this.getExpressionInfo(entry.innerExpr.id)
|
||||
const method = expressionInfo?.methodCall?.methodPointer
|
||||
if (method == null) return
|
||||
const suggestionId = this.suggestionDb.findByMethodPointer(method)
|
||||
@ -340,7 +340,7 @@ export class GraphDb {
|
||||
for (const nodeAst of functionAst_.bodyExpressions()) {
|
||||
const newNode = nodeFromAst(nodeAst)
|
||||
if (!newNode) continue
|
||||
const nodeId = asNodeId(newNode.rootSpan.id)
|
||||
const nodeId = asNodeId(newNode.rootExpr.id)
|
||||
const node = this.nodeIdToNode.get(nodeId)
|
||||
currentNodeIds.add(nodeId)
|
||||
if (node == null) {
|
||||
@ -351,7 +351,7 @@ export class GraphDb {
|
||||
// We are notified of new or changed metadata by `updateMetadata`, so we only need to read existing metadata
|
||||
// when we switch to a different function.
|
||||
if (functionChanged) {
|
||||
const nodeMeta = newNode.rootSpan.nodeMetadata
|
||||
const nodeMeta = newNode.rootExpr.nodeMetadata
|
||||
const pos = nodeMeta.get('position') ?? { x: 0, y: 0 }
|
||||
metadataFields = {
|
||||
position: new Vec2(pos.x, pos.y),
|
||||
@ -360,20 +360,37 @@ export class GraphDb {
|
||||
}
|
||||
this.nodeIdToNode.set(nodeId, { ...newNode, ...metadataFields })
|
||||
} else {
|
||||
const { outerExprId, pattern, rootSpan, primarySubject, documentation } = newNode
|
||||
const {
|
||||
outerExprId,
|
||||
pattern,
|
||||
rootExpr,
|
||||
innerExpr,
|
||||
primarySubject,
|
||||
prefixes,
|
||||
documentation,
|
||||
} = newNode
|
||||
const differentOrDirty = (a: Ast.Ast | undefined, b: Ast.Ast | undefined) =>
|
||||
a?.id !== b?.id || (a && subtreeDirty(a.id))
|
||||
if (differentOrDirty(node.pattern, pattern)) node.pattern = pattern
|
||||
if (differentOrDirty(node.rootSpan, rootSpan)) node.rootSpan = rootSpan
|
||||
if (node.outerExprId !== outerExprId) node.outerExprId = outerExprId
|
||||
if (differentOrDirty(node.pattern, pattern)) node.pattern = pattern
|
||||
if (differentOrDirty(node.rootExpr, rootExpr)) node.rootExpr = rootExpr
|
||||
if (differentOrDirty(node.innerExpr, innerExpr)) node.innerExpr = innerExpr
|
||||
if (node.primarySubject !== primarySubject) node.primarySubject = primarySubject
|
||||
if (node.documentation !== documentation) node.documentation = documentation
|
||||
if (
|
||||
Object.entries(node.prefixes).some(
|
||||
([k, v]) => prefixes[k as keyof typeof node.prefixes] !== v,
|
||||
)
|
||||
)
|
||||
node.prefixes = prefixes
|
||||
// Ensure new fields can't be added to `NodeAstData` without this code being updated.
|
||||
const _allFieldsHandled = {
|
||||
outerExprId,
|
||||
pattern,
|
||||
rootSpan,
|
||||
rootExpr,
|
||||
innerExpr,
|
||||
primarySubject,
|
||||
prefixes,
|
||||
documentation,
|
||||
} satisfies NodeDataFromAst
|
||||
}
|
||||
@ -394,7 +411,7 @@ export class GraphDb {
|
||||
idToExternalNew.set(ast.id, ast.externalId)
|
||||
idFromExternalNew.set(ast.externalId, ast.id)
|
||||
})
|
||||
const updateMap = (map: Map<any, any>, newMap: Map<any, any>) => {
|
||||
const updateMap = <K, V>(map: Map<K, V>, newMap: Map<K, V>) => {
|
||||
for (const key of map.keys()) if (!newMap.has(key)) map.delete(key)
|
||||
for (const [key, value] of newMap) map.set(key, value)
|
||||
}
|
||||
@ -446,7 +463,8 @@ export class GraphDb {
|
||||
...baseMockNode,
|
||||
outerExprId: id,
|
||||
pattern,
|
||||
rootSpan: Ast.parse(code ?? '0'),
|
||||
rootExpr: Ast.parse(code ?? '0'),
|
||||
innerExpr: Ast.parse(code ?? '0'),
|
||||
}
|
||||
const bindingId = pattern.id
|
||||
this.nodeIdToNode.set(asNodeId(id), node)
|
||||
@ -462,9 +480,19 @@ export function asNodeId(id: Ast.AstId): NodeId {
|
||||
}
|
||||
|
||||
export interface NodeDataFromAst {
|
||||
/** The ID of the outer expression. Usually this is an assignment expression (`a = b`). */
|
||||
outerExprId: Ast.AstId
|
||||
/** The left side of the assignment experssion, if `outerExpr` is an assignment expression. */
|
||||
pattern: Ast.Ast | undefined
|
||||
rootSpan: Ast.Ast
|
||||
/** The value of the node. The right side of the assignment, if `outerExpr` is an assignment
|
||||
* expression, else the entire `outerExpr`. */
|
||||
rootExpr: Ast.Ast
|
||||
/** The expression displayed by the node. This is `rootExpr`, minus the prefixes, which are in
|
||||
* `prefixes`. */
|
||||
innerExpr: Ast.Ast
|
||||
/** Prefixes that are present in `rootExpr` but omitted in `innerExpr` to ensure a clean output.
|
||||
*/
|
||||
prefixes: Record<'enableRecording', Ast.AstId[] | undefined>
|
||||
/** A child AST in a syntactic position to be a self-argument input to the node. */
|
||||
primarySubject: Ast.AstId | undefined
|
||||
documentation: string | undefined
|
||||
@ -480,9 +508,10 @@ export interface Node extends NodeDataFromAst, NodeDataFromMetadata {}
|
||||
const baseMockNode = {
|
||||
position: Vec2.Zero,
|
||||
vis: undefined,
|
||||
prefixes: { enableRecording: undefined },
|
||||
primarySubject: undefined,
|
||||
documentation: undefined,
|
||||
}
|
||||
} satisfies Partial<Node>
|
||||
|
||||
/** This should only be used for supplying as initial props when testing.
|
||||
* Please do {@link GraphDb.mockNode} with a `useGraphStore().db` after mount. */
|
||||
@ -491,7 +520,8 @@ export function mockNode(exprId?: Ast.AstId): Node {
|
||||
...baseMockNode,
|
||||
outerExprId: exprId ?? (random.uuidv4() as Ast.AstId),
|
||||
pattern: undefined,
|
||||
rootSpan: Ast.parse('0'),
|
||||
rootExpr: Ast.parse('0'),
|
||||
innerExpr: Ast.parse('0'),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,11 +326,11 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
const node = db.nodeIdToNode.get(id)
|
||||
if (!node) return
|
||||
edit((edit) => {
|
||||
edit.getVersion(node.rootSpan).syncToCode(content)
|
||||
edit.getVersion(node.rootExpr).syncToCode(content)
|
||||
if (withImports) {
|
||||
const conflicts = addMissingImports(edit, withImports)
|
||||
if (conflicts == null) return
|
||||
const wholeAssignment = edit.getVersion(node.rootSpan)?.mutableParent()
|
||||
const wholeAssignment = edit.getVersion(node.rootExpr)?.mutableParent()
|
||||
if (wholeAssignment == null) {
|
||||
console.error('Cannot find parent of the node expression. Conflict resolution failed.')
|
||||
return
|
||||
@ -558,7 +558,7 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
let exprId: AstId | undefined
|
||||
if (expr) {
|
||||
const node = db.nodeIdToNode.get(nodeId)
|
||||
node?.rootSpan.visitRecursive((ast) => {
|
||||
node?.innerExpr.visitRecursive((ast) => {
|
||||
if (ast instanceof Ast.Ast && ast.code() == expr) {
|
||||
exprId = ast.id
|
||||
}
|
||||
|
@ -637,7 +637,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const isOutputContextEnabled = computed(() => executionMode.value === 'live')
|
||||
const isRecordingEnabled = computed(() => executionMode.value === 'live')
|
||||
|
||||
function stopCapturingUndo() {
|
||||
module.value?.undoManager.stopCapturing()
|
||||
@ -713,7 +713,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||
lsRpcConnection: markRaw(lsRpcConnection),
|
||||
dataConnection: markRaw(dataConnection),
|
||||
useVisualizationData,
|
||||
isOutputContextEnabled,
|
||||
isRecordingEnabled,
|
||||
stopCapturingUndo,
|
||||
executionMode,
|
||||
recordMode,
|
||||
|
@ -1,22 +1,24 @@
|
||||
import { Ast } from '@/util/ast'
|
||||
import { nodeFromAst } from '@/util/ast/node'
|
||||
import { initializePrefixes, nodeFromAst } from '@/util/ast/node'
|
||||
import { initializeFFI } from 'shared/ast/ffi'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
await initializeFFI()
|
||||
initializePrefixes()
|
||||
|
||||
test.each`
|
||||
line | pattern | rootSpan | documentation
|
||||
line | pattern | rootExpr | documentation
|
||||
${'2 + 2'} | ${undefined} | ${'2 + 2'} | ${undefined}
|
||||
${'foo = bar'} | ${'foo'} | ${'bar'} | ${undefined}
|
||||
${'## Documentation\n2 + 2'} | ${undefined} | ${'2 + 2'} | ${'Documentation'}
|
||||
${'## Documentation\nfoo = 2 + 2'} | ${'foo'} | ${'2 + 2'} | ${'Documentation'}
|
||||
`('Node information from AST $line line', ({ line, pattern, rootSpan, documentation }) => {
|
||||
`('Node information from AST $line line', ({ line, pattern, rootExpr, documentation }) => {
|
||||
const ast = Ast.Ast.parse(line)
|
||||
const node = nodeFromAst(ast)
|
||||
expect(node?.outerExprId).toBe(ast.id)
|
||||
expect(node?.pattern?.code()).toBe(pattern)
|
||||
expect(node?.rootSpan.code()).toBe(rootSpan)
|
||||
expect(node?.rootExpr.code()).toBe(rootExpr)
|
||||
expect(node?.innerExpr.code()).toBe(rootExpr)
|
||||
expect(node?.documentation).toBe(documentation)
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,20 @@
|
||||
import type { NodeDataFromAst } from '@/stores/graph'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { Prefixes } from '@/util/ast/prefixes'
|
||||
|
||||
export let prefixes!: ReturnType<typeof makePrefixes>
|
||||
|
||||
function makePrefixes() {
|
||||
return Prefixes.FromLines({
|
||||
enableRecording:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
})
|
||||
}
|
||||
|
||||
/** MUST be called after `initializeFFI`. */
|
||||
export function initializePrefixes() {
|
||||
prefixes = makePrefixes()
|
||||
}
|
||||
|
||||
export function nodeFromAst(ast: Ast.Ast): NodeDataFromAst | undefined {
|
||||
const { nodeCode, documentation } =
|
||||
@ -8,12 +23,15 @@ export function nodeFromAst(ast: Ast.Ast): NodeDataFromAst | undefined {
|
||||
: { nodeCode: ast, documentation: undefined }
|
||||
if (!nodeCode) return
|
||||
const pattern = nodeCode instanceof Ast.Assignment ? nodeCode.pattern : undefined
|
||||
const rootSpan = nodeCode instanceof Ast.Assignment ? nodeCode.expression : nodeCode
|
||||
const rootExpr = nodeCode instanceof Ast.Assignment ? nodeCode.expression : nodeCode
|
||||
const { innerExpr, matches } = prefixes.extractMatches(rootExpr)
|
||||
return {
|
||||
outerExprId: ast.id,
|
||||
pattern,
|
||||
rootSpan,
|
||||
primarySubject: primaryApplicationSubject(rootSpan),
|
||||
rootExpr,
|
||||
innerExpr,
|
||||
prefixes: matches,
|
||||
primarySubject: primaryApplicationSubject(innerExpr),
|
||||
documentation,
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { unsafeKeys } from '@/util/record'
|
||||
type Matches<T> = Record<keyof T, Ast.AstId[] | undefined>
|
||||
|
||||
interface MatchResult<T> {
|
||||
innerExpr: Ast.AstId
|
||||
innerExpr: Ast.Ast
|
||||
matches: Record<keyof T, Ast.AstId[] | undefined>
|
||||
}
|
||||
|
||||
@ -33,14 +33,14 @@ export class Prefixes<T extends Record<keyof T, Pattern>> {
|
||||
return [name, matches]
|
||||
}),
|
||||
) as Matches<T>
|
||||
return { matches, innerExpr: expression.id }
|
||||
return { matches, innerExpr: expression }
|
||||
}
|
||||
|
||||
modify(expression: Ast.Mutable, replacements: Partial<Record<keyof T, Ast.Owned[] | undefined>>) {
|
||||
expression.updateValue((expression) => {
|
||||
const matches = this.extractMatches(expression)
|
||||
const edit = expression.module
|
||||
let result = edit.take(matches.innerExpr)
|
||||
let result = edit.take(matches.innerExpr.id)
|
||||
for (const key of unsafeKeys(this.prefixes).reverse()) {
|
||||
if (key in replacements && !replacements[key]) continue
|
||||
const replacement: Ast.Owned[] | undefined =
|
||||
|
@ -8,11 +8,9 @@ import { Rect } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
import { logEvent } from 'histoire/client'
|
||||
import { computed, reactive, ref, watchEffect } from 'vue'
|
||||
import { IdMap, type SourceRange } from '../shared/yjsModel'
|
||||
import { type SourceRange } from '../shared/yjsModel'
|
||||
import { createSetupComponent } from './histoire/utils'
|
||||
|
||||
const idMap = new IdMap()
|
||||
|
||||
const nodeBinding = ref('binding')
|
||||
const nodeContent = ref('content')
|
||||
const nodeX = ref(0)
|
||||
@ -31,23 +29,27 @@ function updateContent(updates: [range: SourceRange, content: string][]) {
|
||||
nodeContent.value = content
|
||||
}
|
||||
|
||||
const rootSpan = computed(() => Ast.parseTransitional(nodeContent.value, idMap))
|
||||
const pattern = computed(() => Ast.parseTransitional(nodeBinding.value, idMap))
|
||||
const innerExpr = computed(() => Ast.parse(nodeContent.value))
|
||||
const pattern = computed(() => Ast.parse(nodeBinding.value))
|
||||
|
||||
const node = computed((): Node => {
|
||||
return {
|
||||
outerExprId: '' as any,
|
||||
pattern: pattern.value,
|
||||
position: position.value,
|
||||
rootSpan: rootSpan.value,
|
||||
prefixes: { enableRecording: undefined },
|
||||
rootExpr: innerExpr.value,
|
||||
innerExpr: innerExpr.value,
|
||||
primarySubject: undefined,
|
||||
vis: undefined,
|
||||
documentation: undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const mockRects = reactive(new Map())
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
const id = node.value.rootSpan.id
|
||||
const id = node.value.innerExpr.id
|
||||
mockRects.set(id, Rect.Zero)
|
||||
onCleanup(() => {
|
||||
mockRects.delete(id)
|
||||
|
Loading…
Reference in New Issue
Block a user