mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 22:01:42 +03:00
Add functionality to handle Expression Updates to GUI2 (#7982)
Implements #7783. Adds functionality to handle and store expression updates, as well as show the output type of node. https://github.com/enso-org/enso/assets/1428930/31ffff78-ff2c-4e0b-bcde-ddc507cc0226
This commit is contained in:
parent
44f2f425c0
commit
10a95e43d6
@ -28,10 +28,10 @@ import {
|
|||||||
import type { WebsocketClient } from './websocket'
|
import type { WebsocketClient } from './websocket'
|
||||||
import type { Uuid } from './yjsModel'
|
import type { Uuid } from './yjsModel'
|
||||||
|
|
||||||
export function uuidFromBits(leastSigBits: bigint, mostSigBits: bigint): string {
|
export function uuidFromBits(leastSigBits: bigint, mostSigBits: bigint): Uuid {
|
||||||
const bits = (mostSigBits << 64n) | leastSigBits
|
const bits = (mostSigBits << 64n) | leastSigBits
|
||||||
const string = bits.toString(16).padStart(32, '0')
|
const string = bits.toString(16).padStart(32, '0')
|
||||||
return string.replace(/(........)(....)(....)(....)(............)/, '$1-$2-$3-$4-$5')
|
return string.replace(/(........)(....)(....)(....)(............)/, '$1-$2-$3-$4-$5') as Uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uuidToBits(uuid: string): [leastSigBits: bigint, mostSigBits: bigint] {
|
export function uuidToBits(uuid: string): [leastSigBits: bigint, mostSigBits: bigint] {
|
||||||
@ -51,8 +51,9 @@ const PAYLOAD_CONSTRUCTOR = {
|
|||||||
} satisfies Record<OutboundPayload, new () => Table>
|
} satisfies Record<OutboundPayload, new () => Table>
|
||||||
|
|
||||||
export type DataServerEvents = {
|
export type DataServerEvents = {
|
||||||
[K in keyof typeof PAYLOAD_CONSTRUCTOR as K | `${K}:${string}`]: (
|
[K in keyof typeof PAYLOAD_CONSTRUCTOR as `${K}`]: (
|
||||||
arg: InstanceType<(typeof PAYLOAD_CONSTRUCTOR)[K]>,
|
payload: InstanceType<(typeof PAYLOAD_CONSTRUCTOR)[K]>,
|
||||||
|
uuid: Uuid | null,
|
||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,18 +85,18 @@ export class DataServer extends ObservableV2<DataServerEvents> {
|
|||||||
const payloadType = binaryMessage.payloadType()
|
const payloadType = binaryMessage.payloadType()
|
||||||
const payload = binaryMessage.payload(new PAYLOAD_CONSTRUCTOR[payloadType]())
|
const payload = binaryMessage.payload(new PAYLOAD_CONSTRUCTOR[payloadType]())
|
||||||
if (payload != null) {
|
if (payload != null) {
|
||||||
this.emit(`${payloadType}`, [payload])
|
this.emit(`${payloadType}`, [payload, null])
|
||||||
const id = binaryMessage.correlationId()
|
const id = binaryMessage.correlationId()
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
const uuid = uuidFromBits(id.leastSigBits(), id.mostSigBits())
|
const uuid = uuidFromBits(id.leastSigBits(), id.mostSigBits())
|
||||||
this.emit(`${payloadType}:${uuid}`, [payload])
|
this.emit(`${payloadType}`, [payload, uuid])
|
||||||
const callback = this.resolveCallbacks.get(uuid)
|
const callback = this.resolveCallbacks.get(uuid)
|
||||||
callback?.(payload)
|
callback?.(payload)
|
||||||
} else if (payload instanceof VisualizationUpdate) {
|
} else if (payload instanceof VisualizationUpdate) {
|
||||||
const id = payload.visualizationContext()?.visualizationId()
|
const id = payload.visualizationContext()?.visualizationId()
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
const uuid = uuidFromBits(id.leastSigBits(), id.mostSigBits())
|
const uuid = uuidFromBits(id.leastSigBits(), id.mostSigBits())
|
||||||
this.emit(`${payloadType}:${uuid}`, [payload])
|
this.emit(`${payloadType}`, [payload, uuid])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,7 +250,7 @@ export class LanguageServer extends ObservableV2<Notifications> {
|
|||||||
expressionId: ExpressionId,
|
expressionId: ExpressionId,
|
||||||
visualizationConfig: VisualizationConfiguration,
|
visualizationConfig: VisualizationConfiguration,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.request('executionContext/interrupt', {
|
return this.request('executionContext/executeExpression', {
|
||||||
visualizationId,
|
visualizationId,
|
||||||
expressionId,
|
expressionId,
|
||||||
visualizationConfig,
|
visualizationConfig,
|
||||||
|
@ -96,6 +96,7 @@ export type ExpressionUpdatePayload = Value | DataflowError | Panic | Pending
|
|||||||
* Indicates that the expression was computed to a value.
|
* Indicates that the expression was computed to a value.
|
||||||
*/
|
*/
|
||||||
export interface Value {
|
export interface Value {
|
||||||
|
type: 'Value'
|
||||||
/**
|
/**
|
||||||
* Information about attached warnings.
|
* Information about attached warnings.
|
||||||
*/
|
*/
|
||||||
@ -111,6 +112,7 @@ export interface Value {
|
|||||||
* Indicates that the expression was computed to an error.
|
* Indicates that the expression was computed to an error.
|
||||||
*/
|
*/
|
||||||
export interface DataflowError {
|
export interface DataflowError {
|
||||||
|
type: 'DataflowError'
|
||||||
/**
|
/**
|
||||||
* The list of expressions leading to the root error.
|
* The list of expressions leading to the root error.
|
||||||
*/
|
*/
|
||||||
@ -121,6 +123,7 @@ export interface DataflowError {
|
|||||||
* Indicates that the expression failed with the runtime exception.
|
* Indicates that the expression failed with the runtime exception.
|
||||||
*/
|
*/
|
||||||
export interface Panic {
|
export interface Panic {
|
||||||
|
type: 'Panic'
|
||||||
/**
|
/**
|
||||||
* The error message.
|
* The error message.
|
||||||
*/
|
*/
|
||||||
@ -137,6 +140,7 @@ export interface Panic {
|
|||||||
* provides description and percentage (`0.0-1.0`) of completeness.
|
* provides description and percentage (`0.0-1.0`) of completeness.
|
||||||
*/
|
*/
|
||||||
export interface Pending {
|
export interface Pending {
|
||||||
|
type: 'Pending'
|
||||||
/** Optional message describing current operation. */
|
/** Optional message describing current operation. */
|
||||||
message?: string
|
message?: string
|
||||||
/** Optional amount of already done work as a number between `0.0` to `1.0`. */
|
/** Optional amount of already done work as a number between `0.0` to `1.0`. */
|
||||||
|
@ -140,7 +140,7 @@ export class DistributedModule {
|
|||||||
return newId
|
return newId
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteNode(id: ExprId): void {
|
deleteExpression(id: ExprId): void {
|
||||||
const rangeBuffer = this.doc.idMap.get(id)
|
const rangeBuffer = this.doc.idMap.get(id)
|
||||||
if (rangeBuffer == null) return
|
if (rangeBuffer == null) return
|
||||||
const [relStart, relEnd] = decodeRange(rangeBuffer)
|
const [relStart, relEnd] = decodeRange(rangeBuffer)
|
||||||
|
@ -12,7 +12,7 @@ export const graphBindings = defineKeybinds('graph-editor', {
|
|||||||
newNode: ['N'],
|
newNode: ['N'],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const nodeBindings = defineKeybinds('node-selection', {
|
export const nodeSelectionBindings = defineKeybinds('node-selection', {
|
||||||
deleteSelected: ['Delete'],
|
deleteSelected: ['Delete'],
|
||||||
selectAll: ['Mod+A'],
|
selectAll: ['Mod+A'],
|
||||||
deselectAll: ['Escape', 'PointerMain'],
|
deselectAll: ['Escape', 'PointerMain'],
|
||||||
@ -23,3 +23,7 @@ export const nodeBindings = defineKeybinds('node-selection', {
|
|||||||
invert: ['Mod+Shift+Alt+PointerMain'],
|
invert: ['Mod+Shift+Alt+PointerMain'],
|
||||||
toggleVisualization: ['Space'],
|
toggleVisualization: ['Space'],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const nodeEditBindings = defineKeybinds('node-edit', {
|
||||||
|
selectAll: ['Mod+A'],
|
||||||
|
})
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { codeEditorBindings, graphBindings, nodeBindings } from '@/bindings'
|
import { codeEditorBindings, graphBindings, nodeSelectionBindings } from '@/bindings'
|
||||||
import CodeEditor from '@/components/CodeEditor.vue'
|
import CodeEditor from '@/components/CodeEditor.vue'
|
||||||
import ComponentBrowser from '@/components/ComponentBrowser.vue'
|
import ComponentBrowser from '@/components/ComponentBrowser.vue'
|
||||||
import GraphEdge from '@/components/GraphEdge.vue'
|
import GraphEdge from '@/components/GraphEdge.vue'
|
||||||
@ -159,6 +159,7 @@ const graphBindingsHandler = graphBindings.handler({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
newNode() {
|
newNode() {
|
||||||
|
if (keyboardBusy()) return false
|
||||||
if (navigator.sceneMousePos != null) {
|
if (navigator.sceneMousePos != null) {
|
||||||
graphStore.createNode(navigator.sceneMousePos, 'hello "world"! 123 + x')
|
graphStore.createNode(navigator.sceneMousePos, 'hello "world"! 123 + x')
|
||||||
}
|
}
|
||||||
@ -174,7 +175,7 @@ const codeEditorHandler = codeEditorBindings.handler({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const nodeSelectionHandler = nodeBindings.handler({
|
const nodeSelectionHandler = nodeSelectionBindings.handler({
|
||||||
deleteSelected() {
|
deleteSelected() {
|
||||||
graphStore.transact(() => {
|
graphStore.transact(() => {
|
||||||
for (const node of selectedNodes.value) {
|
for (const node of selectedNodes.value) {
|
||||||
@ -207,7 +208,7 @@ const nodeSelectionHandler = nodeBindings.handler({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const mouseHandler = nodeBindings.handler({
|
const mouseHandler = nodeSelectionBindings.handler({
|
||||||
replace() {
|
replace() {
|
||||||
selectedNodes.value = new Set(intersectingNodes.value)
|
selectedNodes.value = new Set(intersectingNodes.value)
|
||||||
},
|
},
|
||||||
@ -372,12 +373,4 @@ svg {
|
|||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle {
|
|
||||||
position: absolute;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: purple;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nodeBindings } from '@/bindings'
|
import { nodeEditBindings, nodeSelectionBindings } from '@/bindings'
|
||||||
import CircularMenu from '@/components/CircularMenu.vue'
|
import CircularMenu from '@/components/CircularMenu.vue'
|
||||||
import NodeSpan from '@/components/NodeSpan.vue'
|
import NodeSpan from '@/components/NodeSpan.vue'
|
||||||
import SvgIcon from '@/components/SvgIcon.vue'
|
import SvgIcon from '@/components/SvgIcon.vue'
|
||||||
@ -27,6 +27,7 @@ const MAXIMUM_CLICK_LENGTH_MS = 300
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
node: Node
|
node: Node
|
||||||
|
// id: string & ExprId
|
||||||
selected: boolean
|
selected: boolean
|
||||||
isLatestSelected: boolean
|
isLatestSelected: boolean
|
||||||
fullscreenVis: boolean
|
fullscreenVis: boolean
|
||||||
@ -354,7 +355,7 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const mouseHandler = nodeBindings.handler({
|
const mouseHandler = nodeSelectionBindings.handler({
|
||||||
replace() {
|
replace() {
|
||||||
emit('replaceSelection')
|
emit('replaceSelection')
|
||||||
},
|
},
|
||||||
@ -372,6 +373,18 @@ const mouseHandler = nodeBindings.handler({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const editableKeydownHandler = nodeEditBindings.handler({
|
||||||
|
selectAll() {
|
||||||
|
const element = editableRootNode.value
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (element == null || selection == null) return
|
||||||
|
const range = document.createRange()
|
||||||
|
range.selectNodeContents(element)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const startEpochMs = ref(0)
|
const startEpochMs = ref(0)
|
||||||
const startEvent = ref<PointerEvent>()
|
const startEvent = ref<PointerEvent>()
|
||||||
|
|
||||||
@ -395,6 +408,18 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const expressionInfo = computed(() => {
|
||||||
|
return projectStore.computedValueRegistry.getExpressionInfo(props.node.rootSpan.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
const outputTypeName = computed(() => {
|
||||||
|
return expressionInfo.value?.typename ?? 'Unknown'
|
||||||
|
})
|
||||||
|
|
||||||
|
const executionState = computed(() => {
|
||||||
|
return expressionInfo.value?.payload.type ?? 'Unknown'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -406,6 +431,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
dragging: dragPointer.dragging,
|
dragging: dragPointer.dragging,
|
||||||
selected,
|
selected,
|
||||||
visualizationVisible: isVisualizationVisible,
|
visualizationVisible: isVisualizationVisible,
|
||||||
|
['executionState-' + executionState]: true,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="selection" v-on="dragPointer.events"></div>
|
<div class="selection" v-on="dragPointer.events"></div>
|
||||||
@ -433,6 +459,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
contenteditable
|
contenteditable
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
@beforeinput="editContent"
|
@beforeinput="editContent"
|
||||||
|
@keydown="editableKeydownHandler"
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@blur="projectStore.stopCapturingUndo()"
|
@blur="projectStore.stopCapturingUndo()"
|
||||||
>
|
>
|
||||||
@ -444,6 +471,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="outputTypeName">{{ outputTypeName }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -452,9 +480,19 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
--node-height: 32px;
|
--node-height: 32px;
|
||||||
--node-border-radius: calc(var(--node-height) * 0.5);
|
--node-border-radius: calc(var(--node-height) * 0.5);
|
||||||
|
|
||||||
--node-color-primary: #357ab9;
|
--node-group-color: #357ab9;
|
||||||
|
|
||||||
|
--node-color-primary: color-mix(in oklab, var(--node-group-color) 100%, transparent 0%);
|
||||||
|
--node-color-port: color-mix(in oklab, var(--node-color-primary) 75%, white 15%);
|
||||||
|
--node-color-error: color-mix(in oklab, var(--node-group-color) 30%, rgba(255, 0, 0) 70%);
|
||||||
|
|
||||||
|
&.executionState-Unknown,
|
||||||
|
&.executionState-Pending {
|
||||||
|
--node-color-primary: color-mix(in oklab, var(--node-group-color) 60%, #aaa 40%);
|
||||||
|
}
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--node-border-radius);
|
||||||
transition: box-shadow 0.2s ease-in-out;
|
transition: box-shadow 0.2s ease-in-out;
|
||||||
::selection {
|
::selection {
|
||||||
background-color: rgba(255, 255, 255, 20%);
|
background-color: rgba(255, 255, 255, 20%);
|
||||||
@ -476,10 +514,15 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
transition:
|
||||||
|
background 0.2s ease,
|
||||||
|
outline 0.2s ease;
|
||||||
|
outline: 0px solid transparent;
|
||||||
}
|
}
|
||||||
.GraphNode .selection {
|
.GraphNode .selection {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: calc(0px - var(--selected-node-border-width));
|
inset: calc(0px - var(--selected-node-border-width));
|
||||||
|
--node-current-selection-width: 0px;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: '';
|
||||||
@ -488,7 +531,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
border-radius: var(--node-border-radius);
|
border-radius: var(--node-border-radius);
|
||||||
display: block;
|
display: block;
|
||||||
inset: var(--selected-node-border-width);
|
inset: var(--selected-node-border-width);
|
||||||
box-shadow: 0 0 0 0 var(--node-color-primary);
|
box-shadow: 0 0 0 var(--node-current-selection-width) var(--node-color-primary);
|
||||||
|
|
||||||
transition:
|
transition:
|
||||||
box-shadow 0.2s ease-in-out,
|
box-shadow 0.2s ease-in-out,
|
||||||
@ -498,7 +541,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
|
|
||||||
.GraphNode:is(:hover, .selected) .selection:before,
|
.GraphNode:is(:hover, .selected) .selection:before,
|
||||||
.GraphNode .selection:hover:before {
|
.GraphNode .selection:hover:before {
|
||||||
box-shadow: 0 0 0 var(--selected-node-border-width) var(--node-color-primary);
|
--node-current-selection-width: var(--selected-node-border-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.GraphNode .selection:hover:before {
|
.GraphNode .selection:hover:before {
|
||||||
@ -531,7 +574,8 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
.editable {
|
.editable {
|
||||||
outline: none;
|
outline: none;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 1px 0;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -548,4 +592,20 @@ const dragPointer = usePointer((pos, event, type) => {
|
|||||||
.CircularMenu {
|
.CircularMenu {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.outputTypeName {
|
||||||
|
user-select: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 110%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--node-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.GraphNode:hover .outputTypeName {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -82,16 +82,13 @@ watch(exprRect, (rect) => {
|
|||||||
.Span {
|
.Span {
|
||||||
color: white;
|
color: white;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
align-items: center;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
|
||||||
&.Root {
|
&.Root {
|
||||||
display: inline-block;
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.Ident {
|
|
||||||
/* color: #f97; */
|
|
||||||
}
|
|
||||||
|
|
||||||
&.Token {
|
&.Token {
|
||||||
color: rgb(255 255 255 / 0.33);
|
color: rgb(255 255 255 / 0.33);
|
||||||
}
|
}
|
||||||
@ -99,5 +96,12 @@ watch(exprRect, (rect) => {
|
|||||||
&.Literal {
|
&.Literal {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.Ident {
|
||||||
|
background-color: var(--node-color-port);
|
||||||
|
border-radius: var(--node-border-radius);
|
||||||
|
margin: -2px -4px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -146,6 +146,7 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
function nodeInserted(stmt: Statement, text: Y.Text, content: string, meta: Opt<NodeMetadata>) {
|
function nodeInserted(stmt: Statement, text: Y.Text, content: string, meta: Opt<NodeMetadata>) {
|
||||||
const nodeId = stmt.expression.id
|
const nodeId = stmt.expression.id
|
||||||
const node: Node = {
|
const node: Node = {
|
||||||
|
outerExprId: stmt.id,
|
||||||
content,
|
content,
|
||||||
binding: stmt.binding ?? '',
|
binding: stmt.binding ?? '',
|
||||||
rootSpan: stmt.expression,
|
rootSpan: stmt.expression,
|
||||||
@ -173,6 +174,9 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
node.binding = stmt.binding ?? ''
|
node.binding = stmt.binding ?? ''
|
||||||
identDefinitions.set(node.binding, nodeId)
|
identDefinitions.set(node.binding, nodeId)
|
||||||
}
|
}
|
||||||
|
if (node.outerExprId !== stmt.id) {
|
||||||
|
node.outerExprId = stmt.id
|
||||||
|
}
|
||||||
if (node.rootSpan.id === stmt.expression.id) {
|
if (node.rootSpan.id === stmt.expression.id) {
|
||||||
patchSpan(node.rootSpan, stmt.expression)
|
patchSpan(node.rootSpan, stmt.expression)
|
||||||
} else {
|
} else {
|
||||||
@ -270,7 +274,9 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteNode(id: ExprId) {
|
function deleteNode(id: ExprId) {
|
||||||
proj.module?.deleteNode(id)
|
const node = nodes.get(id)
|
||||||
|
if (node == null) return
|
||||||
|
proj.module?.deleteExpression(node.outerExprId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNodeContent(id: ExprId, content: string) {
|
function setNodeContent(id: ExprId, content: string) {
|
||||||
@ -359,6 +365,7 @@ function randomString() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Node {
|
export interface Node {
|
||||||
|
outerExprId: ExprId
|
||||||
content: string
|
content: string
|
||||||
binding: string
|
binding: string
|
||||||
rootSpan: Span
|
rootSpan: Span
|
||||||
|
@ -1,21 +1,26 @@
|
|||||||
import { useGuiConfig, type GuiConfig } from '@/providers/guiConfig'
|
import { useGuiConfig, type GuiConfig } from '@/providers/guiConfig'
|
||||||
|
import { ComputedValueRegistry } from '@/util/computedValueRegistry'
|
||||||
import { attachProvider } from '@/util/crdt'
|
import { attachProvider } from '@/util/crdt'
|
||||||
import { AsyncQueue, rpcWithRetries as lsRpcWithRetries } from '@/util/net'
|
import { AsyncQueue, rpcWithRetries as lsRpcWithRetries } from '@/util/net'
|
||||||
import { isSome, type Opt } from '@/util/opt'
|
import { isSome, type Opt } from '@/util/opt'
|
||||||
|
import { VisualizationDataRegistry } from '@/util/visualizationDataRegistry'
|
||||||
import { Client, RequestManager, WebSocketTransport } from '@open-rpc/client-js'
|
import { Client, RequestManager, WebSocketTransport } from '@open-rpc/client-js'
|
||||||
import { computedAsync } from '@vueuse/core'
|
import { computedAsync } from '@vueuse/core'
|
||||||
import * as array from 'lib0/array'
|
import * as array from 'lib0/array'
|
||||||
import * as object from 'lib0/object'
|
import * as object from 'lib0/object'
|
||||||
|
import { ObservableV2 } from 'lib0/observable'
|
||||||
import * as random from 'lib0/random'
|
import * as random from 'lib0/random'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { OutboundPayload, VisualizationUpdate } from 'shared/binaryProtocol'
|
|
||||||
import { DataServer } from 'shared/dataServer'
|
import { DataServer } from 'shared/dataServer'
|
||||||
import { LanguageServer } from 'shared/languageServer'
|
import { LanguageServer } from 'shared/languageServer'
|
||||||
import type {
|
import type {
|
||||||
ContentRoot,
|
ContentRoot,
|
||||||
ContextId,
|
ContextId,
|
||||||
|
Diagnostic,
|
||||||
|
ExecutionEnvironment,
|
||||||
ExplicitCall,
|
ExplicitCall,
|
||||||
ExpressionId,
|
ExpressionId,
|
||||||
|
ExpressionUpdate,
|
||||||
StackItem,
|
StackItem,
|
||||||
VisualizationConfiguration,
|
VisualizationConfiguration,
|
||||||
} from 'shared/languageServerTypes'
|
} from 'shared/languageServerTypes'
|
||||||
@ -118,6 +123,20 @@ function visualizationConfigEqual(
|
|||||||
|
|
||||||
type EntryPoint = Omit<ExplicitCall, 'type'>
|
type EntryPoint = Omit<ExplicitCall, 'type'>
|
||||||
|
|
||||||
|
type ExecutionContextNotification = {
|
||||||
|
'expressionUpdates'(updates: ExpressionUpdate[]): void
|
||||||
|
'visualizationEvaluationFailed'(
|
||||||
|
visualizationId: Uuid,
|
||||||
|
expressionId: ExpressionId,
|
||||||
|
message: string,
|
||||||
|
diagnostic: Diagnostic | undefined,
|
||||||
|
): void
|
||||||
|
'executionFailed'(message: string): void
|
||||||
|
'executionComplete'(): void
|
||||||
|
'executionStatus'(diagnostics: Diagnostic[]): void
|
||||||
|
'visualizationsConfigured'(configs: Set<Uuid>): void
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execution Context
|
* Execution Context
|
||||||
*
|
*
|
||||||
@ -127,7 +146,7 @@ type EntryPoint = Omit<ExplicitCall, 'type'>
|
|||||||
* It hides the asynchronous nature of the language server. Each call is scheduled and
|
* It hides the asynchronous nature of the language server. Each call is scheduled and
|
||||||
* run only when the previous call is done.
|
* run only when the previous call is done.
|
||||||
*/
|
*/
|
||||||
export class ExecutionContext {
|
export class ExecutionContext extends ObservableV2<ExecutionContextNotification> {
|
||||||
id: ContextId = random.uuidv4() as ContextId
|
id: ContextId = random.uuidv4() as ContextId
|
||||||
queue: AsyncQueue<ExecutionContextState>
|
queue: AsyncQueue<ExecutionContextState>
|
||||||
taskRunning = false
|
taskRunning = false
|
||||||
@ -136,6 +155,12 @@ export class ExecutionContext {
|
|||||||
abortCtl = new AbortController()
|
abortCtl = new AbortController()
|
||||||
|
|
||||||
constructor(lsRpc: Promise<LanguageServer>, entryPoint: EntryPoint) {
|
constructor(lsRpc: Promise<LanguageServer>, entryPoint: EntryPoint) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.abortCtl.signal.addEventListener('abort', () => {
|
||||||
|
this.queue.clear()
|
||||||
|
})
|
||||||
|
|
||||||
this.queue = new AsyncQueue(
|
this.queue = new AsyncQueue(
|
||||||
lsRpc.then((lsRpc) => ({
|
lsRpc.then((lsRpc) => ({
|
||||||
lsRpc,
|
lsRpc,
|
||||||
@ -144,8 +169,10 @@ export class ExecutionContext {
|
|||||||
stack: [],
|
stack: [],
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
this.registerHandlers()
|
||||||
this.create()
|
this.create()
|
||||||
this.pushItem({ type: 'ExplicitCall', ...entryPoint })
|
this.pushItem({ type: 'ExplicitCall', ...entryPoint })
|
||||||
|
this.recompute()
|
||||||
}
|
}
|
||||||
|
|
||||||
private withBackoff<T>(f: () => Promise<T>, message: string): Promise<T> {
|
private withBackoff<T>(f: () => Promise<T>, message: string): Promise<T> {
|
||||||
@ -241,6 +268,8 @@ export class ExecutionContext {
|
|||||||
console.error('Failed to synchronize visualizations:', errors)
|
console.error('Failed to synchronize visualizations:', errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('visualizationsConfigured', [new Set(this.visualizationConfigs.keys())])
|
||||||
|
|
||||||
// State object was updated in-place in each successful promise.
|
// State object was updated in-place in each successful promise.
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
@ -297,16 +326,65 @@ export class ExecutionContext {
|
|||||||
return { ...state, created: true }
|
return { ...state, created: true }
|
||||||
}, 'Failed to create execution context')
|
}, 'Failed to create execution context')
|
||||||
})
|
})
|
||||||
|
this.abortCtl.signal.addEventListener('abort', () => {
|
||||||
|
this.queue.pushTask(async (state) => {
|
||||||
|
if (!state.created) return state
|
||||||
|
await state.lsRpc.destroyExecutionContext(this.id)
|
||||||
|
return { ...state, created: false }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlers() {
|
||||||
|
this.queue.pushTask(async (state) => {
|
||||||
|
const expressionUpdates = state.lsRpc.on('executionContext/expressionUpdates', (event) => {
|
||||||
|
if (event.contextId == this.id) this.emit('expressionUpdates', [event.updates])
|
||||||
|
})
|
||||||
|
const executionFailed = state.lsRpc.on('executionContext/executionFailed', (event) => {
|
||||||
|
if (event.contextId == this.id) this.emit('executionFailed', [event.message])
|
||||||
|
})
|
||||||
|
const executionComplete = state.lsRpc.on('executionContext/executionComplete', (event) => {
|
||||||
|
if (event.contextId == this.id) this.emit('executionComplete', [])
|
||||||
|
})
|
||||||
|
const executionStatus = state.lsRpc.on('executionContext/executionStatus', (event) => {
|
||||||
|
if (event.contextId == this.id) this.emit('executionStatus', [event.diagnostics])
|
||||||
|
})
|
||||||
|
const visualizationEvaluationFailed = state.lsRpc.on(
|
||||||
|
'executionContext/visualizationEvaluationFailed',
|
||||||
|
(event) => {
|
||||||
|
if (event.contextId == this.id)
|
||||||
|
this.emit('visualizationEvaluationFailed', [
|
||||||
|
event.visualizationId,
|
||||||
|
event.expressionId,
|
||||||
|
event.message,
|
||||||
|
event.diagnostic,
|
||||||
|
])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
this.abortCtl.signal.addEventListener('abort', () => {
|
||||||
|
state.lsRpc.off('executionContext/expressionUpdates', expressionUpdates)
|
||||||
|
state.lsRpc.off('executionContext/executionFailed', executionFailed)
|
||||||
|
state.lsRpc.off('executionContext/executionComplete', executionComplete)
|
||||||
|
state.lsRpc.off('executionContext/executionStatus', executionStatus)
|
||||||
|
state.lsRpc.off(
|
||||||
|
'executionContext/visualizationEvaluationFailed',
|
||||||
|
visualizationEvaluationFailed,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
recompute(expressionIds: 'all' | ExprId[] = 'all', executionEnvironment?: ExecutionEnvironment) {
|
||||||
|
this.queue.pushTask(async (state) => {
|
||||||
|
if (!state.created) return state
|
||||||
|
await state.lsRpc.recomputeExecutionContext(this.id, expressionIds, executionEnvironment)
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.abortCtl.abort()
|
this.abortCtl.abort()
|
||||||
this.queue.clear()
|
|
||||||
this.queue.pushTask(async (state) => {
|
|
||||||
if (!state.created) return state
|
|
||||||
await state.lsRpc.destroyExecutionContext(this.id)
|
|
||||||
return { ...state, created: false }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,38 +471,31 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const executionContext = createExecutionContextForMain()
|
const executionContext = createExecutionContextForMain()
|
||||||
const dataConnectionRef = computedAsync<DataServer | undefined>(() => dataConnection)
|
const computedValueRegistry = new ComputedValueRegistry(executionContext)
|
||||||
|
const visualizationDataRegistry = new VisualizationDataRegistry(executionContext, dataConnection)
|
||||||
|
|
||||||
function useVisualizationData(
|
function useVisualizationData(
|
||||||
configuration: WatchSource<Opt<NodeVisualizationConfiguration>>,
|
configuration: WatchSource<Opt<NodeVisualizationConfiguration>>,
|
||||||
): ShallowRef<{} | undefined> {
|
): ShallowRef<{} | undefined> {
|
||||||
const id = random.uuidv4() as Uuid
|
const id = random.uuidv4() as Uuid
|
||||||
const visualizationData = shallowRef<{}>()
|
|
||||||
|
|
||||||
watch(configuration, async (config, _, onCleanup) => {
|
watch(
|
||||||
executionContext.setVisualization(id, config)
|
configuration,
|
||||||
onCleanup(() => {
|
async (config, _, onCleanup) => {
|
||||||
executionContext.setVisualization(id, null)
|
executionContext.setVisualization(id, config)
|
||||||
})
|
onCleanup(() => {
|
||||||
})
|
executionContext.setVisualization(id, null)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
watchEffect((onCleanup) => {
|
return shallowRef(
|
||||||
const connection = dataConnectionRef.value
|
computed(() => {
|
||||||
const dataEvent = `${OutboundPayload.VISUALIZATION_UPDATE}:${id}`
|
const json = visualizationDataRegistry.getRawData(id)
|
||||||
if (connection == null) return
|
return json != null ? JSON.parse(json) : undefined
|
||||||
connection.on(dataEvent, onVisualizationUpdate)
|
}),
|
||||||
onCleanup(() => {
|
)
|
||||||
connection.off(dataEvent, onVisualizationUpdate)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
function onVisualizationUpdate(vizUpdate: VisualizationUpdate) {
|
|
||||||
const json = vizUpdate.dataString()
|
|
||||||
const newData = json != null ? JSON.parse(json) : undefined
|
|
||||||
visualizationData.value = newData
|
|
||||||
}
|
|
||||||
|
|
||||||
return visualizationData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopCapturingUndo() {
|
function stopCapturingUndo() {
|
||||||
@ -436,11 +507,11 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
observedFileName.value = name
|
observedFileName.value = name
|
||||||
},
|
},
|
||||||
name: projectName,
|
name: projectName,
|
||||||
createExecutionContextForMain,
|
|
||||||
executionContext,
|
executionContext,
|
||||||
module,
|
module,
|
||||||
contentRoots,
|
contentRoots,
|
||||||
awareness,
|
awareness,
|
||||||
|
computedValueRegistry,
|
||||||
lsRpcConnection: markRaw(lsRpcConnection),
|
lsRpcConnection: markRaw(lsRpcConnection),
|
||||||
dataConnection: markRaw(dataConnection),
|
dataConnection: markRaw(dataConnection),
|
||||||
useVisualizationData,
|
useVisualizationData,
|
||||||
|
49
app/gui2/src/util/computedValueRegistry.ts
Normal file
49
app/gui2/src/util/computedValueRegistry.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import type { ExecutionContext } from '@/stores/project.ts'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import type {
|
||||||
|
ExpressionId,
|
||||||
|
ExpressionUpdate,
|
||||||
|
ExpressionUpdatePayload,
|
||||||
|
MethodCall,
|
||||||
|
ProfilingInfo,
|
||||||
|
} from '../../shared/languageServerTypes.ts'
|
||||||
|
|
||||||
|
export interface ExpressionInfo {
|
||||||
|
typename: string | undefined
|
||||||
|
methodCall: MethodCall | undefined
|
||||||
|
payload: ExpressionUpdatePayload
|
||||||
|
profilingInfo: ProfilingInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
//* This class holds the computed values that have been received from the language server. */
|
||||||
|
export class ComputedValueRegistry {
|
||||||
|
private expressionMap: Map<ExpressionId, ExpressionInfo>
|
||||||
|
private _updateHandler = this.processUpdates.bind(this)
|
||||||
|
private executionContext
|
||||||
|
|
||||||
|
constructor(executionContext: ExecutionContext) {
|
||||||
|
this.executionContext = executionContext
|
||||||
|
this.expressionMap = reactive(new Map())
|
||||||
|
|
||||||
|
executionContext.on('expressionUpdates', this._updateHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
processUpdates(updates: ExpressionUpdate[]) {
|
||||||
|
for (const update of updates) {
|
||||||
|
this.expressionMap.set(update.expressionId, {
|
||||||
|
typename: update.type,
|
||||||
|
methodCall: update.methodCall,
|
||||||
|
payload: update.payload,
|
||||||
|
profilingInfo: update.profilingInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getExpressionInfo(exprId: ExpressionId): ExpressionInfo | undefined {
|
||||||
|
return this.expressionMap.get(exprId)
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.executionContext.off('expressionUpdates', this._updateHandler)
|
||||||
|
}
|
||||||
|
}
|
66
app/gui2/src/util/visualizationDataRegistry.ts
Normal file
66
app/gui2/src/util/visualizationDataRegistry.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import type { ExecutionContext } from '@/stores/project.ts'
|
||||||
|
import { OutboundPayload, VisualizationUpdate } from 'shared/binaryProtocol.ts'
|
||||||
|
import type { DataServer } from 'shared/dataServer.ts'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
import type {
|
||||||
|
ExpressionUpdatePayload,
|
||||||
|
MethodCall,
|
||||||
|
ProfilingInfo,
|
||||||
|
Uuid,
|
||||||
|
} from '../../shared/languageServerTypes.ts'
|
||||||
|
|
||||||
|
export interface ExpressionInfo {
|
||||||
|
typename: string | undefined
|
||||||
|
methodCall: MethodCall | undefined
|
||||||
|
payload: ExpressionUpdatePayload
|
||||||
|
profilingInfo: ProfilingInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
//* This class holds the computed values that have been received from the language server. */
|
||||||
|
export class VisualizationDataRegistry {
|
||||||
|
private visualizationValues: Map<Uuid, string | null>
|
||||||
|
private dataServer: Promise<DataServer>
|
||||||
|
private executionContext: ExecutionContext
|
||||||
|
private _reconfiguredHandler = this.visualizationsConfigured.bind(this)
|
||||||
|
private _dataHandler = this.visualizationUpdate.bind(this)
|
||||||
|
|
||||||
|
constructor(executionContext: ExecutionContext, dataServer: Promise<DataServer>) {
|
||||||
|
this.executionContext = executionContext
|
||||||
|
this.dataServer = dataServer
|
||||||
|
this.visualizationValues = reactive(new Map())
|
||||||
|
|
||||||
|
this.executionContext.on('visualizationsConfigured', this._reconfiguredHandler)
|
||||||
|
this.dataServer.then((data) => {
|
||||||
|
data.on(`${OutboundPayload.VISUALIZATION_UPDATE}`, this._dataHandler)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private visualizationsConfigured(uuids: Set<Uuid>) {
|
||||||
|
for (const key of this.visualizationValues.keys()) {
|
||||||
|
if (!uuids.has(key)) {
|
||||||
|
this.visualizationValues.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private visualizationUpdate(update: VisualizationUpdate, uuid: Uuid | null) {
|
||||||
|
if (uuid) {
|
||||||
|
const newData = update.dataString()
|
||||||
|
const current = this.visualizationValues.get(uuid)
|
||||||
|
if (current !== newData) {
|
||||||
|
this.visualizationValues.set(uuid, newData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRawData(visualizationId: Uuid): string | null {
|
||||||
|
return this.visualizationValues.get(visualizationId) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.executionContext.off('visualizationsConfigured', this._reconfiguredHandler)
|
||||||
|
this.dataServer.then((data) => {
|
||||||
|
data.off(`${OutboundPayload.VISUALIZATION_UPDATE}`, this._dataHandler)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user