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:
Michael Mauderer 2023-10-09 07:55:12 +02:00 committed by GitHub
parent 44f2f425c0
commit 10a95e43d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 327 additions and 68 deletions

View File

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

View File

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

View File

@ -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`. */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)
}
}

View 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)
})
}
}