mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 13:02:07 +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 { 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 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] {
|
||||
@ -51,8 +51,9 @@ const PAYLOAD_CONSTRUCTOR = {
|
||||
} satisfies Record<OutboundPayload, new () => Table>
|
||||
|
||||
export type DataServerEvents = {
|
||||
[K in keyof typeof PAYLOAD_CONSTRUCTOR as K | `${K}:${string}`]: (
|
||||
arg: InstanceType<(typeof PAYLOAD_CONSTRUCTOR)[K]>,
|
||||
[K in keyof typeof PAYLOAD_CONSTRUCTOR as `${K}`]: (
|
||||
payload: InstanceType<(typeof PAYLOAD_CONSTRUCTOR)[K]>,
|
||||
uuid: Uuid | null,
|
||||
) => void
|
||||
}
|
||||
|
||||
@ -84,18 +85,18 @@ export class DataServer extends ObservableV2<DataServerEvents> {
|
||||
const payloadType = binaryMessage.payloadType()
|
||||
const payload = binaryMessage.payload(new PAYLOAD_CONSTRUCTOR[payloadType]())
|
||||
if (payload != null) {
|
||||
this.emit(`${payloadType}`, [payload])
|
||||
this.emit(`${payloadType}`, [payload, null])
|
||||
const id = binaryMessage.correlationId()
|
||||
if (id != null) {
|
||||
const uuid = uuidFromBits(id.leastSigBits(), id.mostSigBits())
|
||||
this.emit(`${payloadType}:${uuid}`, [payload])
|
||||
this.emit(`${payloadType}`, [payload, uuid])
|
||||
const callback = this.resolveCallbacks.get(uuid)
|
||||
callback?.(payload)
|
||||
} else if (payload instanceof VisualizationUpdate) {
|
||||
const id = payload.visualizationContext()?.visualizationId()
|
||||
if (id != null) {
|
||||
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,
|
||||
visualizationConfig: VisualizationConfiguration,
|
||||
): Promise<void> {
|
||||
return this.request('executionContext/interrupt', {
|
||||
return this.request('executionContext/executeExpression', {
|
||||
visualizationId,
|
||||
expressionId,
|
||||
visualizationConfig,
|
||||
|
@ -96,6 +96,7 @@ export type ExpressionUpdatePayload = Value | DataflowError | Panic | Pending
|
||||
* Indicates that the expression was computed to a value.
|
||||
*/
|
||||
export interface Value {
|
||||
type: 'Value'
|
||||
/**
|
||||
* Information about attached warnings.
|
||||
*/
|
||||
@ -111,6 +112,7 @@ export interface Value {
|
||||
* Indicates that the expression was computed to an error.
|
||||
*/
|
||||
export interface DataflowError {
|
||||
type: 'DataflowError'
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export interface Panic {
|
||||
type: 'Panic'
|
||||
/**
|
||||
* The error message.
|
||||
*/
|
||||
@ -137,6 +140,7 @@ export interface Panic {
|
||||
* provides description and percentage (`0.0-1.0`) of completeness.
|
||||
*/
|
||||
export interface Pending {
|
||||
type: 'Pending'
|
||||
/** Optional message describing current operation. */
|
||||
message?: string
|
||||
/** Optional amount of already done work as a number between `0.0` to `1.0`. */
|
||||
|
@ -140,7 +140,7 @@ export class DistributedModule {
|
||||
return newId
|
||||
}
|
||||
|
||||
deleteNode(id: ExprId): void {
|
||||
deleteExpression(id: ExprId): void {
|
||||
const rangeBuffer = this.doc.idMap.get(id)
|
||||
if (rangeBuffer == null) return
|
||||
const [relStart, relEnd] = decodeRange(rangeBuffer)
|
||||
|
@ -12,7 +12,7 @@ export const graphBindings = defineKeybinds('graph-editor', {
|
||||
newNode: ['N'],
|
||||
})
|
||||
|
||||
export const nodeBindings = defineKeybinds('node-selection', {
|
||||
export const nodeSelectionBindings = defineKeybinds('node-selection', {
|
||||
deleteSelected: ['Delete'],
|
||||
selectAll: ['Mod+A'],
|
||||
deselectAll: ['Escape', 'PointerMain'],
|
||||
@ -23,3 +23,7 @@ export const nodeBindings = defineKeybinds('node-selection', {
|
||||
invert: ['Mod+Shift+Alt+PointerMain'],
|
||||
toggleVisualization: ['Space'],
|
||||
})
|
||||
|
||||
export const nodeEditBindings = defineKeybinds('node-edit', {
|
||||
selectAll: ['Mod+A'],
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { codeEditorBindings, graphBindings, nodeBindings } from '@/bindings'
|
||||
import { codeEditorBindings, graphBindings, nodeSelectionBindings } from '@/bindings'
|
||||
import CodeEditor from '@/components/CodeEditor.vue'
|
||||
import ComponentBrowser from '@/components/ComponentBrowser.vue'
|
||||
import GraphEdge from '@/components/GraphEdge.vue'
|
||||
@ -159,6 +159,7 @@ const graphBindingsHandler = graphBindings.handler({
|
||||
}
|
||||
},
|
||||
newNode() {
|
||||
if (keyboardBusy()) return false
|
||||
if (navigator.sceneMousePos != null) {
|
||||
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() {
|
||||
graphStore.transact(() => {
|
||||
for (const node of selectedNodes.value) {
|
||||
@ -207,7 +208,7 @@ const nodeSelectionHandler = nodeBindings.handler({
|
||||
},
|
||||
})
|
||||
|
||||
const mouseHandler = nodeBindings.handler({
|
||||
const mouseHandler = nodeSelectionBindings.handler({
|
||||
replace() {
|
||||
selectedNodes.value = new Set(intersectingNodes.value)
|
||||
},
|
||||
@ -372,12 +373,4 @@ svg {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: purple;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { nodeBindings } from '@/bindings'
|
||||
import { nodeEditBindings, nodeSelectionBindings } from '@/bindings'
|
||||
import CircularMenu from '@/components/CircularMenu.vue'
|
||||
import NodeSpan from '@/components/NodeSpan.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
@ -27,6 +27,7 @@ const MAXIMUM_CLICK_LENGTH_MS = 300
|
||||
|
||||
const props = defineProps<{
|
||||
node: Node
|
||||
// id: string & ExprId
|
||||
selected: boolean
|
||||
isLatestSelected: boolean
|
||||
fullscreenVis: boolean
|
||||
@ -354,7 +355,7 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
const mouseHandler = nodeBindings.handler({
|
||||
const mouseHandler = nodeSelectionBindings.handler({
|
||||
replace() {
|
||||
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 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>
|
||||
|
||||
<template>
|
||||
@ -406,6 +431,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
dragging: dragPointer.dragging,
|
||||
selected,
|
||||
visualizationVisible: isVisualizationVisible,
|
||||
['executionState-' + executionState]: true,
|
||||
}"
|
||||
>
|
||||
<div class="selection" v-on="dragPointer.events"></div>
|
||||
@ -433,6 +459,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
contenteditable
|
||||
spellcheck="false"
|
||||
@beforeinput="editContent"
|
||||
@keydown="editableKeydownHandler"
|
||||
@pointerdown.stop
|
||||
@blur="projectStore.stopCapturingUndo()"
|
||||
>
|
||||
@ -444,6 +471,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="outputTypeName">{{ outputTypeName }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -452,9 +480,19 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
--node-height: 32px;
|
||||
--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;
|
||||
border-radius: var(--radius-full);
|
||||
border-radius: var(--node-border-radius);
|
||||
transition: box-shadow 0.2s ease-in-out;
|
||||
::selection {
|
||||
background-color: rgba(255, 255, 255, 20%);
|
||||
@ -476,10 +514,15 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
white-space: nowrap;
|
||||
padding: 4px 8px;
|
||||
z-index: 2;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
outline 0.2s ease;
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
.GraphNode .selection {
|
||||
position: absolute;
|
||||
inset: calc(0px - var(--selected-node-border-width));
|
||||
--node-current-selection-width: 0px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
@ -488,7 +531,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
border-radius: var(--node-border-radius);
|
||||
display: block;
|
||||
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:
|
||||
box-shadow 0.2s ease-in-out,
|
||||
@ -498,7 +541,7 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
|
||||
.GraphNode:is(:hover, .selected) .selection: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 {
|
||||
@ -531,7 +574,8 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
.editable {
|
||||
outline: none;
|
||||
height: 24px;
|
||||
padding: 1px 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
@ -548,4 +592,20 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
.CircularMenu {
|
||||
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>
|
||||
|
@ -82,16 +82,13 @@ watch(exprRect, (rect) => {
|
||||
.Span {
|
||||
color: white;
|
||||
white-space: pre;
|
||||
align-items: center;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&.Root {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.Ident {
|
||||
/* color: #f97; */
|
||||
}
|
||||
|
||||
&.Token {
|
||||
color: rgb(255 255 255 / 0.33);
|
||||
}
|
||||
@ -99,5 +96,12 @@ watch(exprRect, (rect) => {
|
||||
&.Literal {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.Ident {
|
||||
background-color: var(--node-color-port);
|
||||
border-radius: var(--node-border-radius);
|
||||
margin: -2px -4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -146,6 +146,7 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
function nodeInserted(stmt: Statement, text: Y.Text, content: string, meta: Opt<NodeMetadata>) {
|
||||
const nodeId = stmt.expression.id
|
||||
const node: Node = {
|
||||
outerExprId: stmt.id,
|
||||
content,
|
||||
binding: stmt.binding ?? '',
|
||||
rootSpan: stmt.expression,
|
||||
@ -173,6 +174,9 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
node.binding = stmt.binding ?? ''
|
||||
identDefinitions.set(node.binding, nodeId)
|
||||
}
|
||||
if (node.outerExprId !== stmt.id) {
|
||||
node.outerExprId = stmt.id
|
||||
}
|
||||
if (node.rootSpan.id === stmt.expression.id) {
|
||||
patchSpan(node.rootSpan, stmt.expression)
|
||||
} else {
|
||||
@ -270,7 +274,9 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -359,6 +365,7 @@ function randomString() {
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
outerExprId: ExprId
|
||||
content: string
|
||||
binding: string
|
||||
rootSpan: Span
|
||||
|
@ -1,21 +1,26 @@
|
||||
import { useGuiConfig, type GuiConfig } from '@/providers/guiConfig'
|
||||
import { ComputedValueRegistry } from '@/util/computedValueRegistry'
|
||||
import { attachProvider } from '@/util/crdt'
|
||||
import { AsyncQueue, rpcWithRetries as lsRpcWithRetries } from '@/util/net'
|
||||
import { isSome, type Opt } from '@/util/opt'
|
||||
import { VisualizationDataRegistry } from '@/util/visualizationDataRegistry'
|
||||
import { Client, RequestManager, WebSocketTransport } from '@open-rpc/client-js'
|
||||
import { computedAsync } from '@vueuse/core'
|
||||
import * as array from 'lib0/array'
|
||||
import * as object from 'lib0/object'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
import * as random from 'lib0/random'
|
||||
import { defineStore } from 'pinia'
|
||||
import { OutboundPayload, VisualizationUpdate } from 'shared/binaryProtocol'
|
||||
import { DataServer } from 'shared/dataServer'
|
||||
import { LanguageServer } from 'shared/languageServer'
|
||||
import type {
|
||||
ContentRoot,
|
||||
ContextId,
|
||||
Diagnostic,
|
||||
ExecutionEnvironment,
|
||||
ExplicitCall,
|
||||
ExpressionId,
|
||||
ExpressionUpdate,
|
||||
StackItem,
|
||||
VisualizationConfiguration,
|
||||
} from 'shared/languageServerTypes'
|
||||
@ -118,6 +123,20 @@ function visualizationConfigEqual(
|
||||
|
||||
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
|
||||
*
|
||||
@ -127,7 +146,7 @@ type EntryPoint = Omit<ExplicitCall, 'type'>
|
||||
* It hides the asynchronous nature of the language server. Each call is scheduled and
|
||||
* run only when the previous call is done.
|
||||
*/
|
||||
export class ExecutionContext {
|
||||
export class ExecutionContext extends ObservableV2<ExecutionContextNotification> {
|
||||
id: ContextId = random.uuidv4() as ContextId
|
||||
queue: AsyncQueue<ExecutionContextState>
|
||||
taskRunning = false
|
||||
@ -136,6 +155,12 @@ export class ExecutionContext {
|
||||
abortCtl = new AbortController()
|
||||
|
||||
constructor(lsRpc: Promise<LanguageServer>, entryPoint: EntryPoint) {
|
||||
super()
|
||||
|
||||
this.abortCtl.signal.addEventListener('abort', () => {
|
||||
this.queue.clear()
|
||||
})
|
||||
|
||||
this.queue = new AsyncQueue(
|
||||
lsRpc.then((lsRpc) => ({
|
||||
lsRpc,
|
||||
@ -144,8 +169,10 @@ export class ExecutionContext {
|
||||
stack: [],
|
||||
})),
|
||||
)
|
||||
this.registerHandlers()
|
||||
this.create()
|
||||
this.pushItem({ type: 'ExplicitCall', ...entryPoint })
|
||||
this.recompute()
|
||||
}
|
||||
|
||||
private withBackoff<T>(f: () => Promise<T>, message: string): Promise<T> {
|
||||
@ -241,6 +268,8 @@ export class ExecutionContext {
|
||||
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.
|
||||
return state
|
||||
})
|
||||
@ -297,16 +326,65 @@ export class ExecutionContext {
|
||||
return { ...state, created: true }
|
||||
}, 'Failed to create execution context')
|
||||
})
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.abortCtl.abort()
|
||||
this.queue.clear()
|
||||
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() {
|
||||
this.abortCtl.abort()
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,38 +471,31 @@ export const useProjectStore = defineStore('project', () => {
|
||||
}
|
||||
|
||||
const executionContext = createExecutionContextForMain()
|
||||
const dataConnectionRef = computedAsync<DataServer | undefined>(() => dataConnection)
|
||||
const computedValueRegistry = new ComputedValueRegistry(executionContext)
|
||||
const visualizationDataRegistry = new VisualizationDataRegistry(executionContext, dataConnection)
|
||||
|
||||
function useVisualizationData(
|
||||
configuration: WatchSource<Opt<NodeVisualizationConfiguration>>,
|
||||
): ShallowRef<{} | undefined> {
|
||||
const id = random.uuidv4() as Uuid
|
||||
const visualizationData = shallowRef<{}>()
|
||||
|
||||
watch(configuration, async (config, _, onCleanup) => {
|
||||
watch(
|
||||
configuration,
|
||||
async (config, _, onCleanup) => {
|
||||
executionContext.setVisualization(id, config)
|
||||
onCleanup(() => {
|
||||
executionContext.setVisualization(id, null)
|
||||
})
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watchEffect((onCleanup) => {
|
||||
const connection = dataConnectionRef.value
|
||||
const dataEvent = `${OutboundPayload.VISUALIZATION_UPDATE}:${id}`
|
||||
if (connection == null) return
|
||||
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
|
||||
return shallowRef(
|
||||
computed(() => {
|
||||
const json = visualizationDataRegistry.getRawData(id)
|
||||
return json != null ? JSON.parse(json) : undefined
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function stopCapturingUndo() {
|
||||
@ -436,11 +507,11 @@ export const useProjectStore = defineStore('project', () => {
|
||||
observedFileName.value = name
|
||||
},
|
||||
name: projectName,
|
||||
createExecutionContextForMain,
|
||||
executionContext,
|
||||
module,
|
||||
contentRoots,
|
||||
awareness,
|
||||
computedValueRegistry,
|
||||
lsRpcConnection: markRaw(lsRpcConnection),
|
||||
dataConnection: markRaw(dataConnection),
|
||||
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