mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 08:53:31 +03:00
Set output evaluation context for a single node (#8440)
- Closes #8072 - Implement handlers for the corresponding buttons on the circular menu - Add missing icons and styles - Add functionality to match and extract ASTs # Important Notes None
This commit is contained in:
parent
b5c995a7bf
commit
927df167d7
@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getMainFile, setMainFile } from '../mock/engine'
|
||||
import App from '../src/App.vue'
|
||||
import MockProjectStoreWrapper from '../stories/MockProjectStoreWrapper.vue'
|
||||
import { getMainFile, setMainFile } from './mockEngine'
|
||||
|
||||
const mainFile = computed({
|
||||
get() {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import 'enso-dashboard/src/tailwind.css'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp, ref } from 'vue'
|
||||
import { mockDataHandler, mockLSHandler } from '../mock/engine'
|
||||
import '../src/assets/base.css'
|
||||
import { provideGuiConfig } from '../src/providers/guiConfig'
|
||||
import { provideVisualizationConfig } from '../src/providers/visualizationConfig'
|
||||
import { Vec2 } from '../src/util/data/vec2'
|
||||
import { MockTransport, MockWebSocket } from '../src/util/net'
|
||||
import MockApp from './MockApp.vue'
|
||||
import { mockDataHandler, mockLSHandler } from './mockEngine'
|
||||
|
||||
MockTransport.addMock('engine', mockLSHandler)
|
||||
MockWebSocket.addMock('data', mockDataHandler)
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
type ProjectId,
|
||||
type ProjectName,
|
||||
type UTCDateTime,
|
||||
} from './mockProjectManager'
|
||||
} from '../mock/projectManager'
|
||||
import pmSpec from './pm-openrpc.json' assert { type: 'json' }
|
||||
|
||||
export default function setup() {
|
||||
|
92
app/gui2/mock/index.ts
Normal file
92
app/gui2/mock/index.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { provideGuiConfig } from '@/providers/guiConfig'
|
||||
import { provideWidgetRegistry } from '@/providers/widgetRegistry'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { GraphDb, mockNode } from '@/stores/graph/graphDatabase'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { ComputedValueRegistry } from '@/stores/project/computedValueRegistry'
|
||||
import { MockTransport, MockWebSocket } from '@/util/net'
|
||||
import * as random from 'lib0/random'
|
||||
import { getActivePinia } from 'pinia'
|
||||
import type { ExprId } from 'shared/yjsModel'
|
||||
import { ref, type App } from 'vue'
|
||||
import { mockDataHandler, mockLSHandler } from './engine'
|
||||
export * as providers from './providers'
|
||||
export * as vue from './vue'
|
||||
|
||||
export function languageServer() {
|
||||
MockTransport.addMock('engine', mockLSHandler)
|
||||
}
|
||||
|
||||
export function dataServer() {
|
||||
MockWebSocket.addMock('data', mockDataHandler)
|
||||
}
|
||||
|
||||
export function guiConfig(app: App) {
|
||||
return provideGuiConfig._mock(
|
||||
ref({
|
||||
startup: {
|
||||
project: 'Mock Project',
|
||||
displayedProjectName: 'Mock Project',
|
||||
},
|
||||
engine: { rpcUrl: 'mock://engine', dataUrl: 'mock://data' },
|
||||
}),
|
||||
app,
|
||||
)
|
||||
}
|
||||
|
||||
export const computedValueRegistry = ComputedValueRegistry.Mock
|
||||
export const graphDb = GraphDb.Mock
|
||||
|
||||
export function widgetRegistry(app: App) {
|
||||
return widgetRegistry.withGraphDb(graphDb())(app)
|
||||
}
|
||||
|
||||
widgetRegistry.withGraphDb = function widgetRegistryWithGraphDb(graphDb: GraphDb) {
|
||||
return (app: App) => provideWidgetRegistry._mock([graphDb], app)
|
||||
}
|
||||
|
||||
export function graphStore() {
|
||||
return useGraphStore(getActivePinia())
|
||||
}
|
||||
|
||||
type ProjectStore = ReturnType<typeof projectStore>
|
||||
|
||||
export function projectStore() {
|
||||
const projectStore = useProjectStore(getActivePinia())
|
||||
const mod = projectStore.projectModel.createNewModule('Main.enso')
|
||||
mod.doc.ydoc.emit('load', [])
|
||||
mod.doc.contents.insert(0, 'main =\n')
|
||||
return projectStore
|
||||
}
|
||||
|
||||
/** The stores should be initialized in this order, as `graphStore` depends on `projectStore`. */
|
||||
export function projectStoreAndGraphStore() {
|
||||
return [projectStore(), graphStore()] satisfies [] | unknown[]
|
||||
}
|
||||
|
||||
export function newExprId() {
|
||||
return random.uuidv4() as ExprId
|
||||
}
|
||||
|
||||
/** This should only be used for supplying as initial props when testing.
|
||||
* Please do {@link GraphDb.mockNode} with a `useGraphStore().db` after mount. */
|
||||
export function node() {
|
||||
return mockNode()
|
||||
}
|
||||
|
||||
export function waitForMainModule(projectStore?: ProjectStore) {
|
||||
const definedProjectStore = projectStore ?? useProjectStore(getActivePinia())
|
||||
return new Promise((resolve, reject) => {
|
||||
const handle1 = window.setInterval(() => {
|
||||
if (definedProjectStore.module != null) {
|
||||
window.clearInterval(handle1)
|
||||
window.clearTimeout(handle2)
|
||||
resolve(definedProjectStore.module)
|
||||
}
|
||||
}, 10)
|
||||
const handle2 = window.setTimeout(() => {
|
||||
window.clearInterval(handle1)
|
||||
reject()
|
||||
}, 5_000)
|
||||
})
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
declare const projectIdBrand: unique symbol
|
||||
/** A name of a project. */
|
||||
/** An ID of a project. */
|
||||
export type ProjectId = string & { [projectIdBrand]: never }
|
||||
declare const projectNameBrand: unique symbol
|
||||
/** A name of a project. */
|
||||
export type ProjectName = string & { [projectNameBrand]: never }
|
||||
declare const utcDateTimeBrand: unique symbol
|
||||
/** A name of a project. */
|
||||
/** A UTC date and time. */
|
||||
export type UTCDateTime = string & { [utcDateTimeBrand]: never }
|
||||
|
||||
/** A value specifying the hostname and port of a socket. */
|
55
app/gui2/mock/providers.ts
Normal file
55
app/gui2/mock/providers.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { GraphSelection } from '@/providers/graphSelection'
|
||||
import type { GraphNavigator } from '../src/providers/graphNavigator'
|
||||
import { Rect } from '../src/util/data/rect'
|
||||
import { Vec2 } from '../src/util/data/vec2'
|
||||
|
||||
export const graphNavigator: GraphNavigator = {
|
||||
events: {} as any,
|
||||
clientToScenePos: () => Vec2.Zero,
|
||||
clientToSceneRect: () => Rect.Zero,
|
||||
panAndZoomTo: () => {},
|
||||
transform: '',
|
||||
prescaledTransform: '',
|
||||
translate: Vec2.Zero,
|
||||
scale: 1,
|
||||
sceneMousePos: Vec2.Zero,
|
||||
viewBox: '',
|
||||
viewport: Rect.Zero,
|
||||
}
|
||||
|
||||
export function graphNavigatorWith(modifications?: Partial<GraphNavigator>): GraphNavigator {
|
||||
return Object.assign({}, graphNavigator, modifications)
|
||||
}
|
||||
|
||||
export const graphSelection: GraphSelection = {
|
||||
events: {} as any,
|
||||
anchor: undefined,
|
||||
deselectAll: () => {},
|
||||
addHoveredPort: () => new Set(),
|
||||
removeHoveredPort: () => false,
|
||||
handleSelectionOf: () => {},
|
||||
hoveredNode: undefined,
|
||||
hoveredPort: undefined,
|
||||
isSelected: () => false,
|
||||
mouseHandler: () => false,
|
||||
selectAll: () => {},
|
||||
selected: new Set(),
|
||||
}
|
||||
|
||||
export function graphSelectionWith(modifications?: Partial<GraphSelection>): GraphSelection {
|
||||
return Object.assign({}, graphSelection, modifications)
|
||||
}
|
||||
|
||||
export const all = {
|
||||
'graph navigator': graphNavigator,
|
||||
'graph selection': graphSelection,
|
||||
}
|
||||
|
||||
export function allWith(
|
||||
modifications: Partial<{ [K in keyof typeof all]: Partial<(typeof all)[K]> }>,
|
||||
): typeof all {
|
||||
return {
|
||||
'graph navigator': graphNavigatorWith(modifications['graph navigator']),
|
||||
'graph selection': graphSelectionWith(modifications['graph selection']),
|
||||
}
|
||||
}
|
19
app/gui2/mock/vue.ts
Normal file
19
app/gui2/mock/vue.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { type VueWrapper } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
// It is currently not feasible to use generics here, as the type of the component's emits
|
||||
// is not exposed.
|
||||
export function handleEmit(wrapper: VueWrapper<any>, event: string, fn: (...args: any[]) => void) {
|
||||
let previousLength = 0
|
||||
return {
|
||||
async run() {
|
||||
const emitted = wrapper.emitted(event)
|
||||
if (!emitted) return
|
||||
for (let i = previousLength; i < emitted.length; i += 1) {
|
||||
fn(...emitted[i]!)
|
||||
}
|
||||
previousLength = emitted.length
|
||||
await nextTick()
|
||||
},
|
||||
}
|
||||
}
|
@ -1426,6 +1426,12 @@
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.9946 4L5.99463 0L5.99463 3.00002C2.68878 3.00924 0.0117188 5.69199 0.0117188 9C0.0117188 9.85105 0.188908 10.6607 0.508408 11.3941L2.25769 10.3842C2.0986 9.95285 2.01172 9.48657 2.01172 9C2.01172 6.79656 3.79335 5.00924 5.99463 5.00004V8L12.9946 4ZM11.6263 7.28463L13.3624 6.28229L15.0022 5.33554L16.0022 7.06759L13.9646 8.24403C13.9957 8.49164 14.0117 8.74395 14.0117 9C14.0117 12.3137 11.3254 15 8.01172 15H6.01172C5.05341 15 4.14757 14.7753 3.34401 14.3758L1 15.7291L0 13.9971L1.60404 13.071L3.40299 12.0323L11.6263 7.28463ZM5.74242 12.9911L11.9937 9.38188C11.8015 11.4119 10.0921 13 8.01172 13H6.01172C5.92122 13 5.83142 12.997 5.74242 12.9911Z" fill="currentColor" fill-opacity="0.6"/>
|
||||
</svg>
|
||||
</g>
|
||||
<g id="auto_replay" fill="none">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9829 4L6.98291 0V3.00002C3.67706 3.00924 1 5.69199 1 9C1 9.85105 1.17719 10.6607 1.49669 11.3941L3.24597 10.3842C3.08688 9.95285 3 9.48657 3 9C3 6.79656 4.78163 5.00924 6.98291 5.00004V8L13.9829 4ZM14.3506 6.28229L12.6146 7.28463C12.8617 7.80448 13 8.38609 13 9C13 11.2091 11.2091 13 9 13H7C6.00325 13 5.09164 12.6354 4.39127 12.0323L2.59232 13.071C3.68848 14.2572 5.25749 15 7 15H9C12.3137 15 15 12.3137 15 9C15 8.02176 14.7659 7.0982 14.3506 6.28229Z" fill="currentColor" fill-opacity="0.6"/>
|
||||
</svg>
|
||||
</g>
|
||||
<g id="expanded_node" fill="none">
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 149 KiB |
@ -2,12 +2,13 @@
|
||||
import ToggleIcon from '@/components/ToggleIcon.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isAutoEvaluationDisabled: boolean
|
||||
isOutputContextEnabledGlobally: boolean
|
||||
isOutputContextOverridden: boolean
|
||||
isDocsVisible: boolean
|
||||
isVisualizationVisible: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
'update:isAutoEvaluationDisabled': [isAutoEvaluationDisabled: boolean]
|
||||
'update:isOutputContextOverridden': [isOutputContextOverridden: boolean]
|
||||
'update:isDocsVisible': [isDocsVisible: boolean]
|
||||
'update:isVisualizationVisible': [isVisualizationVisible: boolean]
|
||||
}>()
|
||||
@ -16,11 +17,16 @@ const emit = defineEmits<{
|
||||
<template>
|
||||
<div class="CircularMenu">
|
||||
<ToggleIcon
|
||||
icon="no_auto_replay"
|
||||
class="icon-container button no-auto-evaluate-button"
|
||||
:alt="`${props.isAutoEvaluationDisabled ? 'Enable' : 'Disable'} auto-evaluation`"
|
||||
:modelValue="props.isAutoEvaluationDisabled"
|
||||
@update:modelValue="emit('update:isAutoEvaluationDisabled', $event)"
|
||||
:icon="props.isOutputContextEnabledGlobally ? 'no_auto_replay' : 'auto_replay'"
|
||||
class="icon-container button override-output-context-button"
|
||||
:class="{ 'output-context-overridden': props.isOutputContextOverridden }"
|
||||
:alt="`${
|
||||
props.isOutputContextEnabledGlobally != props.isOutputContextOverridden
|
||||
? 'Disable'
|
||||
: 'Enable'
|
||||
} output context`"
|
||||
:modelValue="props.isOutputContextOverridden"
|
||||
@update:modelValue="emit('update:isOutputContextOverridden', $event)"
|
||||
/>
|
||||
<ToggleIcon
|
||||
icon="docs"
|
||||
@ -70,12 +76,17 @@ const emit = defineEmits<{
|
||||
opacity: unset;
|
||||
}
|
||||
|
||||
.no-auto-evaluate-button {
|
||||
.override-output-context-button {
|
||||
position: absolute;
|
||||
left: 9px;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.output-context-overridden {
|
||||
opacity: 100%;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.docs-button {
|
||||
position: absolute;
|
||||
left: 18.54px;
|
||||
|
@ -5,7 +5,7 @@ import { useGraphStore, type Edge } from '@/stores/graph'
|
||||
import { assert } from '@/util/assert'
|
||||
import { Rect } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
import theme from '@/util/theme.json'
|
||||
import theme from '@/util/theme'
|
||||
import { clamp } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@ -79,7 +79,7 @@ const edgeColor = computed(
|
||||
)
|
||||
|
||||
/** The inputs to the edge state computation. */
|
||||
type Inputs = {
|
||||
interface Inputs {
|
||||
/** The width and height of the node that originates the edge, if any.
|
||||
* The edge may begin anywhere around the bottom half of the node. */
|
||||
sourceSize: Vec2
|
||||
@ -93,42 +93,12 @@ type Inputs = {
|
||||
targetPortTopDistanceInNode: number | undefined
|
||||
}
|
||||
|
||||
type JunctionPoints = {
|
||||
interface JunctionPoints {
|
||||
points: Vec2[]
|
||||
maxRadius: number
|
||||
targetAttachment: { target: Vec2; length: number } | undefined
|
||||
}
|
||||
|
||||
/** Minimum height above the target the edge must approach it from. */
|
||||
const MIN_APPROACH_HEIGHT = 32
|
||||
/** The preferred arc radius. */
|
||||
const RADIUS_BASE = 20
|
||||
|
||||
/** Constants configuring the 1-corner layout. */
|
||||
const SingleCorner = {
|
||||
/** The y-allocation for the radius will be the full available height minus this value. */
|
||||
RADIUS_Y_ADJUSTMENT: 29,
|
||||
/** The base x-allocation for the radius. */
|
||||
RADIUS_X_BASE: RADIUS_BASE,
|
||||
/** Proportion (0-1) of extra x-distance allocated to the radius. */
|
||||
RADIUS_X_FACTOR: 0.6,
|
||||
/** Distance for the line to continue under the node, to ensure that there isn't a gap. */
|
||||
SOURCE_NODE_OVERLAP: 4,
|
||||
/** Minimum arc radius at which we offset the source end to exit normal to the node's curve. */
|
||||
MINIMUM_TANGENT_EXIT_RADIUS: 2,
|
||||
} as const
|
||||
|
||||
/** Constants configuring the 3-corner layouts. */
|
||||
const ThreeCorner = {
|
||||
/** The maximum arc radius. */
|
||||
RADIUS_MAX: RADIUS_BASE,
|
||||
BACKWARD_EDGE_ARROW_THRESHOLD: 15,
|
||||
/** The maximum radius reduction (from [`RADIUS_BASE`]) to allow when choosing whether to use
|
||||
* the three-corner layout that doesn't use a backward corner.
|
||||
*/
|
||||
MAX_SQUEEZE: 2,
|
||||
} as const
|
||||
|
||||
function circleIntersection(x: number, r1: number, r2: number): number {
|
||||
let xNorm = clamp(x, -r2, r1)
|
||||
return Math.sqrt(r1 * r1 + r2 * r2 - xNorm * xNorm)
|
||||
@ -188,31 +158,26 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
: undefined
|
||||
|
||||
const targetWellBelowSource =
|
||||
inputs.targetOffset.y - (inputs.targetPortTopDistanceInNode ?? 0) >= MIN_APPROACH_HEIGHT
|
||||
inputs.targetOffset.y - (inputs.targetPortTopDistanceInNode ?? 0) >=
|
||||
theme.edge.min_approach_height
|
||||
const targetBelowSource = inputs.targetOffset.y > theme.node.height / 2.0
|
||||
const targetBeyondSource = Math.abs(inputs.targetOffset.x) > sourceMaxXOffset
|
||||
const horizontalRoomFor3Corners =
|
||||
targetBeyondSource &&
|
||||
Math.abs(inputs.targetOffset.x) - sourceMaxXOffset >=
|
||||
3.0 * (RADIUS_BASE - ThreeCorner.MAX_SQUEEZE)
|
||||
3.0 * (theme.edge.radius - theme.edge.three_corner.max_squeeze)
|
||||
if (targetWellBelowSource || (targetBelowSource && !horizontalRoomFor3Corners)) {
|
||||
const {
|
||||
RADIUS_Y_ADJUSTMENT,
|
||||
RADIUS_X_BASE,
|
||||
RADIUS_X_FACTOR,
|
||||
SOURCE_NODE_OVERLAP,
|
||||
MINIMUM_TANGENT_EXIT_RADIUS,
|
||||
} = SingleCorner
|
||||
const innerTheme = theme.edge.one_corner
|
||||
// The edge can originate anywhere along the length of the node.
|
||||
const sourceX = clamp(inputs.targetOffset.x, -sourceMaxXOffset, sourceMaxXOffset)
|
||||
const distanceX = Math.max(Math.abs(inputs.targetOffset.x) - halfSourceSize.x, 0)
|
||||
const radiusX = RADIUS_X_BASE + distanceX * RADIUS_X_FACTOR
|
||||
const radiusX = innerTheme.radius_x_base + distanceX * innerTheme.radius_x_factor
|
||||
// The minimum length of straight line there should be at the target end of the edge. This
|
||||
// is a fixed value, except it is reduced when the target is horizontally very close to the
|
||||
// edge of the source, so that very short edges are less sharp.
|
||||
const yAdjustment = Math.min(
|
||||
Math.abs(inputs.targetOffset.x) - halfSourceSize.x + RADIUS_Y_ADJUSTMENT / 2.0,
|
||||
RADIUS_Y_ADJUSTMENT,
|
||||
Math.abs(inputs.targetOffset.x) - halfSourceSize.x + innerTheme.radius_y_adjustment / 2.0,
|
||||
innerTheme.radius_y_adjustment,
|
||||
)
|
||||
const radiusY = Math.max(Math.abs(inputs.targetOffset.y) - yAdjustment, 0.0)
|
||||
const maxRadius = Math.min(radiusX, radiusY)
|
||||
@ -222,7 +187,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
Math.abs(inputs.targetOffset.y),
|
||||
)
|
||||
let sourceDY = 0
|
||||
if (naturalRadius > MINIMUM_TANGENT_EXIT_RADIUS) {
|
||||
if (naturalRadius > innerTheme.minimum_tangent_exit_radius) {
|
||||
// Offset the beginning of the edge so that it is normal to the curve of the source node
|
||||
// at the point that it exits the node.
|
||||
const radius = Math.min(naturalRadius, maxRadius)
|
||||
@ -232,7 +197,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
const intersection = circleIntersection(circleOffset, theme.node.corner_radius, radius)
|
||||
sourceDY = -Math.abs(radius - intersection)
|
||||
} else if (halfSourceSize.y != 0) {
|
||||
sourceDY = -SOURCE_NODE_OVERLAP + halfSourceSize.y
|
||||
sourceDY = -innerTheme.source_node_overlap + halfSourceSize.y
|
||||
}
|
||||
const source = new Vec2(sourceX, sourceDY)
|
||||
// The target attachment will extend as far toward the edge of the node as it can without
|
||||
@ -249,7 +214,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
targetAttachment: attachment,
|
||||
}
|
||||
} else {
|
||||
const { RADIUS_MAX } = ThreeCorner
|
||||
const radiusMax = theme.edge.three_corner.radius_max
|
||||
// The edge originates from either side of the node.
|
||||
const signX = Math.sign(inputs.targetOffset.x)
|
||||
const sourceX = Math.abs(sourceMaxXOffset) * signX
|
||||
@ -265,11 +230,11 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
// ╰─────╯────╯\
|
||||
// J0
|
||||
// Junctions (J0, J1) are in between source and target.
|
||||
const j0Dx = Math.min(2 * RADIUS_MAX, distanceX / 2)
|
||||
const j1Dx = Math.min(RADIUS_MAX, (distanceX - j0Dx) / 2)
|
||||
const j0Dx = Math.min(2 * radiusMax, distanceX / 2)
|
||||
const j1Dx = Math.min(radiusMax, (distanceX - j0Dx) / 2)
|
||||
j0x = sourceX + Math.abs(j0Dx) * signX
|
||||
j1x = j0x + Math.abs(j1Dx) * signX
|
||||
heightAdjustment = RADIUS_MAX - j1Dx
|
||||
heightAdjustment = radiusMax - j1Dx
|
||||
} else {
|
||||
// J1
|
||||
// /
|
||||
@ -278,16 +243,16 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
// ╭─────╮ │
|
||||
// ╰─────╯────╯
|
||||
// J0 > source; J0 > J1; J1 > target.
|
||||
j1x = inputs.targetOffset.x + Math.abs(RADIUS_MAX) * signX
|
||||
const j0BeyondSource = Math.abs(inputs.targetOffset.x) + RADIUS_MAX * 2
|
||||
const j0BeyondTarget = Math.abs(sourceX) + RADIUS_MAX
|
||||
j1x = inputs.targetOffset.x + Math.abs(radiusMax) * signX
|
||||
const j0BeyondSource = Math.abs(inputs.targetOffset.x) + radiusMax * 2
|
||||
const j0BeyondTarget = Math.abs(sourceX) + radiusMax
|
||||
j0x = Math.abs(Math.max(j0BeyondTarget, j0BeyondSource)) * signX
|
||||
heightAdjustment = 0
|
||||
}
|
||||
if (j0x == null || j1x == null || heightAdjustment == null) return null
|
||||
const attachmentHeight = inputs.targetPortTopDistanceInNode ?? 0
|
||||
const top = Math.min(
|
||||
inputs.targetOffset.y - MIN_APPROACH_HEIGHT - attachmentHeight + heightAdjustment,
|
||||
inputs.targetOffset.y - theme.edge.min_approach_height - attachmentHeight + heightAdjustment,
|
||||
0,
|
||||
)
|
||||
const source = new Vec2(sourceX, 0)
|
||||
@ -297,7 +262,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
||||
const attachmentTarget = attachment?.target ?? inputs.targetOffset
|
||||
return {
|
||||
points: [source, j0, j1, attachmentTarget],
|
||||
maxRadius: RADIUS_MAX,
|
||||
maxRadius: radiusMax,
|
||||
targetAttachment: attachment,
|
||||
}
|
||||
}
|
||||
@ -440,12 +405,12 @@ const activeStyle = computed(() => {
|
||||
|
||||
const baseStyle = computed(() => ({ '--node-base-color': edgeColor.value ?? 'tan' }))
|
||||
|
||||
function click(_e: PointerEvent) {
|
||||
if (base.value == null) return {}
|
||||
if (navigator?.sceneMousePos == null) return {}
|
||||
function click() {
|
||||
if (base.value == null) return
|
||||
if (navigator?.sceneMousePos == null) return
|
||||
const length = base.value.getTotalLength()
|
||||
let offset = lengthTo(navigator?.sceneMousePos)
|
||||
if (offset == null) return {}
|
||||
if (offset == null) return
|
||||
if (offset < length / 2) graph.disconnectTarget(props.edge)
|
||||
else graph.disconnectSource(props.edge)
|
||||
}
|
||||
@ -457,7 +422,7 @@ function arrowPosition(): Vec2 | undefined {
|
||||
const target = targetRect.value
|
||||
const source = sourceRect.value
|
||||
if (target == null || source == null) return
|
||||
if (target.pos.y > source.pos.y - ThreeCorner.BACKWARD_EDGE_ARROW_THRESHOLD) return
|
||||
if (target.pos.y > source.pos.y - theme.edge.three_corner.backward_edge_arrow_threshold) return
|
||||
if (points[1] == null) return
|
||||
return source.center().add(points[1])
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts">
|
||||
import { nodeEditBindings } from '@/bindings'
|
||||
import CircularMenu from '@/components/CircularMenu.vue'
|
||||
import GraphNodeError from '@/components/GraphEditor/GraphNodeError.vue'
|
||||
@ -12,12 +12,14 @@ import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||
import { injectGraphSelection } from '@/providers/graphSelection'
|
||||
import { useGraphStore, type Node } from '@/stores/graph'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { Prefixes } from '@/util/ast/prefixes'
|
||||
import type { Opt } from '@/util/data/opt'
|
||||
import { Rect } from '@/util/data/rect'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
import { displayedIconOf } from '@/util/getIconName'
|
||||
import { setIfUndefined } from 'lib0/map'
|
||||
import type { ContentRange, ExprId, VisualizationIdentifier } from 'shared/yjsModel'
|
||||
import { type ExprId, type VisualizationIdentifier } from 'shared/yjsModel'
|
||||
import { computed, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
const MAXIMUM_CLICK_LENGTH_MS = 300
|
||||
@ -25,6 +27,18 @@ const MAXIMUM_CLICK_DISTANCE_SQ = 50
|
||||
/** The width in pixels that is not the widget tree. This includes the icon, and padding. */
|
||||
const NODE_EXTRA_WIDTH_PX = 30
|
||||
|
||||
const prefixes = Prefixes.FromLines({
|
||||
enableOutputContext:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
disableOutputContext:
|
||||
'Standard.Base.Runtime.with_disabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
// Currently unused; included as PoC.
|
||||
skip: 'SKIP __',
|
||||
freeze: 'FREEZE __',
|
||||
})
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
node: Node
|
||||
edited: boolean
|
||||
@ -39,10 +53,8 @@ const emit = defineEmits<{
|
||||
outputPortClick: [portId: ExprId]
|
||||
outputPortDoubleClick: [portId: ExprId]
|
||||
doubleClick: []
|
||||
'update:content': [updates: [range: ContentRange, content: string][]]
|
||||
'update:edited': [cursorPosition: number]
|
||||
'update:rect': [rect: Rect]
|
||||
'update:selected': [selected: boolean]
|
||||
'update:visualizationId': [id: Opt<VisualizationIdentifier>]
|
||||
'update:visualizationRect': [rect: Rect | undefined]
|
||||
'update:visualizationVisible': [visible: boolean]
|
||||
@ -89,7 +101,6 @@ watch(isSelected, (selected) => {
|
||||
menuVisible.value = menuVisible.value && selected
|
||||
})
|
||||
|
||||
const isAutoEvaluationDisabled = ref(false)
|
||||
const isDocsVisible = ref(false)
|
||||
const isVisualizationVisible = computed(() => props.node.vis?.visible ?? false)
|
||||
|
||||
@ -145,6 +156,51 @@ const dragPointer = usePointer((pos, event, type) => {
|
||||
}
|
||||
})
|
||||
|
||||
const matches = computed(() => prefixes.extractMatches(props.node.rootSpan))
|
||||
const displayedExpression = computed(() => matches.value.innerExpr)
|
||||
|
||||
const isOutputContextOverridden = computed({
|
||||
get() {
|
||||
const override =
|
||||
matches.value.matches.enableOutputContext ?? matches.value.matches.disableOutputContext
|
||||
const overrideEnabled = matches.value.matches.enableOutputContext != null
|
||||
// An override is only counted as enabled if it is currently in effect. This requires:
|
||||
// - that an override exists
|
||||
if (!override) return false
|
||||
// - that it is setting the "enabled" value to a non-default value
|
||||
else if (overrideEnabled === projectStore.isOutputContextEnabled) return false
|
||||
// - and that it applies to the current execution context.
|
||||
else {
|
||||
const contextWithoutQuotes = override[0]?.code().replace(/^['"]|['"]$/g, '')
|
||||
return contextWithoutQuotes === projectStore.executionMode
|
||||
}
|
||||
},
|
||||
set(shouldOverride) {
|
||||
const module = projectStore.module
|
||||
if (!module) return
|
||||
const replacements = shouldOverride
|
||||
? [Ast.TextLiteral.new(projectStore.executionMode)]
|
||||
: undefined
|
||||
const edit = props.node.rootSpan.module.edit()
|
||||
const newAst = prefixes.modify(
|
||||
edit,
|
||||
props.node.rootSpan,
|
||||
projectStore.isOutputContextEnabled
|
||||
? {
|
||||
enableOutputContext: undefined,
|
||||
disableOutputContext: replacements,
|
||||
}
|
||||
: {
|
||||
enableOutputContext: replacements,
|
||||
disableOutputContext: undefined,
|
||||
},
|
||||
)
|
||||
graph.setNodeContent(props.node.rootSpan.astId, newAst.code())
|
||||
},
|
||||
})
|
||||
|
||||
// FIXME [sb]: https://github.com/enso-org/enso/issues/8442
|
||||
// This does not take into account `displayedExpression`.
|
||||
const expressionInfo = computed(() => graph.db.getExpressionInfo(nodeId.value))
|
||||
const outputPortLabel = computed(() => expressionInfo.value?.typename ?? 'Unknown')
|
||||
const executionState = computed(() => expressionInfo.value?.payload.type ?? 'Unknown')
|
||||
@ -223,6 +279,7 @@ const handleNodeClick = useDoubleClick(
|
||||
(e: MouseEvent) => nodeEditHandler(e),
|
||||
() => emit('doubleClick'),
|
||||
).handleClick
|
||||
|
||||
interface PortData {
|
||||
clipRange: [number, number]
|
||||
label: string
|
||||
@ -232,7 +289,7 @@ interface PortData {
|
||||
const outputPorts = computed((): PortData[] => {
|
||||
const ports = outputPortsSet.value
|
||||
const numPorts = ports.size
|
||||
return Array.from(ports, (portId, index) => {
|
||||
return Array.from(ports, (portId, index): PortData => {
|
||||
const labelIdent = numPorts > 1 ? graph.db.getOutputPortIdentifier(portId) + ': ' : ''
|
||||
const labelType =
|
||||
graph.db.getExpressionInfo(numPorts > 1 ? portId : nodeId.value)?.typename ?? 'Unknown'
|
||||
@ -296,8 +353,9 @@ function portGroupStyle(port: PortData) {
|
||||
</div>
|
||||
<CircularMenu
|
||||
v-if="menuVisible"
|
||||
v-model:isAutoEvaluationDisabled="isAutoEvaluationDisabled"
|
||||
v-model:isOutputContextOverridden="isOutputContextOverridden"
|
||||
v-model:isDocsVisible="isDocsVisible"
|
||||
:isOutputContextEnabledGlobally="projectStore.isOutputContextEnabled"
|
||||
:isVisualizationVisible="isVisualizationVisible"
|
||||
@update:isVisualizationVisible="emit('update:visualizationVisible', $event)"
|
||||
/>
|
||||
@ -320,7 +378,7 @@ function portGroupStyle(port: PortData) {
|
||||
<div class="node" @pointerdown="handleNodeClick" v-on="dragPointer.events">
|
||||
<SvgIcon class="icon grab-handle" :name="icon"></SvgIcon>
|
||||
<div ref="contentNode" class="widget-tree">
|
||||
<NodeWidgetTree :ast="node.rootSpan" />
|
||||
<NodeWidgetTree :ast="displayedExpression" />
|
||||
</div>
|
||||
</div>
|
||||
<GraphNodeError v-if="error" class="error" :error="error" />
|
||||
|
@ -9,7 +9,7 @@ import { useGraphStore } from '@/stores/graph'
|
||||
import { useProjectStore } from '@/stores/project'
|
||||
import type { Vec2 } from '@/util/data/vec2'
|
||||
import { stackItemsEqual } from 'shared/languageServerTypes'
|
||||
import type { ContentRange, ExprId } from 'shared/yjsModel'
|
||||
import type { ExprId } from 'shared/yjsModel'
|
||||
import { computed, toRaw } from 'vue'
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
@ -23,14 +23,6 @@ const emit = defineEmits<{
|
||||
nodeDoubleClick: [nodeId: ExprId]
|
||||
}>()
|
||||
|
||||
function updateNodeContent(id: ExprId, updates: [ContentRange, string][]) {
|
||||
graphStore.transact(() => {
|
||||
for (const [range, content] of updates) {
|
||||
graphStore.replaceNodeSubexpression(id, range, content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function nodeIsDragged(movedId: ExprId, offset: Vec2) {
|
||||
const scaledOffset = offset.scale(1 / (navigator?.scale ?? 1))
|
||||
dragging.startOrUpdate(movedId, scaledOffset)
|
||||
@ -42,7 +34,7 @@ function hoverNode(id: ExprId | undefined) {
|
||||
|
||||
const uploadingFiles = computed<[FileName, File][]>(() => {
|
||||
const currentStackItem = projectStore.executionContext.getStackTop()
|
||||
return [...projectStore.awareness.allUploads()].filter(([_name, file]) =>
|
||||
return [...projectStore.awareness.allUploads()].filter(([, file]) =>
|
||||
stackItemsEqual(file.stackItem, toRaw(currentStackItem)),
|
||||
)
|
||||
})
|
||||
@ -54,15 +46,13 @@ const uploadingFiles = computed<[FileName, File][]>(() => {
|
||||
:key="id"
|
||||
:node="node"
|
||||
:edited="id === graphStore.editedNodeInfo?.id"
|
||||
@delete="graphStore.deleteNode(id)"
|
||||
@pointerenter="hoverNode(id)"
|
||||
@pointerleave="hoverNode(undefined)"
|
||||
@dragging="nodeIsDragged(id, $event)"
|
||||
@draggingCommited="dragging.finishDrag()"
|
||||
@outputPortClick="graphStore.createEdgeFromOutput"
|
||||
@outputPortClick="graphStore.createEdgeFromOutput($event)"
|
||||
@outputPortDoubleClick="emit('nodeOutputPortDoubleClick', $event)"
|
||||
@doubleClick="emit('nodeDoubleClick', id)"
|
||||
@update:content="updateNodeContent(id, $event)"
|
||||
@update:edited="graphStore.setEditedNode(id, $event)"
|
||||
@update:rect="graphStore.updateNodeRect(id, $event)"
|
||||
@update:visualizationId="graphStore.setNodeVisualizationId(id, $event)"
|
||||
|
@ -20,7 +20,7 @@ const value = computed({
|
||||
},
|
||||
set(value) {
|
||||
const newCode = `[${value.map((item) => item.code()).join(', ')}]`
|
||||
graph.replaceExpressionContent(props.input.astId, newCode)
|
||||
graph.setExpressionContent(props.input.astId, newCode)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -14,7 +14,7 @@ const props = withDefaults(defineProps<{ icon: Icon; modelValue?: boolean }>(),
|
||||
modelValue: false,
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', toggledOn: boolean): void
|
||||
'update:modelValue': [toggledOn: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
@ -273,7 +273,8 @@ export function usePointer(
|
||||
trackedPointer.value = e.pointerId
|
||||
// This is mostly SAFE, as virtually all `Element`s also extend `GlobalEventHandlers`.
|
||||
trackedElement = e.currentTarget as Element & GlobalEventHandlers
|
||||
trackedElement.setPointerCapture(e.pointerId)
|
||||
// `setPointerCapture` is not defined in tests.
|
||||
trackedElement.setPointerCapture?.(e.pointerId)
|
||||
initialGrabPos = new Vec2(e.clientX, e.clientY)
|
||||
lastPos = initialGrabPos
|
||||
handler(computePosition(e, initialGrabPos, lastPos), e, 'start')
|
||||
|
@ -3,12 +3,14 @@ import { SuggestionDb, groupColorStyle, type Group } from '@/stores/suggestionDa
|
||||
import type { SuggestionEntry } from '@/stores/suggestionDatabase/entry'
|
||||
import { Ast, RawAst, RawAstExtended } from '@/util/ast'
|
||||
import { AliasAnalyzer } from '@/util/ast/aliasAnalysis'
|
||||
import { nodeFromAst } from '@/util/ast/node'
|
||||
import { colorFromString } from '@/util/colors'
|
||||
import { MappedKeyMap, MappedSet } from '@/util/containers'
|
||||
import { arrayEquals, byteArraysEqual, tryGetIndex } from '@/util/data/array'
|
||||
import type { Opt } from '@/util/data/opt'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
import { ReactiveDb, ReactiveIndex, ReactiveMapping } from '@/util/database/reactiveDb'
|
||||
import * as random from 'lib0/random'
|
||||
import * as set from 'lib0/set'
|
||||
import { methodPointerEquals, type MethodCall } from 'shared/languageServerTypes'
|
||||
import {
|
||||
@ -310,17 +312,19 @@ export class GraphDb {
|
||||
return new GraphDb(db, ref([]), registry)
|
||||
}
|
||||
|
||||
mockNode(binding: string, id: ExprId, code?: string) {
|
||||
const node = {
|
||||
mockNode(binding: string, id: ExprId, code?: string): Node {
|
||||
const pattern = Ast.parse(binding)
|
||||
const node: Node = {
|
||||
outerExprId: id,
|
||||
pattern: Ast.parse(binding),
|
||||
pattern,
|
||||
rootSpan: Ast.parse(code ?? '0'),
|
||||
position: Vec2.Zero,
|
||||
vis: undefined,
|
||||
}
|
||||
const bidingId = node.pattern.astId
|
||||
const bindingId = pattern.astId
|
||||
this.nodeIdToNode.set(id, node)
|
||||
this.bindings.bindings.set(bidingId, { identifier: binding, usages: new Set() })
|
||||
this.bindings.bindings.set(bindingId, { identifier: binding, usages: new Set() })
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,25 +336,16 @@ export interface Node {
|
||||
vis: Opt<VisualizationMetadata>
|
||||
}
|
||||
|
||||
function nodeFromAst(ast: Ast.Ast): Node {
|
||||
const common = {
|
||||
outerExprId: ast.exprId,
|
||||
/** This should only be used for supplying as initial props when testing.
|
||||
* Please do {@link GraphDb.mockNode} with a `useGraphStore().db` after mount. */
|
||||
export function mockNode(exprId?: ExprId): Node {
|
||||
return {
|
||||
outerExprId: exprId ?? (random.uuidv4() as ExprId),
|
||||
pattern: undefined,
|
||||
rootSpan: Ast.parse('0'),
|
||||
position: Vec2.Zero,
|
||||
vis: undefined,
|
||||
}
|
||||
if (ast instanceof Ast.Assignment && ast.expression) {
|
||||
return {
|
||||
...common,
|
||||
pattern: ast.pattern ?? undefined,
|
||||
rootSpan: ast.expression,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...common,
|
||||
pattern: undefined,
|
||||
rootSpan: ast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mathodCallEquals(a: MethodCall | undefined, b: MethodCall | undefined): boolean {
|
||||
|
@ -156,13 +156,13 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
}
|
||||
|
||||
function disconnectSource(edge: Edge) {
|
||||
if (edge.target)
|
||||
unconnectedEdge.value = { target: edge.target, disconnectedEdgeTarget: edge.target }
|
||||
if (!edge.target) return
|
||||
unconnectedEdge.value = { target: edge.target, disconnectedEdgeTarget: edge.target }
|
||||
}
|
||||
|
||||
function disconnectTarget(edge: Edge) {
|
||||
if (edge.source && edge.target)
|
||||
unconnectedEdge.value = { source: edge.source, disconnectedEdgeTarget: edge.target }
|
||||
if (!edge.source || !edge.target) return
|
||||
unconnectedEdge.value = { source: edge.source, disconnectedEdgeTarget: edge.target }
|
||||
}
|
||||
|
||||
function clearUnconnected() {
|
||||
@ -230,20 +230,6 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
proj.stopCapturingUndo()
|
||||
}
|
||||
|
||||
function replaceNodeSubexpression(
|
||||
nodeId: ExprId,
|
||||
range: ContentRange | undefined,
|
||||
content: string,
|
||||
) {
|
||||
const node = db.nodeIdToNode.get(nodeId)
|
||||
if (!node) return
|
||||
proj.module?.replaceExpressionContent(node.rootSpan.astId, content, range)
|
||||
}
|
||||
|
||||
function replaceExpressionContent(exprId: ExprId, content: string) {
|
||||
proj.module?.replaceExpressionContent(exprId, content)
|
||||
}
|
||||
|
||||
function setNodePosition(nodeId: ExprId, position: Vec2) {
|
||||
const node = db.nodeIdToNode.get(nodeId)
|
||||
if (!node) return
|
||||
@ -338,8 +324,6 @@ export const useGraphStore = defineStore('graph', () => {
|
||||
deleteNode,
|
||||
setNodeContent,
|
||||
setExpressionContent,
|
||||
replaceNodeSubexpression,
|
||||
replaceExpressionContent,
|
||||
setNodePosition,
|
||||
setNodeVisualizationId,
|
||||
setNodeVisualizationVisible,
|
||||
|
@ -85,6 +85,9 @@ async function initializeLsRpcConnection(
|
||||
error,
|
||||
)
|
||||
},
|
||||
}).catch((error) => {
|
||||
console.error('Error initializing Language Server RPC:', error)
|
||||
throw error
|
||||
})
|
||||
const contentRoots = initialization.contentRoots
|
||||
return { connection, contentRoots }
|
||||
@ -93,7 +96,10 @@ async function initializeLsRpcConnection(
|
||||
async function initializeDataConnection(clientId: Uuid, url: string) {
|
||||
const client = createWebsocketClient(url, { binaryType: 'arraybuffer', sendPings: false })
|
||||
const connection = new DataServer(client)
|
||||
await connection.initialize(clientId)
|
||||
await connection.initialize(clientId).catch((error) => {
|
||||
console.error('Error initializing data connection:', error)
|
||||
throw error
|
||||
})
|
||||
return connection
|
||||
}
|
||||
|
||||
@ -439,8 +445,20 @@ export const useProjectStore = defineStore('project', () => {
|
||||
const clientId = random.uuidv4() as Uuid
|
||||
const lsUrls = resolveLsUrl(config.value)
|
||||
const initializedConnection = initializeLsRpcConnection(clientId, lsUrls.rpcUrl)
|
||||
const lsRpcConnection = initializedConnection.then(({ connection }) => connection)
|
||||
const contentRoots = initializedConnection.then(({ contentRoots }) => contentRoots)
|
||||
const lsRpcConnection = initializedConnection.then(
|
||||
({ connection }) => connection,
|
||||
(error) => {
|
||||
console.error('Error getting Language Server connection:', error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
const contentRoots = initializedConnection.then(
|
||||
({ contentRoots }) => contentRoots,
|
||||
(error) => {
|
||||
console.error('Error getting content roots:', error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
const dataConnection = initializeDataConnection(clientId, lsUrls.dataUrl)
|
||||
|
||||
const rpcUrl = new URL(lsUrls.rpcUrl)
|
||||
@ -514,7 +532,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||
moduleDocGuid.value = guid
|
||||
}
|
||||
|
||||
projectModel.modules.observe((_) => tryReadDocGuid())
|
||||
projectModel.modules.observe(tryReadDocGuid)
|
||||
watchEffect(tryReadDocGuid)
|
||||
|
||||
const module = computedAsync(async () => {
|
||||
@ -537,8 +555,16 @@ export const useProjectStore = defineStore('project', () => {
|
||||
})
|
||||
}
|
||||
|
||||
const firstExecution = lsRpcConnection.then((lsRpc) =>
|
||||
nextEvent(lsRpc, 'executionContext/executionComplete'),
|
||||
const firstExecution = lsRpcConnection.then(
|
||||
(lsRpc) =>
|
||||
nextEvent(lsRpc, 'executionContext/executionComplete').catch((error) => {
|
||||
console.error('First execution failed:', error)
|
||||
throw error
|
||||
}),
|
||||
(error) => {
|
||||
console.error('Could not get Language Server for first execution:', error)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
const executionContext = createExecutionContextForMain()
|
||||
const visualizationDataRegistry = new VisualizationDataRegistry(executionContext, dataConnection)
|
||||
@ -603,6 +629,8 @@ export const useProjectStore = defineStore('project', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const isOutputContextEnabled = computed(() => executionMode.value === 'live')
|
||||
|
||||
function stopCapturingUndo() {
|
||||
module.value?.undoManager.stopCapturing()
|
||||
}
|
||||
@ -661,6 +689,7 @@ export const useProjectStore = defineStore('project', () => {
|
||||
lsRpcConnection: markRaw(lsRpcConnection),
|
||||
dataConnection: markRaw(dataConnection),
|
||||
useVisualizationData,
|
||||
isOutputContextEnabled,
|
||||
stopCapturingUndo,
|
||||
executionMode,
|
||||
dataflowErrors,
|
||||
|
@ -367,7 +367,7 @@ const parseCases = [
|
||||
]
|
||||
test.each(parseCases)('parse: %s', (testCase) => {
|
||||
const root = Ast.parse(testCase.code)
|
||||
expect(Ast.debug(root)).toEqual(testCase.tree)
|
||||
expect(Ast.tokenTree(root)).toEqual(testCase.tree)
|
||||
})
|
||||
|
||||
// TODO: Edits (#8367).
|
||||
|
109
app/gui2/src/util/ast/__tests__/match.test.ts
Normal file
109
app/gui2/src/util/ast/__tests__/match.test.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { Ast } from '@/util/ast'
|
||||
import { Pattern } from '@/util/ast/match'
|
||||
import { expect, test } from 'vitest'
|
||||
import { MutableModule } from '../abstract'
|
||||
|
||||
test.each([
|
||||
{ target: 'a.b', pattern: '__', extracted: ['a.b'] },
|
||||
{ target: 'a.b', pattern: 'a.__', extracted: ['b'] },
|
||||
{ target: 'a.b', pattern: '__.b', extracted: ['a'] },
|
||||
{ target: '1 + 1', pattern: '1 + 1', extracted: [] },
|
||||
{ target: '1 + 2', pattern: '1 + __', extracted: ['2'] },
|
||||
{ target: '1 + 2', pattern: '__ + 2', extracted: ['1'] },
|
||||
{ target: '1 + 2', pattern: '__ + __', extracted: ['1', '2'] },
|
||||
{ target: '1', pattern: '__', extracted: ['1'] },
|
||||
{ target: '1', pattern: '(__)' },
|
||||
{ target: '("a")', pattern: '(__)', extracted: ['"a"'] },
|
||||
{ target: '[1, "a", True]', pattern: '[1, "a", True]', extracted: [] },
|
||||
{ target: '[1, "a", True]', pattern: '[__, "a", __]', extracted: ['1', 'True'] },
|
||||
{ target: '[1, "a", True]', pattern: '[1, "a", False]' },
|
||||
{ target: '[1, "a", True]', pattern: '[1, "a", True, 2]' },
|
||||
{ target: '[1, "a", True]', pattern: '[1, "a", True, __]' },
|
||||
{ target: '(1)', pattern: '1' },
|
||||
{ target: '(1)', pattern: '__', extracted: ['(1)'] }, // True because `__` matches any expression.
|
||||
{ target: '1 + 1', pattern: '1 + 2' },
|
||||
{ target: '1 + 1', pattern: '"1" + 1' },
|
||||
{ target: '1 + 1', pattern: '1 + "1"' },
|
||||
{ target: '1 + 1 + 1', pattern: '(1 + 1) + 1' },
|
||||
{ target: '1 + 1 + 1', pattern: '1 + (1 + 1)' },
|
||||
{ target: '(1 + 1) + 1', pattern: '1 + 1 + 1' },
|
||||
{ target: '1 + (1 + 1)', pattern: '1 + 1 + 1' },
|
||||
{
|
||||
target:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context_name" <| node1.fn',
|
||||
pattern:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context_name" <| __',
|
||||
extracted: ['node1.fn'],
|
||||
},
|
||||
{
|
||||
target:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context_name" <| node1.fn',
|
||||
pattern:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
extracted: ['"current_context_name"', 'node1.fn'],
|
||||
},
|
||||
{
|
||||
target:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context_name" <| node1.fn',
|
||||
pattern: 'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output __',
|
||||
},
|
||||
{
|
||||
target:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context_name" <| node1.fn',
|
||||
pattern:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context#name" <| node1.fn',
|
||||
},
|
||||
{
|
||||
target:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context_name" <| node1.fn',
|
||||
pattern:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context_name" <| node2.fn',
|
||||
},
|
||||
{
|
||||
target:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output "current_context_name" <| a + b',
|
||||
pattern:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
extracted: ['"current_context_name"', 'a + b'],
|
||||
},
|
||||
{
|
||||
target:
|
||||
"Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output 'current_context_name' <| a + b",
|
||||
pattern:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
extracted: ["'current_context_name'", 'a + b'],
|
||||
},
|
||||
{
|
||||
target:
|
||||
"Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output 'current_context_name' <| a + b",
|
||||
pattern: 'Standard.Base.Runtime.__ Standard.Base.Runtime.Context.Output __ <| __',
|
||||
extracted: ['with_enabled_context', "'current_context_name'", 'a + b'],
|
||||
},
|
||||
])('`isMatch` and `extractMatches`', ({ target, pattern, extracted }) => {
|
||||
const targetAst = Ast.parseLine(target)
|
||||
const module = targetAst.module
|
||||
const patternAst = Pattern.parse(pattern)
|
||||
expect(
|
||||
patternAst.match(targetAst) !== undefined,
|
||||
`'${target}' has CST ${extracted != null ? '' : 'not '}matching '${pattern}'`,
|
||||
).toBe(extracted != null)
|
||||
expect(
|
||||
patternAst.match(targetAst)?.map((match) => module.get(match)?.code()),
|
||||
extracted != null
|
||||
? `'${target}' matches '${pattern}' with '__'s corresponding to ${JSON.stringify(extracted)
|
||||
.slice(1, -1)
|
||||
.replace(/"/g, "'")}`
|
||||
: `'${target}' does not match '${pattern}'`,
|
||||
).toStrictEqual(extracted)
|
||||
})
|
||||
|
||||
test.each([
|
||||
{ template: 'a __ c', source: 'b', result: 'a b c' },
|
||||
{ template: 'a . __ . c', source: 'b', result: 'a . b . c' },
|
||||
])('instantiate', ({ template, source, result }) => {
|
||||
const pattern = Pattern.parse(template)
|
||||
const edit = MutableModule.Transient()
|
||||
const intron = Ast.parse(source, edit)
|
||||
const instantiated = pattern.instantiate(edit, [intron.exprId])
|
||||
expect(instantiated.code(edit)).toBe(result)
|
||||
})
|
79
app/gui2/src/util/ast/__tests__/prefixes.test.ts
Normal file
79
app/gui2/src/util/ast/__tests__/prefixes.test.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { Ast } from '@/util/ast/abstract'
|
||||
import { Prefixes } from '@/util/ast/prefixes'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
test.each([
|
||||
{
|
||||
prefixes: {
|
||||
a: 'a + __',
|
||||
},
|
||||
modifications: {
|
||||
a: [],
|
||||
},
|
||||
source: 'b',
|
||||
target: 'a + b',
|
||||
},
|
||||
{
|
||||
prefixes: {
|
||||
a: 'a + __ + c',
|
||||
},
|
||||
modifications: {
|
||||
a: [],
|
||||
},
|
||||
source: 'd',
|
||||
target: 'a + d + c',
|
||||
},
|
||||
{
|
||||
prefixes: {
|
||||
a: '__+e + __',
|
||||
},
|
||||
modifications: {
|
||||
a: ['d'],
|
||||
},
|
||||
source: 'f',
|
||||
target: 'd+e + f',
|
||||
},
|
||||
{
|
||||
prefixes: {
|
||||
enable:
|
||||
'Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output __ <| __',
|
||||
},
|
||||
modifications: {
|
||||
enable: ["'foo'"],
|
||||
},
|
||||
source: 'a + b',
|
||||
target:
|
||||
"Standard.Base.Runtime.with_enabled_context Standard.Base.Runtime.Context.Output 'foo' <| a + b",
|
||||
},
|
||||
{
|
||||
prefixes: {
|
||||
a: '__+e + __',
|
||||
},
|
||||
modifications: {
|
||||
a: undefined,
|
||||
},
|
||||
source: 'd+e + f',
|
||||
target: 'f',
|
||||
},
|
||||
{
|
||||
prefixes: {
|
||||
a: '__+e + __',
|
||||
},
|
||||
modifications: {
|
||||
a: undefined,
|
||||
},
|
||||
source: 'd + e + f',
|
||||
target: 'f',
|
||||
},
|
||||
])('modify', ({ prefixes: lines, modifications, source, target }) => {
|
||||
const prefixes = Prefixes.FromLines(lines as any)
|
||||
const sourceAst = Ast.parseLine(source)
|
||||
const edit = sourceAst.module.edit()
|
||||
const modificationAsts = Object.fromEntries(
|
||||
Object.entries(modifications).map(([k, v]) => [
|
||||
k,
|
||||
v ? Array.from(v, (mod) => Ast.parse(mod, edit)) : undefined,
|
||||
]),
|
||||
)
|
||||
expect(prefixes.modify(edit, sourceAst, modificationAsts).code(edit)).toBe(target)
|
||||
})
|
10
app/gui2/src/util/ast/__tests__/text.ts
Normal file
10
app/gui2/src/util/ast/__tests__/text.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import * as astText from '@/util/ast/text'
|
||||
import { expect, test } from 'vitest'
|
||||
|
||||
test.each([
|
||||
{ string: 'abcdef_123', escaped: 'abcdef_123' },
|
||||
{ string: '\t\r\n\v"\'`', escaped: '\\t\\r\\n\\v\\"\\\'``' },
|
||||
{ string: '`foo` `bar` `baz`', escaped: '``foo`` ``bar`` ``baz``' },
|
||||
])('`escape`', ({ string, escaped }) => {
|
||||
expect(astText.escape(string)).toBe(escaped)
|
||||
})
|
@ -1,77 +1,92 @@
|
||||
import * as RawAst from '@/generated/ast'
|
||||
import { parseEnso } from '@/util/ast'
|
||||
import { AstExtended as RawAstExtended } from '@/util/ast/extended'
|
||||
import type { Opt } from '@/util/data/opt'
|
||||
import { Err, Ok, type Result } from '@/util/data/result'
|
||||
import type { LazyObject } from '@/util/parserSupport'
|
||||
import { unsafeEntries } from '@/util/record'
|
||||
import * as random from 'lib0/random'
|
||||
import { IdMap, type ExprId } from 'shared/yjsModel'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
interface Module {
|
||||
export interface Module {
|
||||
get raw(): MutableModule
|
||||
get(id: AstId): Ast | null
|
||||
getExtended(id: AstId): RawAstExtended | undefined
|
||||
edit(): MutableModule
|
||||
apply(module: MutableModule): void
|
||||
}
|
||||
|
||||
class Committed implements Module {
|
||||
nodes: Map<AstId, Ast>
|
||||
astExtended: Map<AstId, RawAstExtended>
|
||||
export class MutableModule implements Module {
|
||||
base: Module | null
|
||||
nodes: Map<AstId, Ast | null>
|
||||
astExtended: Map<AstId, RawAstExtended> | null
|
||||
|
||||
constructor() {
|
||||
this.nodes = reactive(new Map<AstId, Ast>())
|
||||
this.astExtended = reactive(new Map<AstId, RawAstExtended>())
|
||||
}
|
||||
|
||||
/** Returns a syntax node representing the current committed state of the given ID. */
|
||||
get(id: AstId): Ast | null {
|
||||
return this.nodes.get(id) ?? null
|
||||
}
|
||||
|
||||
getExtended(id: AstId): RawAstExtended | undefined {
|
||||
return this.astExtended.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
class Edit implements Module {
|
||||
base: Committed
|
||||
pending: Map<AstId, Ast | null>
|
||||
|
||||
constructor(base: Committed) {
|
||||
constructor(
|
||||
base: Module | null,
|
||||
nodes: Map<AstId, Ast | null>,
|
||||
astExtended: Map<AstId, RawAstExtended> | null,
|
||||
) {
|
||||
this.base = base
|
||||
this.pending = new Map()
|
||||
this.nodes = nodes
|
||||
this.astExtended = astExtended
|
||||
}
|
||||
|
||||
/** Replace all committed values with the state of the uncommitted parse. */
|
||||
commit() {
|
||||
for (const [id, ast] of this.pending.entries()) {
|
||||
static Observable(): MutableModule {
|
||||
const nodes = reactive(new Map<AstId, Ast>())
|
||||
const astExtended = reactive(new Map<AstId, RawAstExtended>())
|
||||
return new MutableModule(null, nodes, astExtended)
|
||||
}
|
||||
|
||||
static Transient(): MutableModule {
|
||||
const nodes = new Map<AstId, Ast>()
|
||||
return new MutableModule(null, nodes, null)
|
||||
}
|
||||
|
||||
edit(): MutableModule {
|
||||
const nodes = new Map<AstId, Ast>()
|
||||
return new MutableModule(this, nodes, null)
|
||||
}
|
||||
|
||||
get raw(): MutableModule {
|
||||
return this
|
||||
}
|
||||
|
||||
apply(edit: MutableModule) {
|
||||
for (const [id, ast] of edit.nodes.entries()) {
|
||||
if (ast === null) {
|
||||
this.base.nodes.delete(id)
|
||||
this.nodes.delete(id)
|
||||
} else {
|
||||
this.base.nodes.set(id, ast)
|
||||
this.nodes.set(id, ast)
|
||||
}
|
||||
}
|
||||
this.pending.clear()
|
||||
if (edit.astExtended) {
|
||||
if (this.astExtended)
|
||||
console.error(`Merging astExtended not implemented, probably doesn't make sense`)
|
||||
this.astExtended = edit.astExtended
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a syntax node representing the current committed state of the given ID. */
|
||||
get(id: AstId): Ast | null {
|
||||
const editedNode = this.pending.get(id)
|
||||
const editedNode = this.nodes.get(id)
|
||||
if (editedNode === null) {
|
||||
return null
|
||||
} else {
|
||||
return editedNode ?? this.base.get(id) ?? null
|
||||
return editedNode ?? this.base?.get(id) ?? null
|
||||
}
|
||||
}
|
||||
|
||||
set(id: AstId, ast: Ast) {
|
||||
this.pending.set(id, ast)
|
||||
this.nodes.set(id, ast)
|
||||
}
|
||||
|
||||
getExtended(id: AstId): RawAstExtended | undefined {
|
||||
return this.base.astExtended.get(id)
|
||||
return this.astExtended?.get(id) ?? this.base?.getExtended(id)
|
||||
}
|
||||
|
||||
delete(id: AstId) {
|
||||
this.pending.set(id, null)
|
||||
this.nodes.set(id, null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,13 +117,17 @@ function newTokenId(): TokenId {
|
||||
}
|
||||
|
||||
export class Token {
|
||||
private _code: string
|
||||
code_: string
|
||||
exprId: TokenId
|
||||
readonly _tokenType: RawAst.Token.Type
|
||||
constructor(code: string, id: TokenId, type: RawAst.Token.Type) {
|
||||
this._code = code
|
||||
tokenType_: RawAst.Token.Type | undefined
|
||||
constructor(code: string, id: TokenId, type: RawAst.Token.Type | undefined) {
|
||||
this.code_ = code
|
||||
this.exprId = id
|
||||
this._tokenType = type
|
||||
this.tokenType_ = type
|
||||
}
|
||||
|
||||
static new(code: string) {
|
||||
return new Token(code, newTokenId(), undefined)
|
||||
}
|
||||
|
||||
// Compatibility wrapper for `exprId`.
|
||||
@ -117,18 +136,19 @@ export class Token {
|
||||
}
|
||||
|
||||
code(): string {
|
||||
return this._code
|
||||
return this.code_
|
||||
}
|
||||
|
||||
typeName(): string {
|
||||
return RawAst.Token.typeNames[this._tokenType]!
|
||||
if (this.tokenType_) return RawAst.Token.typeNames[this.tokenType_]!
|
||||
else return 'Raw'
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Ast {
|
||||
readonly treeType: RawAst.Tree.Type | undefined
|
||||
_id: AstId
|
||||
readonly module: Committed
|
||||
readonly module: Module
|
||||
|
||||
// Deprecated interface for incremental integration of Ast API. Eliminate usages for #8367.
|
||||
get astExtended(): RawAstExtended | undefined {
|
||||
@ -140,16 +160,12 @@ export abstract class Ast {
|
||||
}
|
||||
|
||||
static deserialize(serialized: string): Ast {
|
||||
const parsed: any = JSON.parse(serialized)
|
||||
const nodes: NodeSpanMap = new Map(Object.entries(parsed.info.nodes))
|
||||
const tokens: TokenSpanMap = new Map(Object.entries(parsed.info.tokens))
|
||||
const module = new Committed()
|
||||
const edit = new Edit(module)
|
||||
const parsed: SerializedPrintedSource = JSON.parse(serialized)
|
||||
const nodes = new Map(unsafeEntries(parsed.info.nodes))
|
||||
const tokens = new Map(unsafeEntries(parsed.info.tokens))
|
||||
const module = MutableModule.Transient()
|
||||
const tree = parseEnso(parsed.code)
|
||||
type NodeSpanMap = Map<NodeKey, AstId[]>
|
||||
type TokenSpanMap = Map<TokenKey, TokenId>
|
||||
const root = abstract(edit, tree, parsed.code, { nodes, tokens }).node
|
||||
edit.commit()
|
||||
const root = abstract(module, tree, parsed.code, { nodes, tokens }).node
|
||||
return module.get(root)!
|
||||
}
|
||||
|
||||
@ -176,8 +192,8 @@ export abstract class Ast {
|
||||
/** Returns child subtrees, including information about the whitespace between them. */
|
||||
abstract concreteChildren(): IterableIterator<NodeChild>
|
||||
|
||||
code(): string {
|
||||
return print(this).code
|
||||
code(module?: Module): string {
|
||||
return print(this, module).code
|
||||
}
|
||||
|
||||
repr(): string {
|
||||
@ -189,17 +205,25 @@ export abstract class Ast {
|
||||
return RawAst.Tree.typeNames[this.treeType]
|
||||
}
|
||||
|
||||
static parse(source: PrintedSource | string): Ast {
|
||||
static parse(source: PrintedSource | string, inModule?: MutableModule): Ast {
|
||||
const code = typeof source === 'object' ? source.code : source
|
||||
const ids = typeof source === 'object' ? source.info : undefined
|
||||
const tree = parseEnso(code)
|
||||
const module = new Committed()
|
||||
const edit = new Edit(module)
|
||||
const newRoot = abstract(edit, tree, code, ids).node
|
||||
edit.commit()
|
||||
const module = inModule ?? MutableModule.Observable()
|
||||
const newRoot = abstract(module, tree, code, ids).node
|
||||
return module.get(newRoot)!
|
||||
}
|
||||
|
||||
static parseLine(source: PrintedSource | string): Ast {
|
||||
const ast = Ast.parse(source)
|
||||
if (ast instanceof BodyBlock) {
|
||||
const [expr] = ast.expressions()
|
||||
return expr instanceof Ast ? expr : ast
|
||||
} else {
|
||||
return ast
|
||||
}
|
||||
}
|
||||
|
||||
visitRecursive(visit: (node: Ast | Token) => void) {
|
||||
visit(this)
|
||||
for (const child of this.concreteChildren()) {
|
||||
@ -211,21 +235,23 @@ export abstract class Ast {
|
||||
}
|
||||
}
|
||||
|
||||
protected constructor(module: Edit, id?: AstId, treeType?: RawAst.Tree.Type) {
|
||||
this.module = module.base
|
||||
protected constructor(module: MutableModule, id?: AstId, treeType?: RawAst.Tree.Type) {
|
||||
this.module = module
|
||||
this._id = id ?? newNodeId()
|
||||
this.treeType = treeType
|
||||
module.set(this._id, this)
|
||||
}
|
||||
|
||||
_print(info: InfoMap, offset: number, indent: string): string {
|
||||
_print(
|
||||
info: InfoMap,
|
||||
offset: number,
|
||||
indent: string,
|
||||
moduleOverride?: Module | undefined,
|
||||
): string {
|
||||
const module_ = moduleOverride ?? this.module
|
||||
let code = ''
|
||||
for (const child of this.concreteChildren()) {
|
||||
if (
|
||||
child.node != null &&
|
||||
!(child.node instanceof Token) &&
|
||||
this.module.get(child.node) === null
|
||||
)
|
||||
if (child.node != null && !(child.node instanceof Token) && module_.get(child.node) === null)
|
||||
continue
|
||||
if (child.whitespace != null) {
|
||||
code += child.whitespace
|
||||
@ -237,7 +263,9 @@ export abstract class Ast {
|
||||
if (child.node instanceof Token) {
|
||||
code += child.node.code()
|
||||
} else {
|
||||
code += this.module.get(child.node)!._print(info, offset + code.length, indent)
|
||||
code += module_
|
||||
.get(child.node)!
|
||||
._print(info, offset + code.length, indent, moduleOverride)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -253,12 +281,12 @@ export abstract class Ast {
|
||||
}
|
||||
|
||||
export class App extends Ast {
|
||||
private _func: NodeChild<AstId>
|
||||
private _leftParen: NodeChild<Token> | null
|
||||
private _argumentName: NodeChild<Token> | null
|
||||
private _equals: NodeChild<Token> | null
|
||||
private _arg: NodeChild<AstId>
|
||||
private _rightParen: NodeChild<Token> | null
|
||||
_func: NodeChild<AstId>
|
||||
_leftParen: NodeChild<Token> | null
|
||||
_argumentName: NodeChild<Token> | null
|
||||
_equals: NodeChild<Token> | null
|
||||
_arg: NodeChild<AstId>
|
||||
_rightParen: NodeChild<Token> | null
|
||||
|
||||
get function(): Ast {
|
||||
return this.module.get(this._func.node)!
|
||||
@ -273,7 +301,7 @@ export class App extends Ast {
|
||||
}
|
||||
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
func: NodeChild<AstId>,
|
||||
leftParen: NodeChild<Token> | null,
|
||||
@ -303,8 +331,26 @@ export class App extends Ast {
|
||||
}
|
||||
}
|
||||
|
||||
const mapping: Record<string, string> = {
|
||||
'\b': '\\b',
|
||||
'\f': '\\f',
|
||||
'\n': '\\n',
|
||||
'\r': '\\r',
|
||||
'\t': '\\t',
|
||||
'\v': '\\v',
|
||||
'"': '\\"',
|
||||
"'": "\\'",
|
||||
'`': '``',
|
||||
}
|
||||
|
||||
/** Escape a string so it can be safely spliced into an interpolated (`''`) Enso string.
|
||||
* NOT USABLE to insert into raw strings. Does not include quotes. */
|
||||
export function escape(string: string) {
|
||||
return string.replace(/[\0\b\f\n\r\t\v"'`]/g, (match) => mapping[match]!)
|
||||
}
|
||||
|
||||
function positionalApp(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
func: NodeChild<AstId>,
|
||||
arg: NodeChild<AstId>,
|
||||
@ -313,7 +359,7 @@ function positionalApp(
|
||||
}
|
||||
|
||||
function namedApp(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
func: NodeChild<AstId>,
|
||||
leftParen: NodeChild<Token> | null,
|
||||
@ -336,8 +382,8 @@ function namedApp(
|
||||
}
|
||||
|
||||
export class UnaryOprApp extends Ast {
|
||||
private _opr: NodeChild<Token>
|
||||
private _arg: NodeChild<AstId> | null
|
||||
_opr: NodeChild<Token>
|
||||
_arg: NodeChild<AstId> | null
|
||||
|
||||
get operator(): Token {
|
||||
return this._opr.node
|
||||
@ -349,7 +395,7 @@ export class UnaryOprApp extends Ast {
|
||||
}
|
||||
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
opr: NodeChild<Token>,
|
||||
arg: NodeChild<AstId> | null,
|
||||
@ -367,7 +413,7 @@ export class UnaryOprApp extends Ast {
|
||||
|
||||
export class NegationOprApp extends UnaryOprApp {
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
opr: NodeChild<Token>,
|
||||
arg: NodeChild<AstId> | null,
|
||||
@ -377,9 +423,9 @@ export class NegationOprApp extends UnaryOprApp {
|
||||
}
|
||||
|
||||
export class OprApp extends Ast {
|
||||
protected _lhs: NodeChild<AstId> | null
|
||||
protected _opr: NodeChild[]
|
||||
protected _rhs: NodeChild<AstId> | null
|
||||
_lhs: NodeChild<AstId> | null
|
||||
_opr: NodeChild[]
|
||||
_rhs: NodeChild<AstId> | null
|
||||
|
||||
get lhs(): Ast | null {
|
||||
return this._lhs ? this.module.get(this._lhs.node) : null
|
||||
@ -399,7 +445,7 @@ export class OprApp extends Ast {
|
||||
}
|
||||
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
lhs: NodeChild<AstId> | null,
|
||||
opr: NodeChild[],
|
||||
@ -420,7 +466,7 @@ export class OprApp extends Ast {
|
||||
|
||||
export class PropertyAccess extends OprApp {
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
lhs: NodeChild<AstId> | null,
|
||||
opr: NodeChild<Token>,
|
||||
@ -432,9 +478,14 @@ export class PropertyAccess extends OprApp {
|
||||
|
||||
/** Representation without any type-specific accessors, for tree types that don't require any special treatment. */
|
||||
export class Generic extends Ast {
|
||||
private readonly _children: NodeChild[]
|
||||
_children: NodeChild[]
|
||||
|
||||
constructor(module: Edit, id?: AstId, children?: NodeChild[], treeType?: RawAst.Tree.Type) {
|
||||
constructor(
|
||||
module: MutableModule,
|
||||
id?: AstId,
|
||||
children?: NodeChild[],
|
||||
treeType?: RawAst.Tree.Type,
|
||||
) {
|
||||
super(module, id, treeType)
|
||||
this._children = children ?? []
|
||||
}
|
||||
@ -447,39 +498,39 @@ export class Generic extends Ast {
|
||||
type MultiSegmentAppSegment = { header: NodeChild<Token>; body: NodeChild<AstId> | null }
|
||||
|
||||
export class Import extends Ast {
|
||||
private polyglot_: MultiSegmentAppSegment | null
|
||||
private from_: MultiSegmentAppSegment | null
|
||||
private import__: MultiSegmentAppSegment
|
||||
private all_: NodeChild<Token> | null
|
||||
private as_: MultiSegmentAppSegment | null
|
||||
private hiding_: MultiSegmentAppSegment | null
|
||||
_polyglot: MultiSegmentAppSegment | null
|
||||
_from: MultiSegmentAppSegment | null
|
||||
_import: MultiSegmentAppSegment
|
||||
_all: NodeChild<Token> | null
|
||||
_as: MultiSegmentAppSegment | null
|
||||
_hiding: MultiSegmentAppSegment | null
|
||||
|
||||
get polyglot(): Ast | null {
|
||||
return this.polyglot_?.body ? this.module.get(this.polyglot_.body.node) : null
|
||||
return this._polyglot?.body ? this.module.get(this._polyglot.body.node) : null
|
||||
}
|
||||
|
||||
get from(): Ast | null {
|
||||
return this.from_?.body ? this.module.get(this.from_.body.node) : null
|
||||
return this._from?.body ? this.module.get(this._from.body.node) : null
|
||||
}
|
||||
|
||||
get import_(): Ast | null {
|
||||
return this.import__?.body ? this.module.get(this.import__.body.node) : null
|
||||
return this._import?.body ? this.module.get(this._import.body.node) : null
|
||||
}
|
||||
|
||||
get all(): Token | null {
|
||||
return this.all_?.node ?? null
|
||||
return this._all?.node ?? null
|
||||
}
|
||||
|
||||
get as(): Ast | null {
|
||||
return this.as_?.body ? this.module.get(this.as_.body.node) : null
|
||||
return this._as?.body ? this.module.get(this._as.body.node) : null
|
||||
}
|
||||
|
||||
get hiding(): Ast | null {
|
||||
return this.hiding_?.body ? this.module.get(this.hiding_.body.node) : null
|
||||
return this._hiding?.body ? this.module.get(this._hiding.body.node) : null
|
||||
}
|
||||
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
polyglot: MultiSegmentAppSegment | null,
|
||||
from: MultiSegmentAppSegment | null,
|
||||
@ -489,12 +540,12 @@ export class Import extends Ast {
|
||||
hiding: MultiSegmentAppSegment | null,
|
||||
) {
|
||||
super(module, id, RawAst.Tree.Type.Import)
|
||||
this.polyglot_ = polyglot
|
||||
this.from_ = from
|
||||
this.import__ = import_
|
||||
this.all_ = all
|
||||
this.as_ = as
|
||||
this.hiding_ = hiding
|
||||
this._polyglot = polyglot
|
||||
this._from = from
|
||||
this._import = import_
|
||||
this._all = all
|
||||
this._as = as
|
||||
this._hiding = hiding
|
||||
}
|
||||
|
||||
*concreteChildren(): IterableIterator<NodeChild> {
|
||||
@ -504,23 +555,23 @@ export class Import extends Ast {
|
||||
if (segment?.body) parts.push(segment.body)
|
||||
return parts
|
||||
}
|
||||
yield* segment(this.polyglot_)
|
||||
yield* segment(this.from_)
|
||||
yield* segment(this.import__)
|
||||
if (this.all_) yield this.all_
|
||||
yield* segment(this.as_)
|
||||
yield* segment(this.hiding_)
|
||||
yield* segment(this._polyglot)
|
||||
yield* segment(this._from)
|
||||
yield* segment(this._import)
|
||||
if (this._all) yield this._all
|
||||
yield* segment(this._as)
|
||||
yield* segment(this._hiding)
|
||||
}
|
||||
}
|
||||
|
||||
export class TextLiteral extends Ast {
|
||||
private readonly open_: NodeChild<Token> | null
|
||||
private readonly newline_: NodeChild<Token> | null
|
||||
private readonly elements_: NodeChild[]
|
||||
private readonly close_: NodeChild<Token> | null
|
||||
_open: NodeChild<Token> | null
|
||||
_newline: NodeChild<Token> | null
|
||||
_elements: NodeChild[]
|
||||
_close: NodeChild<Token> | null
|
||||
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
open: NodeChild<Token> | null,
|
||||
newline: NodeChild<Token> | null,
|
||||
@ -528,62 +579,70 @@ export class TextLiteral extends Ast {
|
||||
close: NodeChild<Token> | null,
|
||||
) {
|
||||
super(module, id, RawAst.Tree.Type.TextLiteral)
|
||||
this.open_ = open
|
||||
this.newline_ = newline
|
||||
this.elements_ = elements
|
||||
this.close_ = close
|
||||
this._open = open
|
||||
this._newline = newline
|
||||
this._elements = elements
|
||||
this._close = close
|
||||
}
|
||||
|
||||
static new(rawText: string): TextLiteral {
|
||||
const module = MutableModule.Transient()
|
||||
const text = Token.new(escape(rawText))
|
||||
return new TextLiteral(module, undefined, { node: Token.new("'") }, null, [{ node: text }], {
|
||||
node: Token.new("'"),
|
||||
})
|
||||
}
|
||||
|
||||
*concreteChildren(): IterableIterator<NodeChild> {
|
||||
if (this.open_) yield this.open_
|
||||
if (this.newline_) yield this.newline_
|
||||
yield* this.elements_
|
||||
if (this.close_) yield this.close_
|
||||
if (this._open) yield this._open
|
||||
if (this._newline) yield this._newline
|
||||
yield* this._elements
|
||||
if (this._close) yield this._close
|
||||
}
|
||||
}
|
||||
|
||||
export class Invalid extends Ast {
|
||||
private readonly expression_: NodeChild<AstId>
|
||||
_expression: NodeChild<AstId>
|
||||
|
||||
constructor(module: Edit, id: AstId | undefined, expression: NodeChild<AstId>) {
|
||||
constructor(module: MutableModule, id: AstId | undefined, expression: NodeChild<AstId>) {
|
||||
super(module, id, RawAst.Tree.Type.Invalid)
|
||||
this.expression_ = expression
|
||||
this._expression = expression
|
||||
}
|
||||
|
||||
*concreteChildren(): IterableIterator<NodeChild> {
|
||||
yield this.expression_
|
||||
yield this._expression
|
||||
}
|
||||
}
|
||||
|
||||
export class Group extends Ast {
|
||||
private readonly open_: NodeChild<Token> | undefined
|
||||
private readonly expression_: NodeChild<AstId> | null
|
||||
private readonly close_: NodeChild<Token> | undefined
|
||||
_open: NodeChild<Token> | undefined
|
||||
_expression: NodeChild<AstId> | null
|
||||
_close: NodeChild<Token> | undefined
|
||||
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
open: NodeChild<Token> | undefined,
|
||||
expression: NodeChild<AstId> | null,
|
||||
close: NodeChild<Token> | undefined,
|
||||
) {
|
||||
super(module, id, RawAst.Tree.Type.Group)
|
||||
this.open_ = open
|
||||
this.expression_ = expression
|
||||
this.close_ = close
|
||||
this._open = open
|
||||
this._expression = expression
|
||||
this._close = close
|
||||
}
|
||||
|
||||
*concreteChildren(): IterableIterator<NodeChild> {
|
||||
if (this.open_) yield this.open_
|
||||
if (this.expression_) yield this.expression_
|
||||
if (this.close_) yield this.close_
|
||||
if (this._open) yield this._open
|
||||
if (this._expression) yield this._expression
|
||||
if (this._close) yield this._close
|
||||
}
|
||||
}
|
||||
|
||||
export class NumericLiteral extends Ast {
|
||||
private readonly _tokens: NodeChild[]
|
||||
_tokens: NodeChild[]
|
||||
|
||||
constructor(module: Edit, id: AstId | undefined, tokens: NodeChild[]) {
|
||||
constructor(module: MutableModule, id: AstId | undefined, tokens: NodeChild[]) {
|
||||
super(module, id, RawAst.Tree.Type.Number)
|
||||
this._tokens = tokens ?? []
|
||||
}
|
||||
@ -594,11 +653,12 @@ export class NumericLiteral extends Ast {
|
||||
}
|
||||
|
||||
type FunctionArgument = NodeChild[]
|
||||
|
||||
export class Function extends Ast {
|
||||
private _name: NodeChild<AstId>
|
||||
private _args: FunctionArgument[]
|
||||
private _equals: NodeChild<Token>
|
||||
private _body: NodeChild<AstId> | null
|
||||
_name: NodeChild<AstId>
|
||||
_args: FunctionArgument[]
|
||||
_equals: NodeChild<Token>
|
||||
_body: NodeChild<AstId> | null
|
||||
// FIXME for #8367: This should not be nullable. If the `ExprId` has been deleted, the same placeholder logic should be applied
|
||||
// here and in `rawChildren` (and indirectly, `print`).
|
||||
get name(): Ast | null {
|
||||
@ -616,7 +676,7 @@ export class Function extends Ast {
|
||||
}
|
||||
}
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
name: NodeChild<AstId>,
|
||||
args: FunctionArgument[],
|
||||
@ -640,9 +700,9 @@ export class Function extends Ast {
|
||||
}
|
||||
|
||||
export class Assignment extends Ast {
|
||||
private _pattern: NodeChild<AstId>
|
||||
private _equals: NodeChild<Token>
|
||||
private _expression: NodeChild<AstId>
|
||||
_pattern: NodeChild<AstId>
|
||||
_equals: NodeChild<Token>
|
||||
_expression: NodeChild<AstId>
|
||||
get pattern(): Ast | null {
|
||||
return this.module.get(this._pattern.node)
|
||||
}
|
||||
@ -650,7 +710,7 @@ export class Assignment extends Ast {
|
||||
return this.module.get(this._expression.node)
|
||||
}
|
||||
constructor(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
id: AstId | undefined,
|
||||
pattern: NodeChild<AstId>,
|
||||
equals: NodeChild<Token>, // TODO: Edits (#8367): Allow undefined
|
||||
@ -682,12 +742,13 @@ export class Assignment extends Ast {
|
||||
}
|
||||
}
|
||||
|
||||
type BlockLine = {
|
||||
interface BlockLine {
|
||||
newline: NodeChild<Token> // Edits (#8367): Allow undefined
|
||||
expression: NodeChild<AstId> | null
|
||||
}
|
||||
|
||||
export class BodyBlock extends Ast {
|
||||
private _lines: BlockLine[];
|
||||
_lines: BlockLine[];
|
||||
|
||||
*expressions(): IterableIterator<Ast> {
|
||||
for (const line of this._lines) {
|
||||
@ -702,7 +763,7 @@ export class BodyBlock extends Ast {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(module: Edit, id: AstId | undefined, lines: BlockLine[]) {
|
||||
constructor(module: MutableModule, id: AstId | undefined, lines: BlockLine[]) {
|
||||
super(module, id, RawAst.Tree.Type.BodyBlock)
|
||||
this._lines = lines
|
||||
}
|
||||
@ -730,16 +791,24 @@ export class BodyBlock extends Ast {
|
||||
}
|
||||
}
|
||||
|
||||
_print(info: InfoMap, offset: number, indent: string): string {
|
||||
_print(
|
||||
info: InfoMap,
|
||||
offset: number,
|
||||
indent: string,
|
||||
moduleOverride?: Module | undefined,
|
||||
): string {
|
||||
const module_ = moduleOverride ?? this.module
|
||||
let code = ''
|
||||
for (const line of this._lines) {
|
||||
if (line.expression?.node != null && this.module.get(line.expression.node) === null) continue
|
||||
if (line.expression?.node != null && module_.get(line.expression.node) === null) continue
|
||||
code += line.newline?.whitespace ?? ''
|
||||
code += line.newline?.node.code() ?? '\n'
|
||||
if (line.expression !== null) {
|
||||
code += line.expression.whitespace ?? indent
|
||||
if (line.expression.node !== null) {
|
||||
code += this.module.get(line.expression.node)!._print(info, offset, indent + ' ')
|
||||
code += module_
|
||||
.get(line.expression.node)!
|
||||
._print(info, offset, indent + ' ', moduleOverride)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -757,7 +826,7 @@ export class BodyBlock extends Ast {
|
||||
export class Ident extends Ast {
|
||||
public token: NodeChild<Token>
|
||||
|
||||
constructor(module: Edit, id: AstId | undefined, token: NodeChild<Token>) {
|
||||
constructor(module: MutableModule, id: AstId | undefined, token: NodeChild<Token>) {
|
||||
super(module, id, RawAst.Tree.Type.Ident)
|
||||
this.token = token
|
||||
}
|
||||
@ -777,18 +846,16 @@ export class Ident extends Ast {
|
||||
export class Wildcard extends Ast {
|
||||
public token: NodeChild<Token>
|
||||
|
||||
constructor(module: Edit, id: AstId | undefined, token: NodeChild<Token>) {
|
||||
constructor(module: MutableModule, id: AstId | undefined, token: NodeChild<Token>) {
|
||||
super(module, id, RawAst.Tree.Type.Wildcard)
|
||||
this.token = token
|
||||
}
|
||||
|
||||
static new(): Wildcard {
|
||||
const module = new Committed()
|
||||
const edit = new Edit(module)
|
||||
const ast = new Wildcard(edit, undefined, {
|
||||
const module = MutableModule.Transient()
|
||||
const ast = new Wildcard(module, undefined, {
|
||||
node: new Token('_', newTokenId(), RawAst.Token.Type.Wildcard),
|
||||
})
|
||||
edit.commit()
|
||||
return ast
|
||||
}
|
||||
|
||||
@ -798,9 +865,9 @@ export class Wildcard extends Ast {
|
||||
}
|
||||
|
||||
export class RawCode extends Ast {
|
||||
private _code: NodeChild
|
||||
_code: NodeChild
|
||||
|
||||
constructor(module: Edit, id: AstId | undefined, code: NodeChild) {
|
||||
constructor(module: MutableModule, id: AstId | undefined, code: NodeChild) {
|
||||
super(module, id)
|
||||
this._code = code
|
||||
}
|
||||
@ -819,7 +886,7 @@ export class RawCode extends Ast {
|
||||
}
|
||||
|
||||
function abstract(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
tree: RawAst.Tree,
|
||||
code: string,
|
||||
info: InfoMap | undefined,
|
||||
@ -830,8 +897,9 @@ function abstract(
|
||||
const tokenIds = info?.tokens ?? new Map()
|
||||
return abstractTree(module, tree, code, nodesExpected, tokenIds)
|
||||
}
|
||||
|
||||
function abstractTree(
|
||||
module: Edit,
|
||||
module: MutableModule,
|
||||
tree: RawAst.Tree,
|
||||
code: string,
|
||||
nodesExpected: NodeSpanMap,
|
||||
@ -858,8 +926,9 @@ function abstractTree(
|
||||
const whitespaceEnd = whitespaceStart + tree.whitespaceLengthInCodeParsed
|
||||
const codeStart = whitespaceEnd
|
||||
const codeEnd = codeStart + tree.childrenLengthInCodeParsed
|
||||
// All node types use this value in the same way to obtain the ID type, but each node does so separately because we
|
||||
// must pop the tree's span from the ID map *after* processing children.
|
||||
// All node types use this value in the same way to obtain the ID type,
|
||||
// but each node does so separately because we must pop the tree's span from the ID map
|
||||
// *after* processing children.
|
||||
const spanKey = nodeKey(codeStart, codeEnd - codeStart, tree.type)
|
||||
let node: AstId
|
||||
switch (tree.type) {
|
||||
@ -1030,54 +1099,68 @@ function abstractToken(
|
||||
return { whitespace, node }
|
||||
}
|
||||
|
||||
type NodeKey = string
|
||||
type TokenKey = string
|
||||
function nodeKey(start: number, length: number, type: RawAst.Tree.Type | undefined): NodeKey {
|
||||
declare const nodeKeyBrand: unique symbol
|
||||
type NodeKey = string & { [nodeKeyBrand]: never }
|
||||
declare const tokenKeyBrand: unique symbol
|
||||
type TokenKey = string & { [tokenKeyBrand]: never }
|
||||
function nodeKey(start: number, length: number, type: Opt<RawAst.Tree.Type>): NodeKey {
|
||||
const type_ = type?.toString() ?? '?'
|
||||
return `${start}:${length}:${type_}`
|
||||
return `${start}:${length}:${type_}` as NodeKey
|
||||
}
|
||||
function tokenKey(start: number, length: number): TokenKey {
|
||||
return `${start}:${length}`
|
||||
return `${start}:${length}` as TokenKey
|
||||
}
|
||||
|
||||
interface SerializedInfoMap {
|
||||
nodes: Record<NodeKey, AstId[]>
|
||||
tokens: Record<TokenKey, TokenId>
|
||||
}
|
||||
|
||||
interface SerializedPrintedSource {
|
||||
info: SerializedInfoMap
|
||||
code: string
|
||||
}
|
||||
|
||||
type NodeSpanMap = Map<NodeKey, AstId[]>
|
||||
type TokenSpanMap = Map<TokenKey, TokenId>
|
||||
export type InfoMap = {
|
||||
|
||||
export interface InfoMap {
|
||||
nodes: NodeSpanMap
|
||||
tokens: TokenSpanMap
|
||||
}
|
||||
|
||||
type PrintedSource = {
|
||||
interface PrintedSource {
|
||||
info: InfoMap
|
||||
code: string
|
||||
}
|
||||
|
||||
/** Return stringification with associated ID map. This is only exported for testing. */
|
||||
export function print(ast: Ast): PrintedSource {
|
||||
export function print(ast: Ast, module?: Module | undefined): PrintedSource {
|
||||
const info: InfoMap = {
|
||||
nodes: new Map(),
|
||||
tokens: new Map(),
|
||||
}
|
||||
const code = ast._print(info, 0, '')
|
||||
const code = ast._print(info, 0, '', module)
|
||||
return { info, code }
|
||||
}
|
||||
|
||||
type DebugTree = (DebugTree | string)[]
|
||||
export function debug(root: Ast, universe?: Map<AstId, Ast>): DebugTree {
|
||||
export type TokenTree = (TokenTree | string)[]
|
||||
|
||||
export function tokenTree(root: Ast): TokenTree {
|
||||
const module = root.module
|
||||
return Array.from(root.concreteChildren(), (child) => {
|
||||
if (child.node instanceof Token) {
|
||||
return child.node.code()
|
||||
} else {
|
||||
const node = module.get(child.node)
|
||||
return node ? debug(node, universe) : '<missing>'
|
||||
return node ? tokenTree(node) : '<missing>'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME: We should use alias analysis to handle ambiguous names correctly.
|
||||
export function findModuleMethod(module: Committed, name: string): Function | null {
|
||||
for (const node of module.nodes.values()) {
|
||||
export function findModuleMethod(module: Module, name: string): Function | null {
|
||||
for (const node of module.raw.nodes.values()) {
|
||||
if (node instanceof Function) {
|
||||
if (node.name && node.name.code() === name) {
|
||||
return node
|
||||
@ -1087,7 +1170,7 @@ export function findModuleMethod(module: Committed, name: string): Function | nu
|
||||
return null
|
||||
}
|
||||
|
||||
export function functionBlock(module: Committed, name: string): BodyBlock | null {
|
||||
export function functionBlock(module: Module, name: string): BodyBlock | null {
|
||||
const method = findModuleMethod(module, name)
|
||||
if (!method || !(method.body instanceof BodyBlock)) return null
|
||||
return method.body
|
||||
@ -1120,7 +1203,7 @@ export function parseTransitional(code: string, idMap: IdMap): Ast {
|
||||
idMap.finishAndSynchronize()
|
||||
const nodes = new Map<NodeKey, AstId[]>()
|
||||
const tokens = new Map<TokenKey, TokenId>()
|
||||
const astExtended = new Map<AstId, RawAstExtended>()
|
||||
const astExtended = reactive(new Map<AstId, RawAstExtended>())
|
||||
legacyAst.visitRecursive((nodeOrToken: RawAstExtended<RawAst.Tree | RawAst.Token>) => {
|
||||
const start = nodeOrToken.span()[0]
|
||||
const length = nodeOrToken.span()[1] - nodeOrToken.span()[0]
|
||||
@ -1154,13 +1237,12 @@ export function parseTransitional(code: string, idMap: IdMap): Ast {
|
||||
return true
|
||||
})
|
||||
const newRoot = Ast.parse({ info: { nodes, tokens }, code })
|
||||
newRoot.module.astExtended = astExtended
|
||||
newRoot.module.raw.astExtended = astExtended
|
||||
return newRoot
|
||||
}
|
||||
|
||||
export function parse(source: PrintedSource | string): Ast {
|
||||
return Ast.parse(source)
|
||||
}
|
||||
export const parse = Ast.parse
|
||||
export const parseLine = Ast.parseLine
|
||||
|
||||
export function deserialize(serialized: string): Ast {
|
||||
return Ast.deserialize(serialized)
|
||||
|
98
app/gui2/src/util/ast/match.ts
Normal file
98
app/gui2/src/util/ast/match.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { Ast } from '@/util/ast'
|
||||
import { MutableModule } from '@/util/ast/abstract'
|
||||
|
||||
export class Pattern {
|
||||
private readonly tokenTree: Ast.TokenTree
|
||||
private readonly template: string
|
||||
private readonly placeholder: string
|
||||
|
||||
constructor(template: string, placeholder: string) {
|
||||
this.tokenTree = Ast.tokenTree(Ast.parseLine(template))
|
||||
this.template = template
|
||||
this.placeholder = placeholder
|
||||
}
|
||||
|
||||
/** Parse an expression template in which a specified identifier (by default `__`)
|
||||
* may match any arbitrary subtree. */
|
||||
static parse(template: string, placeholder: string = '__'): Pattern {
|
||||
return new Pattern(template, placeholder)
|
||||
}
|
||||
|
||||
/** If the given expression matches the pattern, return the subtrees that matched the holes in the pattern. */
|
||||
match(target: Ast.Ast): Ast.AstId[] | undefined {
|
||||
const extracted: Ast.AstId[] = []
|
||||
if (this.tokenTree.length === 1 && this.tokenTree[0] === this.placeholder) {
|
||||
return [target.exprId]
|
||||
}
|
||||
if (
|
||||
isMatch_(
|
||||
this.tokenTree,
|
||||
target.concreteChildren(),
|
||||
target.module,
|
||||
this.placeholder,
|
||||
extracted,
|
||||
)
|
||||
) {
|
||||
return extracted
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a new concrete example of the pattern, with the placeholders replaced with the given subtrees.
|
||||
* The subtree IDs provided must be accessible in the `edit` module. */
|
||||
instantiate(edit: MutableModule, subtrees: Ast.AstId[]): Ast.Ast {
|
||||
const ast = Ast.parse(this.template, edit)
|
||||
for (const matched of placeholders(ast, this.placeholder)) {
|
||||
const replacement = subtrees.shift()
|
||||
if (replacement === undefined) break
|
||||
matched.node = replacement
|
||||
}
|
||||
return ast
|
||||
}
|
||||
}
|
||||
|
||||
function isMatch_(
|
||||
pattern: Ast.TokenTree,
|
||||
target: Iterator<Ast.NodeChild>,
|
||||
module: Ast.Module,
|
||||
placeholder: string,
|
||||
extracted: Ast.AstId[],
|
||||
): boolean {
|
||||
for (const subpattern of pattern) {
|
||||
const next = target.next()
|
||||
if (next.done) return false
|
||||
const astOrToken = next.value.node
|
||||
const isPlaceholder = typeof subpattern !== 'string' && subpattern[0] === placeholder
|
||||
if (typeof subpattern === 'string') {
|
||||
if (!(astOrToken instanceof Ast.Token) || astOrToken.code() !== subpattern) {
|
||||
return false
|
||||
}
|
||||
} else if (astOrToken instanceof Ast.Token) {
|
||||
return false
|
||||
} else if (isPlaceholder) {
|
||||
extracted.push(astOrToken)
|
||||
} else {
|
||||
const ast = module.get(astOrToken)
|
||||
if (!ast) return false
|
||||
if (!isMatch_(subpattern, ast.concreteChildren(), module, placeholder, extracted))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function placeholders(ast: Ast.Ast, placeholder: string, outIn?: Ast.NodeChild<Ast.AstId>[]) {
|
||||
const out = outIn ?? []
|
||||
for (const child of ast.concreteChildren()) {
|
||||
if (!(child.node instanceof Ast.Token)) {
|
||||
// The type of `child` has been determined by checking the type of `child.node`
|
||||
const nodeChild = child as Ast.NodeChild<Ast.AstId>
|
||||
const subtree = ast.module.get(child.node)!
|
||||
if (subtree instanceof Ast.Ident && subtree.code() === placeholder) {
|
||||
out.push(nodeChild)
|
||||
} else {
|
||||
placeholders(subtree, placeholder, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
23
app/gui2/src/util/ast/node.ts
Normal file
23
app/gui2/src/util/ast/node.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { Node } from '@/stores/graph'
|
||||
import { Ast } from '@/util/ast'
|
||||
import { Vec2 } from '@/util/data/vec2'
|
||||
|
||||
export function nodeFromAst(ast: Ast.Ast): Node {
|
||||
if (ast instanceof Ast.Assignment) {
|
||||
return {
|
||||
outerExprId: ast.astId,
|
||||
pattern: ast.pattern ?? undefined,
|
||||
rootSpan: ast.expression ?? ast,
|
||||
position: Vec2.Zero,
|
||||
vis: undefined,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
outerExprId: ast.astId,
|
||||
pattern: undefined,
|
||||
rootSpan: ast,
|
||||
position: Vec2.Zero,
|
||||
vis: undefined,
|
||||
}
|
||||
}
|
||||
}
|
60
app/gui2/src/util/ast/prefixes.ts
Normal file
60
app/gui2/src/util/ast/prefixes.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Ast } from '@/util/ast'
|
||||
import { Pattern } from '@/util/ast/match'
|
||||
import { unsafeKeys } from '@/util/record'
|
||||
|
||||
type Matches<T> = Record<keyof T, Ast.Ast[] | undefined>
|
||||
|
||||
interface MatchResult<T> {
|
||||
innerExpr: Ast.Ast
|
||||
matches: Record<keyof T, Ast.Ast[] | undefined>
|
||||
}
|
||||
|
||||
export class Prefixes<T extends Record<keyof T, Pattern>> {
|
||||
constructor(
|
||||
/** Note that these are checked in order of definition. */
|
||||
public prefixes: T,
|
||||
) {}
|
||||
|
||||
/** Note that these are checked in order of definition. */
|
||||
static FromLines<T>(lines: Record<keyof T, string>) {
|
||||
return new Prefixes(
|
||||
Object.fromEntries(
|
||||
Object.entries<string>(lines).map(([name, line]) => [name, Pattern.parse(line)]),
|
||||
) as Record<keyof T, Pattern>,
|
||||
)
|
||||
}
|
||||
|
||||
extractMatches(expression: Ast.Ast): MatchResult<T> {
|
||||
const matches = Object.fromEntries(
|
||||
Object.entries<Pattern>(this.prefixes).map(([name, pattern]) => {
|
||||
const matchIds = pattern.match(expression)
|
||||
const matches = matchIds
|
||||
? Array.from(matchIds, (id) => expression.module.get(id)!)
|
||||
: undefined
|
||||
const lastMatch = matches != null ? matches[matches.length - 1] : undefined
|
||||
if (lastMatch) expression = lastMatch
|
||||
return [name, matches]
|
||||
}),
|
||||
) as Matches<T>
|
||||
return { matches, innerExpr: expression }
|
||||
}
|
||||
|
||||
modify(
|
||||
edit: Ast.MutableModule,
|
||||
expression: Ast.Ast,
|
||||
replacements: Partial<Record<keyof T, Ast.Ast[] | undefined>>,
|
||||
) {
|
||||
const matches = this.extractMatches(expression)
|
||||
let result = matches.innerExpr
|
||||
for (const key of unsafeKeys(this.prefixes).reverse()) {
|
||||
if (key in replacements && !replacements[key]) continue
|
||||
const replacement: Ast.Ast[] | undefined = replacements[key] ?? matches.matches[key]
|
||||
if (!replacement) continue
|
||||
const pattern = this.prefixes[key]
|
||||
const parts = [...replacement, result]
|
||||
const partsIds = Array.from(parts, (ast) => ast.exprId)
|
||||
result = pattern.instantiate(edit, partsIds)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
@ -36,15 +36,27 @@ export function* chain<T>(...iters: Iterable<T>[]) {
|
||||
export function* zip<T, U>(left: Iterable<T>, right: Iterable<U>): Generator<[T, U]> {
|
||||
const leftIterator = left[Symbol.iterator]()
|
||||
const rightIterator = right[Symbol.iterator]()
|
||||
|
||||
while (true) {
|
||||
const leftResult = leftIterator.next()
|
||||
const rightResult = rightIterator.next()
|
||||
|
||||
if (leftResult.done || rightResult.done) {
|
||||
break
|
||||
}
|
||||
|
||||
if (leftResult.done || rightResult.done) break
|
||||
yield [leftResult.value, rightResult.value]
|
||||
}
|
||||
}
|
||||
|
||||
export function* zipLongest<T, U>(
|
||||
left: Iterable<T>,
|
||||
right: Iterable<U>,
|
||||
): Generator<[T | undefined, U | undefined]> {
|
||||
const leftIterator = left[Symbol.iterator]()
|
||||
const rightIterator = right[Symbol.iterator]()
|
||||
while (true) {
|
||||
const leftResult = leftIterator.next()
|
||||
const rightResult = rightIterator.next()
|
||||
if (leftResult.done && rightResult.done) break
|
||||
yield [
|
||||
leftResult.done ? undefined : leftResult.value,
|
||||
rightResult.done ? undefined : rightResult.value,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +211,16 @@ export class AsyncQueue<State> {
|
||||
if (task == null) return
|
||||
this.taskRunning = true
|
||||
this.lastTask = this.lastTask
|
||||
.then((state) => task(state))
|
||||
.then(
|
||||
(state) => task(state),
|
||||
(error) => {
|
||||
console.error(
|
||||
"AsyncQueue failed to run task '" + task.toString() + "' with error:",
|
||||
error,
|
||||
)
|
||||
throw error
|
||||
},
|
||||
)
|
||||
.finally(() => {
|
||||
this.taskRunning = false
|
||||
this.run()
|
||||
|
9
app/gui2/src/util/record.ts
Normal file
9
app/gui2/src/util/record.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/** Unsafe whe the record can have extra keys which are not in `K`. */
|
||||
export function unsafeEntries<K extends PropertyKey, V>(obj: Record<K, V>): [K, V][] {
|
||||
return Object.entries(obj) as any
|
||||
}
|
||||
|
||||
/** Unsafe whe the record can have extra keys which are not in `K`. */
|
||||
export function unsafeKeys<K extends PropertyKey>(obj: Record<K, unknown>): K[] {
|
||||
return Object.keys(obj) as any
|
||||
}
|
@ -4,5 +4,21 @@
|
||||
"corner_radius": 16,
|
||||
"vertical_gap": 32,
|
||||
"horizontal_gap": 32
|
||||
},
|
||||
"edge": {
|
||||
"min_approach_height": 32,
|
||||
"radius": 20,
|
||||
"one_corner": {
|
||||
"radius_y_adjustment": 29,
|
||||
"radius_x_base": 20,
|
||||
"radius_x_factor": 0.6,
|
||||
"source_node_overlap": 4,
|
||||
"minimum_tangent_exit_radius": 2
|
||||
},
|
||||
"three_corner": {
|
||||
"radius_max": 20,
|
||||
"backward_edge_arrow_threshold": 15,
|
||||
"max_squeeze": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
60
app/gui2/src/util/theme.ts
Normal file
60
app/gui2/src/util/theme.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import originalTheme from '@/util/theme.json'
|
||||
|
||||
const theme: Theme = originalTheme
|
||||
export default theme
|
||||
|
||||
export interface Theme {
|
||||
/** Configuration for node rendering. */
|
||||
node: NodeTheme
|
||||
/** Configuration for edge rendering. */
|
||||
edge: EdgeTheme
|
||||
}
|
||||
|
||||
/** Configuration for node rendering. */
|
||||
export interface NodeTheme {
|
||||
/** The default height of a node. */
|
||||
height: number
|
||||
/** The maximum corner radius of a node. If the node is shorter than `2 * corner_radius`,
|
||||
* the corner radius will be half of the node's height instead. */
|
||||
corner_radius: number
|
||||
/** The vertical gap between nodes in automatic layout. */
|
||||
vertical_gap: number
|
||||
/** The horizontal gap between nodes in automatic layout. */
|
||||
horizontal_gap: number
|
||||
}
|
||||
|
||||
/** Configuration for edge rendering. */
|
||||
export interface EdgeTheme {
|
||||
/** Minimum height above the target the edge must approach it from. */
|
||||
min_approach_height: number
|
||||
/** The preferred arc radius for corners, when an edge changes direction. */
|
||||
radius: number
|
||||
/** Configuration for edges that change direction once. */
|
||||
one_corner: EdgeOneCornerTheme
|
||||
/** Configuration for edges with change direction three times. */
|
||||
three_corner: EdgeThreeCornerTheme
|
||||
}
|
||||
|
||||
/** Configuration for edges that change direction once. */
|
||||
export interface EdgeOneCornerTheme {
|
||||
/** The y-allocation for the radius will be the full available height minus this value. */
|
||||
radius_y_adjustment: number
|
||||
/** The base x-allocation for the radius. */
|
||||
radius_x_base: number
|
||||
/** Proportion (0-1) of extra x-distance allocated to the radius. */
|
||||
radius_x_factor: number
|
||||
/** Distance for the line to continue under the node, to ensure that there isn't a gap. */
|
||||
source_node_overlap: number
|
||||
/** Minimum arc radius at which we offset the source end to exit normal to the node's curve. */
|
||||
minimum_tangent_exit_radius: number
|
||||
}
|
||||
|
||||
/** Configuration for edges with change direction three times. */
|
||||
export interface EdgeThreeCornerTheme {
|
||||
/** The maximum arc radius. */
|
||||
radius_max: number
|
||||
backward_edge_arrow_threshold: number
|
||||
/** The maximum radius reduction (from [`RADIUS_BASE`]) to allow when choosing whether to use
|
||||
* the three-corner layout that doesn't use a backward corner. */
|
||||
max_squeeze: number
|
||||
}
|
@ -6,7 +6,9 @@
|
||||
"src/**/*.vue",
|
||||
"shared/**/*",
|
||||
"shared/**/*.vue",
|
||||
"src/util/theme.json"
|
||||
"src/util/theme.json",
|
||||
"stories/mockSuggestions.json",
|
||||
"mock/**/*"
|
||||
],
|
||||
"exclude": ["src/**/__tests__/*", "shared/**/__tests__/*", "public/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
@ -15,6 +17,7 @@
|
||||
"composite": true,
|
||||
"outDir": "../../node_modules/.cache/tsc",
|
||||
"baseUrl": ".",
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
@ -8,7 +8,8 @@
|
||||
"histoire.config.ts",
|
||||
"e2e/**/*",
|
||||
"parser-codegen/**/*",
|
||||
"node.env.d.ts"
|
||||
"node.env.d.ts",
|
||||
"mock/engine.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
|
Loading…
Reference in New Issue
Block a user