mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 17:34:10 +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">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { getMainFile, setMainFile } from '../mock/engine'
|
||||||
import App from '../src/App.vue'
|
import App from '../src/App.vue'
|
||||||
import MockProjectStoreWrapper from '../stories/MockProjectStoreWrapper.vue'
|
import MockProjectStoreWrapper from '../stories/MockProjectStoreWrapper.vue'
|
||||||
import { getMainFile, setMainFile } from './mockEngine'
|
|
||||||
|
|
||||||
const mainFile = computed({
|
const mainFile = computed({
|
||||||
get() {
|
get() {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import 'enso-dashboard/src/tailwind.css'
|
import 'enso-dashboard/src/tailwind.css'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { createApp, ref } from 'vue'
|
import { createApp, ref } from 'vue'
|
||||||
|
import { mockDataHandler, mockLSHandler } from '../mock/engine'
|
||||||
import '../src/assets/base.css'
|
import '../src/assets/base.css'
|
||||||
import { provideGuiConfig } from '../src/providers/guiConfig'
|
import { provideGuiConfig } from '../src/providers/guiConfig'
|
||||||
import { provideVisualizationConfig } from '../src/providers/visualizationConfig'
|
import { provideVisualizationConfig } from '../src/providers/visualizationConfig'
|
||||||
import { Vec2 } from '../src/util/data/vec2'
|
import { Vec2 } from '../src/util/data/vec2'
|
||||||
import { MockTransport, MockWebSocket } from '../src/util/net'
|
import { MockTransport, MockWebSocket } from '../src/util/net'
|
||||||
import MockApp from './MockApp.vue'
|
import MockApp from './MockApp.vue'
|
||||||
import { mockDataHandler, mockLSHandler } from './mockEngine'
|
|
||||||
|
|
||||||
MockTransport.addMock('engine', mockLSHandler)
|
MockTransport.addMock('engine', mockLSHandler)
|
||||||
MockWebSocket.addMock('data', mockDataHandler)
|
MockWebSocket.addMock('data', mockDataHandler)
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
type ProjectId,
|
type ProjectId,
|
||||||
type ProjectName,
|
type ProjectName,
|
||||||
type UTCDateTime,
|
type UTCDateTime,
|
||||||
} from './mockProjectManager'
|
} from '../mock/projectManager'
|
||||||
import pmSpec from './pm-openrpc.json' assert { type: 'json' }
|
import pmSpec from './pm-openrpc.json' assert { type: 'json' }
|
||||||
|
|
||||||
export default function setup() {
|
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
|
declare const projectIdBrand: unique symbol
|
||||||
/** A name of a project. */
|
/** An ID of a project. */
|
||||||
export type ProjectId = string & { [projectIdBrand]: never }
|
export type ProjectId = string & { [projectIdBrand]: never }
|
||||||
declare const projectNameBrand: unique symbol
|
declare const projectNameBrand: unique symbol
|
||||||
/** A name of a project. */
|
/** A name of a project. */
|
||||||
export type ProjectName = string & { [projectNameBrand]: never }
|
export type ProjectName = string & { [projectNameBrand]: never }
|
||||||
declare const utcDateTimeBrand: unique symbol
|
declare const utcDateTimeBrand: unique symbol
|
||||||
/** A name of a project. */
|
/** A UTC date and time. */
|
||||||
export type UTCDateTime = string & { [utcDateTimeBrand]: never }
|
export type UTCDateTime = string & { [utcDateTimeBrand]: never }
|
||||||
|
|
||||||
/** A value specifying the hostname and port of a socket. */
|
/** 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"/>
|
<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>
|
</svg>
|
||||||
</g>
|
</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">
|
<g id="expanded_node" fill="none">
|
||||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none"
|
<svg width="17" height="17" viewBox="0 0 17 17" fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
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'
|
import ToggleIcon from '@/components/ToggleIcon.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isAutoEvaluationDisabled: boolean
|
isOutputContextEnabledGlobally: boolean
|
||||||
|
isOutputContextOverridden: boolean
|
||||||
isDocsVisible: boolean
|
isDocsVisible: boolean
|
||||||
isVisualizationVisible: boolean
|
isVisualizationVisible: boolean
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:isAutoEvaluationDisabled': [isAutoEvaluationDisabled: boolean]
|
'update:isOutputContextOverridden': [isOutputContextOverridden: boolean]
|
||||||
'update:isDocsVisible': [isDocsVisible: boolean]
|
'update:isDocsVisible': [isDocsVisible: boolean]
|
||||||
'update:isVisualizationVisible': [isVisualizationVisible: boolean]
|
'update:isVisualizationVisible': [isVisualizationVisible: boolean]
|
||||||
}>()
|
}>()
|
||||||
@ -16,11 +17,16 @@ const emit = defineEmits<{
|
|||||||
<template>
|
<template>
|
||||||
<div class="CircularMenu">
|
<div class="CircularMenu">
|
||||||
<ToggleIcon
|
<ToggleIcon
|
||||||
icon="no_auto_replay"
|
:icon="props.isOutputContextEnabledGlobally ? 'no_auto_replay' : 'auto_replay'"
|
||||||
class="icon-container button no-auto-evaluate-button"
|
class="icon-container button override-output-context-button"
|
||||||
:alt="`${props.isAutoEvaluationDisabled ? 'Enable' : 'Disable'} auto-evaluation`"
|
:class="{ 'output-context-overridden': props.isOutputContextOverridden }"
|
||||||
:modelValue="props.isAutoEvaluationDisabled"
|
:alt="`${
|
||||||
@update:modelValue="emit('update:isAutoEvaluationDisabled', $event)"
|
props.isOutputContextEnabledGlobally != props.isOutputContextOverridden
|
||||||
|
? 'Disable'
|
||||||
|
: 'Enable'
|
||||||
|
} output context`"
|
||||||
|
:modelValue="props.isOutputContextOverridden"
|
||||||
|
@update:modelValue="emit('update:isOutputContextOverridden', $event)"
|
||||||
/>
|
/>
|
||||||
<ToggleIcon
|
<ToggleIcon
|
||||||
icon="docs"
|
icon="docs"
|
||||||
@ -70,12 +76,17 @@ const emit = defineEmits<{
|
|||||||
opacity: unset;
|
opacity: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-auto-evaluate-button {
|
.override-output-context-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 9px;
|
left: 9px;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.output-context-overridden {
|
||||||
|
opacity: 100%;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.docs-button {
|
.docs-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 18.54px;
|
left: 18.54px;
|
||||||
|
@ -5,7 +5,7 @@ import { useGraphStore, type Edge } from '@/stores/graph'
|
|||||||
import { assert } from '@/util/assert'
|
import { assert } from '@/util/assert'
|
||||||
import { Rect } from '@/util/data/rect'
|
import { Rect } from '@/util/data/rect'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import theme from '@/util/theme.json'
|
import theme from '@/util/theme'
|
||||||
import { clamp } from '@vueuse/core'
|
import { clamp } from '@vueuse/core'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ const edgeColor = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** The inputs to the edge state computation. */
|
/** 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 width and height of the node that originates the edge, if any.
|
||||||
* The edge may begin anywhere around the bottom half of the node. */
|
* The edge may begin anywhere around the bottom half of the node. */
|
||||||
sourceSize: Vec2
|
sourceSize: Vec2
|
||||||
@ -93,42 +93,12 @@ type Inputs = {
|
|||||||
targetPortTopDistanceInNode: number | undefined
|
targetPortTopDistanceInNode: number | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
type JunctionPoints = {
|
interface JunctionPoints {
|
||||||
points: Vec2[]
|
points: Vec2[]
|
||||||
maxRadius: number
|
maxRadius: number
|
||||||
targetAttachment: { target: Vec2; length: number } | undefined
|
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 {
|
function circleIntersection(x: number, r1: number, r2: number): number {
|
||||||
let xNorm = clamp(x, -r2, r1)
|
let xNorm = clamp(x, -r2, r1)
|
||||||
return Math.sqrt(r1 * r1 + r2 * r2 - xNorm * xNorm)
|
return Math.sqrt(r1 * r1 + r2 * r2 - xNorm * xNorm)
|
||||||
@ -188,31 +158,26 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const targetWellBelowSource =
|
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 targetBelowSource = inputs.targetOffset.y > theme.node.height / 2.0
|
||||||
const targetBeyondSource = Math.abs(inputs.targetOffset.x) > sourceMaxXOffset
|
const targetBeyondSource = Math.abs(inputs.targetOffset.x) > sourceMaxXOffset
|
||||||
const horizontalRoomFor3Corners =
|
const horizontalRoomFor3Corners =
|
||||||
targetBeyondSource &&
|
targetBeyondSource &&
|
||||||
Math.abs(inputs.targetOffset.x) - sourceMaxXOffset >=
|
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)) {
|
if (targetWellBelowSource || (targetBelowSource && !horizontalRoomFor3Corners)) {
|
||||||
const {
|
const innerTheme = theme.edge.one_corner
|
||||||
RADIUS_Y_ADJUSTMENT,
|
|
||||||
RADIUS_X_BASE,
|
|
||||||
RADIUS_X_FACTOR,
|
|
||||||
SOURCE_NODE_OVERLAP,
|
|
||||||
MINIMUM_TANGENT_EXIT_RADIUS,
|
|
||||||
} = SingleCorner
|
|
||||||
// The edge can originate anywhere along the length of the node.
|
// The edge can originate anywhere along the length of the node.
|
||||||
const sourceX = clamp(inputs.targetOffset.x, -sourceMaxXOffset, sourceMaxXOffset)
|
const sourceX = clamp(inputs.targetOffset.x, -sourceMaxXOffset, sourceMaxXOffset)
|
||||||
const distanceX = Math.max(Math.abs(inputs.targetOffset.x) - halfSourceSize.x, 0)
|
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
|
// 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
|
// 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.
|
// edge of the source, so that very short edges are less sharp.
|
||||||
const yAdjustment = Math.min(
|
const yAdjustment = Math.min(
|
||||||
Math.abs(inputs.targetOffset.x) - halfSourceSize.x + RADIUS_Y_ADJUSTMENT / 2.0,
|
Math.abs(inputs.targetOffset.x) - halfSourceSize.x + innerTheme.radius_y_adjustment / 2.0,
|
||||||
RADIUS_Y_ADJUSTMENT,
|
innerTheme.radius_y_adjustment,
|
||||||
)
|
)
|
||||||
const radiusY = Math.max(Math.abs(inputs.targetOffset.y) - yAdjustment, 0.0)
|
const radiusY = Math.max(Math.abs(inputs.targetOffset.y) - yAdjustment, 0.0)
|
||||||
const maxRadius = Math.min(radiusX, radiusY)
|
const maxRadius = Math.min(radiusX, radiusY)
|
||||||
@ -222,7 +187,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
|||||||
Math.abs(inputs.targetOffset.y),
|
Math.abs(inputs.targetOffset.y),
|
||||||
)
|
)
|
||||||
let sourceDY = 0
|
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
|
// 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.
|
// at the point that it exits the node.
|
||||||
const radius = Math.min(naturalRadius, maxRadius)
|
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)
|
const intersection = circleIntersection(circleOffset, theme.node.corner_radius, radius)
|
||||||
sourceDY = -Math.abs(radius - intersection)
|
sourceDY = -Math.abs(radius - intersection)
|
||||||
} else if (halfSourceSize.y != 0) {
|
} else if (halfSourceSize.y != 0) {
|
||||||
sourceDY = -SOURCE_NODE_OVERLAP + halfSourceSize.y
|
sourceDY = -innerTheme.source_node_overlap + halfSourceSize.y
|
||||||
}
|
}
|
||||||
const source = new Vec2(sourceX, sourceDY)
|
const source = new Vec2(sourceX, sourceDY)
|
||||||
// The target attachment will extend as far toward the edge of the node as it can without
|
// 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,
|
targetAttachment: attachment,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { RADIUS_MAX } = ThreeCorner
|
const radiusMax = theme.edge.three_corner.radius_max
|
||||||
// The edge originates from either side of the node.
|
// The edge originates from either side of the node.
|
||||||
const signX = Math.sign(inputs.targetOffset.x)
|
const signX = Math.sign(inputs.targetOffset.x)
|
||||||
const sourceX = Math.abs(sourceMaxXOffset) * signX
|
const sourceX = Math.abs(sourceMaxXOffset) * signX
|
||||||
@ -265,11 +230,11 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
|||||||
// ╰─────╯────╯\
|
// ╰─────╯────╯\
|
||||||
// J0
|
// J0
|
||||||
// Junctions (J0, J1) are in between source and target.
|
// Junctions (J0, J1) are in between source and target.
|
||||||
const j0Dx = Math.min(2 * RADIUS_MAX, distanceX / 2)
|
const j0Dx = Math.min(2 * radiusMax, distanceX / 2)
|
||||||
const j1Dx = Math.min(RADIUS_MAX, (distanceX - j0Dx) / 2)
|
const j1Dx = Math.min(radiusMax, (distanceX - j0Dx) / 2)
|
||||||
j0x = sourceX + Math.abs(j0Dx) * signX
|
j0x = sourceX + Math.abs(j0Dx) * signX
|
||||||
j1x = j0x + Math.abs(j1Dx) * signX
|
j1x = j0x + Math.abs(j1Dx) * signX
|
||||||
heightAdjustment = RADIUS_MAX - j1Dx
|
heightAdjustment = radiusMax - j1Dx
|
||||||
} else {
|
} else {
|
||||||
// J1
|
// J1
|
||||||
// /
|
// /
|
||||||
@ -278,16 +243,16 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
|||||||
// ╭─────╮ │
|
// ╭─────╮ │
|
||||||
// ╰─────╯────╯
|
// ╰─────╯────╯
|
||||||
// J0 > source; J0 > J1; J1 > target.
|
// J0 > source; J0 > J1; J1 > target.
|
||||||
j1x = inputs.targetOffset.x + Math.abs(RADIUS_MAX) * signX
|
j1x = inputs.targetOffset.x + Math.abs(radiusMax) * signX
|
||||||
const j0BeyondSource = Math.abs(inputs.targetOffset.x) + RADIUS_MAX * 2
|
const j0BeyondSource = Math.abs(inputs.targetOffset.x) + radiusMax * 2
|
||||||
const j0BeyondTarget = Math.abs(sourceX) + RADIUS_MAX
|
const j0BeyondTarget = Math.abs(sourceX) + radiusMax
|
||||||
j0x = Math.abs(Math.max(j0BeyondTarget, j0BeyondSource)) * signX
|
j0x = Math.abs(Math.max(j0BeyondTarget, j0BeyondSource)) * signX
|
||||||
heightAdjustment = 0
|
heightAdjustment = 0
|
||||||
}
|
}
|
||||||
if (j0x == null || j1x == null || heightAdjustment == null) return null
|
if (j0x == null || j1x == null || heightAdjustment == null) return null
|
||||||
const attachmentHeight = inputs.targetPortTopDistanceInNode ?? 0
|
const attachmentHeight = inputs.targetPortTopDistanceInNode ?? 0
|
||||||
const top = Math.min(
|
const top = Math.min(
|
||||||
inputs.targetOffset.y - MIN_APPROACH_HEIGHT - attachmentHeight + heightAdjustment,
|
inputs.targetOffset.y - theme.edge.min_approach_height - attachmentHeight + heightAdjustment,
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
const source = new Vec2(sourceX, 0)
|
const source = new Vec2(sourceX, 0)
|
||||||
@ -297,7 +262,7 @@ function junctionPoints(inputs: Inputs): JunctionPoints | null {
|
|||||||
const attachmentTarget = attachment?.target ?? inputs.targetOffset
|
const attachmentTarget = attachment?.target ?? inputs.targetOffset
|
||||||
return {
|
return {
|
||||||
points: [source, j0, j1, attachmentTarget],
|
points: [source, j0, j1, attachmentTarget],
|
||||||
maxRadius: RADIUS_MAX,
|
maxRadius: radiusMax,
|
||||||
targetAttachment: attachment,
|
targetAttachment: attachment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -440,12 +405,12 @@ const activeStyle = computed(() => {
|
|||||||
|
|
||||||
const baseStyle = computed(() => ({ '--node-base-color': edgeColor.value ?? 'tan' }))
|
const baseStyle = computed(() => ({ '--node-base-color': edgeColor.value ?? 'tan' }))
|
||||||
|
|
||||||
function click(_e: PointerEvent) {
|
function click() {
|
||||||
if (base.value == null) return {}
|
if (base.value == null) return
|
||||||
if (navigator?.sceneMousePos == null) return {}
|
if (navigator?.sceneMousePos == null) return
|
||||||
const length = base.value.getTotalLength()
|
const length = base.value.getTotalLength()
|
||||||
let offset = lengthTo(navigator?.sceneMousePos)
|
let offset = lengthTo(navigator?.sceneMousePos)
|
||||||
if (offset == null) return {}
|
if (offset == null) return
|
||||||
if (offset < length / 2) graph.disconnectTarget(props.edge)
|
if (offset < length / 2) graph.disconnectTarget(props.edge)
|
||||||
else graph.disconnectSource(props.edge)
|
else graph.disconnectSource(props.edge)
|
||||||
}
|
}
|
||||||
@ -457,7 +422,7 @@ function arrowPosition(): Vec2 | undefined {
|
|||||||
const target = targetRect.value
|
const target = targetRect.value
|
||||||
const source = sourceRect.value
|
const source = sourceRect.value
|
||||||
if (target == null || source == null) return
|
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
|
if (points[1] == null) return
|
||||||
return source.center().add(points[1])
|
return source.center().add(points[1])
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts">
|
||||||
import { nodeEditBindings } from '@/bindings'
|
import { nodeEditBindings } from '@/bindings'
|
||||||
import CircularMenu from '@/components/CircularMenu.vue'
|
import CircularMenu from '@/components/CircularMenu.vue'
|
||||||
import GraphNodeError from '@/components/GraphEditor/GraphNodeError.vue'
|
import GraphNodeError from '@/components/GraphEditor/GraphNodeError.vue'
|
||||||
@ -12,12 +12,14 @@ import { injectGraphNavigator } from '@/providers/graphNavigator'
|
|||||||
import { injectGraphSelection } from '@/providers/graphSelection'
|
import { injectGraphSelection } from '@/providers/graphSelection'
|
||||||
import { useGraphStore, type Node } from '@/stores/graph'
|
import { useGraphStore, type Node } from '@/stores/graph'
|
||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
|
import { Ast } from '@/util/ast'
|
||||||
|
import { Prefixes } from '@/util/ast/prefixes'
|
||||||
import type { Opt } from '@/util/data/opt'
|
import type { Opt } from '@/util/data/opt'
|
||||||
import { Rect } from '@/util/data/rect'
|
import { Rect } from '@/util/data/rect'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import { displayedIconOf } from '@/util/getIconName'
|
import { displayedIconOf } from '@/util/getIconName'
|
||||||
import { setIfUndefined } from 'lib0/map'
|
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'
|
import { computed, ref, watch, watchEffect } from 'vue'
|
||||||
|
|
||||||
const MAXIMUM_CLICK_LENGTH_MS = 300
|
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. */
|
/** The width in pixels that is not the widget tree. This includes the icon, and padding. */
|
||||||
const NODE_EXTRA_WIDTH_PX = 30
|
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<{
|
const props = defineProps<{
|
||||||
node: Node
|
node: Node
|
||||||
edited: boolean
|
edited: boolean
|
||||||
@ -39,10 +53,8 @@ const emit = defineEmits<{
|
|||||||
outputPortClick: [portId: ExprId]
|
outputPortClick: [portId: ExprId]
|
||||||
outputPortDoubleClick: [portId: ExprId]
|
outputPortDoubleClick: [portId: ExprId]
|
||||||
doubleClick: []
|
doubleClick: []
|
||||||
'update:content': [updates: [range: ContentRange, content: string][]]
|
|
||||||
'update:edited': [cursorPosition: number]
|
'update:edited': [cursorPosition: number]
|
||||||
'update:rect': [rect: Rect]
|
'update:rect': [rect: Rect]
|
||||||
'update:selected': [selected: boolean]
|
|
||||||
'update:visualizationId': [id: Opt<VisualizationIdentifier>]
|
'update:visualizationId': [id: Opt<VisualizationIdentifier>]
|
||||||
'update:visualizationRect': [rect: Rect | undefined]
|
'update:visualizationRect': [rect: Rect | undefined]
|
||||||
'update:visualizationVisible': [visible: boolean]
|
'update:visualizationVisible': [visible: boolean]
|
||||||
@ -89,7 +101,6 @@ watch(isSelected, (selected) => {
|
|||||||
menuVisible.value = menuVisible.value && selected
|
menuVisible.value = menuVisible.value && selected
|
||||||
})
|
})
|
||||||
|
|
||||||
const isAutoEvaluationDisabled = ref(false)
|
|
||||||
const isDocsVisible = ref(false)
|
const isDocsVisible = ref(false)
|
||||||
const isVisualizationVisible = computed(() => props.node.vis?.visible ?? 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 expressionInfo = computed(() => graph.db.getExpressionInfo(nodeId.value))
|
||||||
const outputPortLabel = computed(() => expressionInfo.value?.typename ?? 'Unknown')
|
const outputPortLabel = computed(() => expressionInfo.value?.typename ?? 'Unknown')
|
||||||
const executionState = computed(() => expressionInfo.value?.payload.type ?? 'Unknown')
|
const executionState = computed(() => expressionInfo.value?.payload.type ?? 'Unknown')
|
||||||
@ -223,6 +279,7 @@ const handleNodeClick = useDoubleClick(
|
|||||||
(e: MouseEvent) => nodeEditHandler(e),
|
(e: MouseEvent) => nodeEditHandler(e),
|
||||||
() => emit('doubleClick'),
|
() => emit('doubleClick'),
|
||||||
).handleClick
|
).handleClick
|
||||||
|
|
||||||
interface PortData {
|
interface PortData {
|
||||||
clipRange: [number, number]
|
clipRange: [number, number]
|
||||||
label: string
|
label: string
|
||||||
@ -232,7 +289,7 @@ interface PortData {
|
|||||||
const outputPorts = computed((): PortData[] => {
|
const outputPorts = computed((): PortData[] => {
|
||||||
const ports = outputPortsSet.value
|
const ports = outputPortsSet.value
|
||||||
const numPorts = ports.size
|
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 labelIdent = numPorts > 1 ? graph.db.getOutputPortIdentifier(portId) + ': ' : ''
|
||||||
const labelType =
|
const labelType =
|
||||||
graph.db.getExpressionInfo(numPorts > 1 ? portId : nodeId.value)?.typename ?? 'Unknown'
|
graph.db.getExpressionInfo(numPorts > 1 ? portId : nodeId.value)?.typename ?? 'Unknown'
|
||||||
@ -296,8 +353,9 @@ function portGroupStyle(port: PortData) {
|
|||||||
</div>
|
</div>
|
||||||
<CircularMenu
|
<CircularMenu
|
||||||
v-if="menuVisible"
|
v-if="menuVisible"
|
||||||
v-model:isAutoEvaluationDisabled="isAutoEvaluationDisabled"
|
v-model:isOutputContextOverridden="isOutputContextOverridden"
|
||||||
v-model:isDocsVisible="isDocsVisible"
|
v-model:isDocsVisible="isDocsVisible"
|
||||||
|
:isOutputContextEnabledGlobally="projectStore.isOutputContextEnabled"
|
||||||
:isVisualizationVisible="isVisualizationVisible"
|
:isVisualizationVisible="isVisualizationVisible"
|
||||||
@update:isVisualizationVisible="emit('update:visualizationVisible', $event)"
|
@update:isVisualizationVisible="emit('update:visualizationVisible', $event)"
|
||||||
/>
|
/>
|
||||||
@ -320,7 +378,7 @@ function portGroupStyle(port: PortData) {
|
|||||||
<div class="node" @pointerdown="handleNodeClick" v-on="dragPointer.events">
|
<div class="node" @pointerdown="handleNodeClick" v-on="dragPointer.events">
|
||||||
<SvgIcon class="icon grab-handle" :name="icon"></SvgIcon>
|
<SvgIcon class="icon grab-handle" :name="icon"></SvgIcon>
|
||||||
<div ref="contentNode" class="widget-tree">
|
<div ref="contentNode" class="widget-tree">
|
||||||
<NodeWidgetTree :ast="node.rootSpan" />
|
<NodeWidgetTree :ast="displayedExpression" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GraphNodeError v-if="error" class="error" :error="error" />
|
<GraphNodeError v-if="error" class="error" :error="error" />
|
||||||
|
@ -9,7 +9,7 @@ import { useGraphStore } from '@/stores/graph'
|
|||||||
import { useProjectStore } from '@/stores/project'
|
import { useProjectStore } from '@/stores/project'
|
||||||
import type { Vec2 } from '@/util/data/vec2'
|
import type { Vec2 } from '@/util/data/vec2'
|
||||||
import { stackItemsEqual } from 'shared/languageServerTypes'
|
import { stackItemsEqual } from 'shared/languageServerTypes'
|
||||||
import type { ContentRange, ExprId } from 'shared/yjsModel'
|
import type { ExprId } from 'shared/yjsModel'
|
||||||
import { computed, toRaw } from 'vue'
|
import { computed, toRaw } from 'vue'
|
||||||
|
|
||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
@ -23,14 +23,6 @@ const emit = defineEmits<{
|
|||||||
nodeDoubleClick: [nodeId: ExprId]
|
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) {
|
function nodeIsDragged(movedId: ExprId, offset: Vec2) {
|
||||||
const scaledOffset = offset.scale(1 / (navigator?.scale ?? 1))
|
const scaledOffset = offset.scale(1 / (navigator?.scale ?? 1))
|
||||||
dragging.startOrUpdate(movedId, scaledOffset)
|
dragging.startOrUpdate(movedId, scaledOffset)
|
||||||
@ -42,7 +34,7 @@ function hoverNode(id: ExprId | undefined) {
|
|||||||
|
|
||||||
const uploadingFiles = computed<[FileName, File][]>(() => {
|
const uploadingFiles = computed<[FileName, File][]>(() => {
|
||||||
const currentStackItem = projectStore.executionContext.getStackTop()
|
const currentStackItem = projectStore.executionContext.getStackTop()
|
||||||
return [...projectStore.awareness.allUploads()].filter(([_name, file]) =>
|
return [...projectStore.awareness.allUploads()].filter(([, file]) =>
|
||||||
stackItemsEqual(file.stackItem, toRaw(currentStackItem)),
|
stackItemsEqual(file.stackItem, toRaw(currentStackItem)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -54,15 +46,13 @@ const uploadingFiles = computed<[FileName, File][]>(() => {
|
|||||||
:key="id"
|
:key="id"
|
||||||
:node="node"
|
:node="node"
|
||||||
:edited="id === graphStore.editedNodeInfo?.id"
|
:edited="id === graphStore.editedNodeInfo?.id"
|
||||||
@delete="graphStore.deleteNode(id)"
|
|
||||||
@pointerenter="hoverNode(id)"
|
@pointerenter="hoverNode(id)"
|
||||||
@pointerleave="hoverNode(undefined)"
|
@pointerleave="hoverNode(undefined)"
|
||||||
@dragging="nodeIsDragged(id, $event)"
|
@dragging="nodeIsDragged(id, $event)"
|
||||||
@draggingCommited="dragging.finishDrag()"
|
@draggingCommited="dragging.finishDrag()"
|
||||||
@outputPortClick="graphStore.createEdgeFromOutput"
|
@outputPortClick="graphStore.createEdgeFromOutput($event)"
|
||||||
@outputPortDoubleClick="emit('nodeOutputPortDoubleClick', $event)"
|
@outputPortDoubleClick="emit('nodeOutputPortDoubleClick', $event)"
|
||||||
@doubleClick="emit('nodeDoubleClick', id)"
|
@doubleClick="emit('nodeDoubleClick', id)"
|
||||||
@update:content="updateNodeContent(id, $event)"
|
|
||||||
@update:edited="graphStore.setEditedNode(id, $event)"
|
@update:edited="graphStore.setEditedNode(id, $event)"
|
||||||
@update:rect="graphStore.updateNodeRect(id, $event)"
|
@update:rect="graphStore.updateNodeRect(id, $event)"
|
||||||
@update:visualizationId="graphStore.setNodeVisualizationId(id, $event)"
|
@update:visualizationId="graphStore.setNodeVisualizationId(id, $event)"
|
||||||
|
@ -20,7 +20,7 @@ const value = computed({
|
|||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
const newCode = `[${value.map((item) => item.code()).join(', ')}]`
|
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,
|
modelValue: false,
|
||||||
})
|
})
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', toggledOn: boolean): void
|
'update:modelValue': [toggledOn: boolean]
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -273,7 +273,8 @@ export function usePointer(
|
|||||||
trackedPointer.value = e.pointerId
|
trackedPointer.value = e.pointerId
|
||||||
// This is mostly SAFE, as virtually all `Element`s also extend `GlobalEventHandlers`.
|
// This is mostly SAFE, as virtually all `Element`s also extend `GlobalEventHandlers`.
|
||||||
trackedElement = e.currentTarget as Element & 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)
|
initialGrabPos = new Vec2(e.clientX, e.clientY)
|
||||||
lastPos = initialGrabPos
|
lastPos = initialGrabPos
|
||||||
handler(computePosition(e, initialGrabPos, lastPos), e, 'start')
|
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 type { SuggestionEntry } from '@/stores/suggestionDatabase/entry'
|
||||||
import { Ast, RawAst, RawAstExtended } from '@/util/ast'
|
import { Ast, RawAst, RawAstExtended } from '@/util/ast'
|
||||||
import { AliasAnalyzer } from '@/util/ast/aliasAnalysis'
|
import { AliasAnalyzer } from '@/util/ast/aliasAnalysis'
|
||||||
|
import { nodeFromAst } from '@/util/ast/node'
|
||||||
import { colorFromString } from '@/util/colors'
|
import { colorFromString } from '@/util/colors'
|
||||||
import { MappedKeyMap, MappedSet } from '@/util/containers'
|
import { MappedKeyMap, MappedSet } from '@/util/containers'
|
||||||
import { arrayEquals, byteArraysEqual, tryGetIndex } from '@/util/data/array'
|
import { arrayEquals, byteArraysEqual, tryGetIndex } from '@/util/data/array'
|
||||||
import type { Opt } from '@/util/data/opt'
|
import type { Opt } from '@/util/data/opt'
|
||||||
import { Vec2 } from '@/util/data/vec2'
|
import { Vec2 } from '@/util/data/vec2'
|
||||||
import { ReactiveDb, ReactiveIndex, ReactiveMapping } from '@/util/database/reactiveDb'
|
import { ReactiveDb, ReactiveIndex, ReactiveMapping } from '@/util/database/reactiveDb'
|
||||||
|
import * as random from 'lib0/random'
|
||||||
import * as set from 'lib0/set'
|
import * as set from 'lib0/set'
|
||||||
import { methodPointerEquals, type MethodCall } from 'shared/languageServerTypes'
|
import { methodPointerEquals, type MethodCall } from 'shared/languageServerTypes'
|
||||||
import {
|
import {
|
||||||
@ -310,17 +312,19 @@ export class GraphDb {
|
|||||||
return new GraphDb(db, ref([]), registry)
|
return new GraphDb(db, ref([]), registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
mockNode(binding: string, id: ExprId, code?: string) {
|
mockNode(binding: string, id: ExprId, code?: string): Node {
|
||||||
const node = {
|
const pattern = Ast.parse(binding)
|
||||||
|
const node: Node = {
|
||||||
outerExprId: id,
|
outerExprId: id,
|
||||||
pattern: Ast.parse(binding),
|
pattern,
|
||||||
rootSpan: Ast.parse(code ?? '0'),
|
rootSpan: Ast.parse(code ?? '0'),
|
||||||
position: Vec2.Zero,
|
position: Vec2.Zero,
|
||||||
vis: undefined,
|
vis: undefined,
|
||||||
}
|
}
|
||||||
const bidingId = node.pattern.astId
|
const bindingId = pattern.astId
|
||||||
this.nodeIdToNode.set(id, node)
|
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>
|
vis: Opt<VisualizationMetadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeFromAst(ast: Ast.Ast): Node {
|
/** This should only be used for supplying as initial props when testing.
|
||||||
const common = {
|
* Please do {@link GraphDb.mockNode} with a `useGraphStore().db` after mount. */
|
||||||
outerExprId: ast.exprId,
|
export function mockNode(exprId?: ExprId): Node {
|
||||||
|
return {
|
||||||
|
outerExprId: exprId ?? (random.uuidv4() as ExprId),
|
||||||
|
pattern: undefined,
|
||||||
|
rootSpan: Ast.parse('0'),
|
||||||
position: Vec2.Zero,
|
position: Vec2.Zero,
|
||||||
vis: undefined,
|
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 {
|
function mathodCallEquals(a: MethodCall | undefined, b: MethodCall | undefined): boolean {
|
||||||
|
@ -156,13 +156,13 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function disconnectSource(edge: Edge) {
|
function disconnectSource(edge: Edge) {
|
||||||
if (edge.target)
|
if (!edge.target) return
|
||||||
unconnectedEdge.value = { target: edge.target, disconnectedEdgeTarget: edge.target }
|
unconnectedEdge.value = { target: edge.target, disconnectedEdgeTarget: edge.target }
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnectTarget(edge: Edge) {
|
function disconnectTarget(edge: Edge) {
|
||||||
if (edge.source && edge.target)
|
if (!edge.source || !edge.target) return
|
||||||
unconnectedEdge.value = { source: edge.source, disconnectedEdgeTarget: edge.target }
|
unconnectedEdge.value = { source: edge.source, disconnectedEdgeTarget: edge.target }
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearUnconnected() {
|
function clearUnconnected() {
|
||||||
@ -230,20 +230,6 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
proj.stopCapturingUndo()
|
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) {
|
function setNodePosition(nodeId: ExprId, position: Vec2) {
|
||||||
const node = db.nodeIdToNode.get(nodeId)
|
const node = db.nodeIdToNode.get(nodeId)
|
||||||
if (!node) return
|
if (!node) return
|
||||||
@ -338,8 +324,6 @@ export const useGraphStore = defineStore('graph', () => {
|
|||||||
deleteNode,
|
deleteNode,
|
||||||
setNodeContent,
|
setNodeContent,
|
||||||
setExpressionContent,
|
setExpressionContent,
|
||||||
replaceNodeSubexpression,
|
|
||||||
replaceExpressionContent,
|
|
||||||
setNodePosition,
|
setNodePosition,
|
||||||
setNodeVisualizationId,
|
setNodeVisualizationId,
|
||||||
setNodeVisualizationVisible,
|
setNodeVisualizationVisible,
|
||||||
|
@ -85,6 +85,9 @@ async function initializeLsRpcConnection(
|
|||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Error initializing Language Server RPC:', error)
|
||||||
|
throw error
|
||||||
})
|
})
|
||||||
const contentRoots = initialization.contentRoots
|
const contentRoots = initialization.contentRoots
|
||||||
return { connection, contentRoots }
|
return { connection, contentRoots }
|
||||||
@ -93,7 +96,10 @@ async function initializeLsRpcConnection(
|
|||||||
async function initializeDataConnection(clientId: Uuid, url: string) {
|
async function initializeDataConnection(clientId: Uuid, url: string) {
|
||||||
const client = createWebsocketClient(url, { binaryType: 'arraybuffer', sendPings: false })
|
const client = createWebsocketClient(url, { binaryType: 'arraybuffer', sendPings: false })
|
||||||
const connection = new DataServer(client)
|
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
|
return connection
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,8 +445,20 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
const clientId = random.uuidv4() as Uuid
|
const clientId = random.uuidv4() as Uuid
|
||||||
const lsUrls = resolveLsUrl(config.value)
|
const lsUrls = resolveLsUrl(config.value)
|
||||||
const initializedConnection = initializeLsRpcConnection(clientId, lsUrls.rpcUrl)
|
const initializedConnection = initializeLsRpcConnection(clientId, lsUrls.rpcUrl)
|
||||||
const lsRpcConnection = initializedConnection.then(({ connection }) => connection)
|
const lsRpcConnection = initializedConnection.then(
|
||||||
const contentRoots = initializedConnection.then(({ contentRoots }) => contentRoots)
|
({ 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 dataConnection = initializeDataConnection(clientId, lsUrls.dataUrl)
|
||||||
|
|
||||||
const rpcUrl = new URL(lsUrls.rpcUrl)
|
const rpcUrl = new URL(lsUrls.rpcUrl)
|
||||||
@ -514,7 +532,7 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
moduleDocGuid.value = guid
|
moduleDocGuid.value = guid
|
||||||
}
|
}
|
||||||
|
|
||||||
projectModel.modules.observe((_) => tryReadDocGuid())
|
projectModel.modules.observe(tryReadDocGuid)
|
||||||
watchEffect(tryReadDocGuid)
|
watchEffect(tryReadDocGuid)
|
||||||
|
|
||||||
const module = computedAsync(async () => {
|
const module = computedAsync(async () => {
|
||||||
@ -537,8 +555,16 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstExecution = lsRpcConnection.then((lsRpc) =>
|
const firstExecution = lsRpcConnection.then(
|
||||||
nextEvent(lsRpc, 'executionContext/executionComplete'),
|
(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 executionContext = createExecutionContextForMain()
|
||||||
const visualizationDataRegistry = new VisualizationDataRegistry(executionContext, dataConnection)
|
const visualizationDataRegistry = new VisualizationDataRegistry(executionContext, dataConnection)
|
||||||
@ -603,6 +629,8 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isOutputContextEnabled = computed(() => executionMode.value === 'live')
|
||||||
|
|
||||||
function stopCapturingUndo() {
|
function stopCapturingUndo() {
|
||||||
module.value?.undoManager.stopCapturing()
|
module.value?.undoManager.stopCapturing()
|
||||||
}
|
}
|
||||||
@ -661,6 +689,7 @@ export const useProjectStore = defineStore('project', () => {
|
|||||||
lsRpcConnection: markRaw(lsRpcConnection),
|
lsRpcConnection: markRaw(lsRpcConnection),
|
||||||
dataConnection: markRaw(dataConnection),
|
dataConnection: markRaw(dataConnection),
|
||||||
useVisualizationData,
|
useVisualizationData,
|
||||||
|
isOutputContextEnabled,
|
||||||
stopCapturingUndo,
|
stopCapturingUndo,
|
||||||
executionMode,
|
executionMode,
|
||||||
dataflowErrors,
|
dataflowErrors,
|
||||||
|
@ -367,7 +367,7 @@ const parseCases = [
|
|||||||
]
|
]
|
||||||
test.each(parseCases)('parse: %s', (testCase) => {
|
test.each(parseCases)('parse: %s', (testCase) => {
|
||||||
const root = Ast.parse(testCase.code)
|
const root = Ast.parse(testCase.code)
|
||||||
expect(Ast.debug(root)).toEqual(testCase.tree)
|
expect(Ast.tokenTree(root)).toEqual(testCase.tree)
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Edits (#8367).
|
// 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 * as RawAst from '@/generated/ast'
|
||||||
import { parseEnso } from '@/util/ast'
|
import { parseEnso } from '@/util/ast'
|
||||||
import { AstExtended as RawAstExtended } from '@/util/ast/extended'
|
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 { Err, Ok, type Result } from '@/util/data/result'
|
||||||
import type { LazyObject } from '@/util/parserSupport'
|
import type { LazyObject } from '@/util/parserSupport'
|
||||||
|
import { unsafeEntries } from '@/util/record'
|
||||||
import * as random from 'lib0/random'
|
import * as random from 'lib0/random'
|
||||||
import { IdMap, type ExprId } from 'shared/yjsModel'
|
import { IdMap, type ExprId } from 'shared/yjsModel'
|
||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
interface Module {
|
export interface Module {
|
||||||
|
get raw(): MutableModule
|
||||||
get(id: AstId): Ast | null
|
get(id: AstId): Ast | null
|
||||||
getExtended(id: AstId): RawAstExtended | undefined
|
getExtended(id: AstId): RawAstExtended | undefined
|
||||||
|
edit(): MutableModule
|
||||||
|
apply(module: MutableModule): void
|
||||||
}
|
}
|
||||||
|
|
||||||
class Committed implements Module {
|
export class MutableModule implements Module {
|
||||||
nodes: Map<AstId, Ast>
|
base: Module | null
|
||||||
astExtended: Map<AstId, RawAstExtended>
|
nodes: Map<AstId, Ast | null>
|
||||||
|
astExtended: Map<AstId, RawAstExtended> | null
|
||||||
|
|
||||||
constructor() {
|
constructor(
|
||||||
this.nodes = reactive(new Map<AstId, Ast>())
|
base: Module | null,
|
||||||
this.astExtended = reactive(new Map<AstId, RawAstExtended>())
|
nodes: Map<AstId, Ast | null>,
|
||||||
}
|
astExtended: Map<AstId, RawAstExtended> | null,
|
||||||
|
) {
|
||||||
/** 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) {
|
|
||||||
this.base = base
|
this.base = base
|
||||||
this.pending = new Map()
|
this.nodes = nodes
|
||||||
|
this.astExtended = astExtended
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Replace all committed values with the state of the uncommitted parse. */
|
static Observable(): MutableModule {
|
||||||
commit() {
|
const nodes = reactive(new Map<AstId, Ast>())
|
||||||
for (const [id, ast] of this.pending.entries()) {
|
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) {
|
if (ast === null) {
|
||||||
this.base.nodes.delete(id)
|
this.nodes.delete(id)
|
||||||
} else {
|
} 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. */
|
/** Returns a syntax node representing the current committed state of the given ID. */
|
||||||
get(id: AstId): Ast | null {
|
get(id: AstId): Ast | null {
|
||||||
const editedNode = this.pending.get(id)
|
const editedNode = this.nodes.get(id)
|
||||||
if (editedNode === null) {
|
if (editedNode === null) {
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
return editedNode ?? this.base.get(id) ?? null
|
return editedNode ?? this.base?.get(id) ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set(id: AstId, ast: Ast) {
|
set(id: AstId, ast: Ast) {
|
||||||
this.pending.set(id, ast)
|
this.nodes.set(id, ast)
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtended(id: AstId): RawAstExtended | undefined {
|
getExtended(id: AstId): RawAstExtended | undefined {
|
||||||
return this.base.astExtended.get(id)
|
return this.astExtended?.get(id) ?? this.base?.getExtended(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id: AstId) {
|
delete(id: AstId) {
|
||||||
this.pending.set(id, null)
|
this.nodes.set(id, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,13 +117,17 @@ function newTokenId(): TokenId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Token {
|
export class Token {
|
||||||
private _code: string
|
code_: string
|
||||||
exprId: TokenId
|
exprId: TokenId
|
||||||
readonly _tokenType: RawAst.Token.Type
|
tokenType_: RawAst.Token.Type | undefined
|
||||||
constructor(code: string, id: TokenId, type: RawAst.Token.Type) {
|
constructor(code: string, id: TokenId, type: RawAst.Token.Type | undefined) {
|
||||||
this._code = code
|
this.code_ = code
|
||||||
this.exprId = id
|
this.exprId = id
|
||||||
this._tokenType = type
|
this.tokenType_ = type
|
||||||
|
}
|
||||||
|
|
||||||
|
static new(code: string) {
|
||||||
|
return new Token(code, newTokenId(), undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compatibility wrapper for `exprId`.
|
// Compatibility wrapper for `exprId`.
|
||||||
@ -117,18 +136,19 @@ export class Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code(): string {
|
code(): string {
|
||||||
return this._code
|
return this.code_
|
||||||
}
|
}
|
||||||
|
|
||||||
typeName(): string {
|
typeName(): string {
|
||||||
return RawAst.Token.typeNames[this._tokenType]!
|
if (this.tokenType_) return RawAst.Token.typeNames[this.tokenType_]!
|
||||||
|
else return 'Raw'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class Ast {
|
export abstract class Ast {
|
||||||
readonly treeType: RawAst.Tree.Type | undefined
|
readonly treeType: RawAst.Tree.Type | undefined
|
||||||
_id: AstId
|
_id: AstId
|
||||||
readonly module: Committed
|
readonly module: Module
|
||||||
|
|
||||||
// Deprecated interface for incremental integration of Ast API. Eliminate usages for #8367.
|
// Deprecated interface for incremental integration of Ast API. Eliminate usages for #8367.
|
||||||
get astExtended(): RawAstExtended | undefined {
|
get astExtended(): RawAstExtended | undefined {
|
||||||
@ -140,16 +160,12 @@ export abstract class Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static deserialize(serialized: string): Ast {
|
static deserialize(serialized: string): Ast {
|
||||||
const parsed: any = JSON.parse(serialized)
|
const parsed: SerializedPrintedSource = JSON.parse(serialized)
|
||||||
const nodes: NodeSpanMap = new Map(Object.entries(parsed.info.nodes))
|
const nodes = new Map(unsafeEntries(parsed.info.nodes))
|
||||||
const tokens: TokenSpanMap = new Map(Object.entries(parsed.info.tokens))
|
const tokens = new Map(unsafeEntries(parsed.info.tokens))
|
||||||
const module = new Committed()
|
const module = MutableModule.Transient()
|
||||||
const edit = new Edit(module)
|
|
||||||
const tree = parseEnso(parsed.code)
|
const tree = parseEnso(parsed.code)
|
||||||
type NodeSpanMap = Map<NodeKey, AstId[]>
|
const root = abstract(module, tree, parsed.code, { nodes, tokens }).node
|
||||||
type TokenSpanMap = Map<TokenKey, TokenId>
|
|
||||||
const root = abstract(edit, tree, parsed.code, { nodes, tokens }).node
|
|
||||||
edit.commit()
|
|
||||||
return module.get(root)!
|
return module.get(root)!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,8 +192,8 @@ export abstract class Ast {
|
|||||||
/** Returns child subtrees, including information about the whitespace between them. */
|
/** Returns child subtrees, including information about the whitespace between them. */
|
||||||
abstract concreteChildren(): IterableIterator<NodeChild>
|
abstract concreteChildren(): IterableIterator<NodeChild>
|
||||||
|
|
||||||
code(): string {
|
code(module?: Module): string {
|
||||||
return print(this).code
|
return print(this, module).code
|
||||||
}
|
}
|
||||||
|
|
||||||
repr(): string {
|
repr(): string {
|
||||||
@ -189,17 +205,25 @@ export abstract class Ast {
|
|||||||
return RawAst.Tree.typeNames[this.treeType]
|
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 code = typeof source === 'object' ? source.code : source
|
||||||
const ids = typeof source === 'object' ? source.info : undefined
|
const ids = typeof source === 'object' ? source.info : undefined
|
||||||
const tree = parseEnso(code)
|
const tree = parseEnso(code)
|
||||||
const module = new Committed()
|
const module = inModule ?? MutableModule.Observable()
|
||||||
const edit = new Edit(module)
|
const newRoot = abstract(module, tree, code, ids).node
|
||||||
const newRoot = abstract(edit, tree, code, ids).node
|
|
||||||
edit.commit()
|
|
||||||
return module.get(newRoot)!
|
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) {
|
visitRecursive(visit: (node: Ast | Token) => void) {
|
||||||
visit(this)
|
visit(this)
|
||||||
for (const child of this.concreteChildren()) {
|
for (const child of this.concreteChildren()) {
|
||||||
@ -211,21 +235,23 @@ export abstract class Ast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected constructor(module: Edit, id?: AstId, treeType?: RawAst.Tree.Type) {
|
protected constructor(module: MutableModule, id?: AstId, treeType?: RawAst.Tree.Type) {
|
||||||
this.module = module.base
|
this.module = module
|
||||||
this._id = id ?? newNodeId()
|
this._id = id ?? newNodeId()
|
||||||
this.treeType = treeType
|
this.treeType = treeType
|
||||||
module.set(this._id, this)
|
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 = ''
|
let code = ''
|
||||||
for (const child of this.concreteChildren()) {
|
for (const child of this.concreteChildren()) {
|
||||||
if (
|
if (child.node != null && !(child.node instanceof Token) && module_.get(child.node) === null)
|
||||||
child.node != null &&
|
|
||||||
!(child.node instanceof Token) &&
|
|
||||||
this.module.get(child.node) === null
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
if (child.whitespace != null) {
|
if (child.whitespace != null) {
|
||||||
code += child.whitespace
|
code += child.whitespace
|
||||||
@ -237,7 +263,9 @@ export abstract class Ast {
|
|||||||
if (child.node instanceof Token) {
|
if (child.node instanceof Token) {
|
||||||
code += child.node.code()
|
code += child.node.code()
|
||||||
} else {
|
} 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 {
|
export class App extends Ast {
|
||||||
private _func: NodeChild<AstId>
|
_func: NodeChild<AstId>
|
||||||
private _leftParen: NodeChild<Token> | null
|
_leftParen: NodeChild<Token> | null
|
||||||
private _argumentName: NodeChild<Token> | null
|
_argumentName: NodeChild<Token> | null
|
||||||
private _equals: NodeChild<Token> | null
|
_equals: NodeChild<Token> | null
|
||||||
private _arg: NodeChild<AstId>
|
_arg: NodeChild<AstId>
|
||||||
private _rightParen: NodeChild<Token> | null
|
_rightParen: NodeChild<Token> | null
|
||||||
|
|
||||||
get function(): Ast {
|
get function(): Ast {
|
||||||
return this.module.get(this._func.node)!
|
return this.module.get(this._func.node)!
|
||||||
@ -273,7 +301,7 @@ export class App extends Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
func: NodeChild<AstId>,
|
func: NodeChild<AstId>,
|
||||||
leftParen: NodeChild<Token> | null,
|
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(
|
function positionalApp(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
func: NodeChild<AstId>,
|
func: NodeChild<AstId>,
|
||||||
arg: NodeChild<AstId>,
|
arg: NodeChild<AstId>,
|
||||||
@ -313,7 +359,7 @@ function positionalApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function namedApp(
|
function namedApp(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
func: NodeChild<AstId>,
|
func: NodeChild<AstId>,
|
||||||
leftParen: NodeChild<Token> | null,
|
leftParen: NodeChild<Token> | null,
|
||||||
@ -336,8 +382,8 @@ function namedApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UnaryOprApp extends Ast {
|
export class UnaryOprApp extends Ast {
|
||||||
private _opr: NodeChild<Token>
|
_opr: NodeChild<Token>
|
||||||
private _arg: NodeChild<AstId> | null
|
_arg: NodeChild<AstId> | null
|
||||||
|
|
||||||
get operator(): Token {
|
get operator(): Token {
|
||||||
return this._opr.node
|
return this._opr.node
|
||||||
@ -349,7 +395,7 @@ export class UnaryOprApp extends Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
opr: NodeChild<Token>,
|
opr: NodeChild<Token>,
|
||||||
arg: NodeChild<AstId> | null,
|
arg: NodeChild<AstId> | null,
|
||||||
@ -367,7 +413,7 @@ export class UnaryOprApp extends Ast {
|
|||||||
|
|
||||||
export class NegationOprApp extends UnaryOprApp {
|
export class NegationOprApp extends UnaryOprApp {
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
opr: NodeChild<Token>,
|
opr: NodeChild<Token>,
|
||||||
arg: NodeChild<AstId> | null,
|
arg: NodeChild<AstId> | null,
|
||||||
@ -377,9 +423,9 @@ export class NegationOprApp extends UnaryOprApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class OprApp extends Ast {
|
export class OprApp extends Ast {
|
||||||
protected _lhs: NodeChild<AstId> | null
|
_lhs: NodeChild<AstId> | null
|
||||||
protected _opr: NodeChild[]
|
_opr: NodeChild[]
|
||||||
protected _rhs: NodeChild<AstId> | null
|
_rhs: NodeChild<AstId> | null
|
||||||
|
|
||||||
get lhs(): Ast | null {
|
get lhs(): Ast | null {
|
||||||
return this._lhs ? this.module.get(this._lhs.node) : null
|
return this._lhs ? this.module.get(this._lhs.node) : null
|
||||||
@ -399,7 +445,7 @@ export class OprApp extends Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
lhs: NodeChild<AstId> | null,
|
lhs: NodeChild<AstId> | null,
|
||||||
opr: NodeChild[],
|
opr: NodeChild[],
|
||||||
@ -420,7 +466,7 @@ export class OprApp extends Ast {
|
|||||||
|
|
||||||
export class PropertyAccess extends OprApp {
|
export class PropertyAccess extends OprApp {
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
lhs: NodeChild<AstId> | null,
|
lhs: NodeChild<AstId> | null,
|
||||||
opr: NodeChild<Token>,
|
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. */
|
/** Representation without any type-specific accessors, for tree types that don't require any special treatment. */
|
||||||
export class Generic extends Ast {
|
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)
|
super(module, id, treeType)
|
||||||
this._children = children ?? []
|
this._children = children ?? []
|
||||||
}
|
}
|
||||||
@ -447,39 +498,39 @@ export class Generic extends Ast {
|
|||||||
type MultiSegmentAppSegment = { header: NodeChild<Token>; body: NodeChild<AstId> | null }
|
type MultiSegmentAppSegment = { header: NodeChild<Token>; body: NodeChild<AstId> | null }
|
||||||
|
|
||||||
export class Import extends Ast {
|
export class Import extends Ast {
|
||||||
private polyglot_: MultiSegmentAppSegment | null
|
_polyglot: MultiSegmentAppSegment | null
|
||||||
private from_: MultiSegmentAppSegment | null
|
_from: MultiSegmentAppSegment | null
|
||||||
private import__: MultiSegmentAppSegment
|
_import: MultiSegmentAppSegment
|
||||||
private all_: NodeChild<Token> | null
|
_all: NodeChild<Token> | null
|
||||||
private as_: MultiSegmentAppSegment | null
|
_as: MultiSegmentAppSegment | null
|
||||||
private hiding_: MultiSegmentAppSegment | null
|
_hiding: MultiSegmentAppSegment | null
|
||||||
|
|
||||||
get polyglot(): Ast | 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 {
|
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 {
|
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 {
|
get all(): Token | null {
|
||||||
return this.all_?.node ?? null
|
return this._all?.node ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
get as(): Ast | 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 {
|
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(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
polyglot: MultiSegmentAppSegment | null,
|
polyglot: MultiSegmentAppSegment | null,
|
||||||
from: MultiSegmentAppSegment | null,
|
from: MultiSegmentAppSegment | null,
|
||||||
@ -489,12 +540,12 @@ export class Import extends Ast {
|
|||||||
hiding: MultiSegmentAppSegment | null,
|
hiding: MultiSegmentAppSegment | null,
|
||||||
) {
|
) {
|
||||||
super(module, id, RawAst.Tree.Type.Import)
|
super(module, id, RawAst.Tree.Type.Import)
|
||||||
this.polyglot_ = polyglot
|
this._polyglot = polyglot
|
||||||
this.from_ = from
|
this._from = from
|
||||||
this.import__ = import_
|
this._import = import_
|
||||||
this.all_ = all
|
this._all = all
|
||||||
this.as_ = as
|
this._as = as
|
||||||
this.hiding_ = hiding
|
this._hiding = hiding
|
||||||
}
|
}
|
||||||
|
|
||||||
*concreteChildren(): IterableIterator<NodeChild> {
|
*concreteChildren(): IterableIterator<NodeChild> {
|
||||||
@ -504,23 +555,23 @@ export class Import extends Ast {
|
|||||||
if (segment?.body) parts.push(segment.body)
|
if (segment?.body) parts.push(segment.body)
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
yield* segment(this.polyglot_)
|
yield* segment(this._polyglot)
|
||||||
yield* segment(this.from_)
|
yield* segment(this._from)
|
||||||
yield* segment(this.import__)
|
yield* segment(this._import)
|
||||||
if (this.all_) yield this.all_
|
if (this._all) yield this._all
|
||||||
yield* segment(this.as_)
|
yield* segment(this._as)
|
||||||
yield* segment(this.hiding_)
|
yield* segment(this._hiding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TextLiteral extends Ast {
|
export class TextLiteral extends Ast {
|
||||||
private readonly open_: NodeChild<Token> | null
|
_open: NodeChild<Token> | null
|
||||||
private readonly newline_: NodeChild<Token> | null
|
_newline: NodeChild<Token> | null
|
||||||
private readonly elements_: NodeChild[]
|
_elements: NodeChild[]
|
||||||
private readonly close_: NodeChild<Token> | null
|
_close: NodeChild<Token> | null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
open: NodeChild<Token> | null,
|
open: NodeChild<Token> | null,
|
||||||
newline: NodeChild<Token> | null,
|
newline: NodeChild<Token> | null,
|
||||||
@ -528,62 +579,70 @@ export class TextLiteral extends Ast {
|
|||||||
close: NodeChild<Token> | null,
|
close: NodeChild<Token> | null,
|
||||||
) {
|
) {
|
||||||
super(module, id, RawAst.Tree.Type.TextLiteral)
|
super(module, id, RawAst.Tree.Type.TextLiteral)
|
||||||
this.open_ = open
|
this._open = open
|
||||||
this.newline_ = newline
|
this._newline = newline
|
||||||
this.elements_ = elements
|
this._elements = elements
|
||||||
this.close_ = close
|
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> {
|
*concreteChildren(): IterableIterator<NodeChild> {
|
||||||
if (this.open_) yield this.open_
|
if (this._open) yield this._open
|
||||||
if (this.newline_) yield this.newline_
|
if (this._newline) yield this._newline
|
||||||
yield* this.elements_
|
yield* this._elements
|
||||||
if (this.close_) yield this.close_
|
if (this._close) yield this._close
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Invalid extends Ast {
|
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)
|
super(module, id, RawAst.Tree.Type.Invalid)
|
||||||
this.expression_ = expression
|
this._expression = expression
|
||||||
}
|
}
|
||||||
|
|
||||||
*concreteChildren(): IterableIterator<NodeChild> {
|
*concreteChildren(): IterableIterator<NodeChild> {
|
||||||
yield this.expression_
|
yield this._expression
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Group extends Ast {
|
export class Group extends Ast {
|
||||||
private readonly open_: NodeChild<Token> | undefined
|
_open: NodeChild<Token> | undefined
|
||||||
private readonly expression_: NodeChild<AstId> | null
|
_expression: NodeChild<AstId> | null
|
||||||
private readonly close_: NodeChild<Token> | undefined
|
_close: NodeChild<Token> | undefined
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
open: NodeChild<Token> | undefined,
|
open: NodeChild<Token> | undefined,
|
||||||
expression: NodeChild<AstId> | null,
|
expression: NodeChild<AstId> | null,
|
||||||
close: NodeChild<Token> | undefined,
|
close: NodeChild<Token> | undefined,
|
||||||
) {
|
) {
|
||||||
super(module, id, RawAst.Tree.Type.Group)
|
super(module, id, RawAst.Tree.Type.Group)
|
||||||
this.open_ = open
|
this._open = open
|
||||||
this.expression_ = expression
|
this._expression = expression
|
||||||
this.close_ = close
|
this._close = close
|
||||||
}
|
}
|
||||||
|
|
||||||
*concreteChildren(): IterableIterator<NodeChild> {
|
*concreteChildren(): IterableIterator<NodeChild> {
|
||||||
if (this.open_) yield this.open_
|
if (this._open) yield this._open
|
||||||
if (this.expression_) yield this.expression_
|
if (this._expression) yield this._expression
|
||||||
if (this.close_) yield this.close_
|
if (this._close) yield this._close
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NumericLiteral extends Ast {
|
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)
|
super(module, id, RawAst.Tree.Type.Number)
|
||||||
this._tokens = tokens ?? []
|
this._tokens = tokens ?? []
|
||||||
}
|
}
|
||||||
@ -594,11 +653,12 @@ export class NumericLiteral extends Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FunctionArgument = NodeChild[]
|
type FunctionArgument = NodeChild[]
|
||||||
|
|
||||||
export class Function extends Ast {
|
export class Function extends Ast {
|
||||||
private _name: NodeChild<AstId>
|
_name: NodeChild<AstId>
|
||||||
private _args: FunctionArgument[]
|
_args: FunctionArgument[]
|
||||||
private _equals: NodeChild<Token>
|
_equals: NodeChild<Token>
|
||||||
private _body: NodeChild<AstId> | null
|
_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
|
// 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`).
|
// here and in `rawChildren` (and indirectly, `print`).
|
||||||
get name(): Ast | null {
|
get name(): Ast | null {
|
||||||
@ -616,7 +676,7 @@ export class Function extends Ast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
name: NodeChild<AstId>,
|
name: NodeChild<AstId>,
|
||||||
args: FunctionArgument[],
|
args: FunctionArgument[],
|
||||||
@ -640,9 +700,9 @@ export class Function extends Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Assignment extends Ast {
|
export class Assignment extends Ast {
|
||||||
private _pattern: NodeChild<AstId>
|
_pattern: NodeChild<AstId>
|
||||||
private _equals: NodeChild<Token>
|
_equals: NodeChild<Token>
|
||||||
private _expression: NodeChild<AstId>
|
_expression: NodeChild<AstId>
|
||||||
get pattern(): Ast | null {
|
get pattern(): Ast | null {
|
||||||
return this.module.get(this._pattern.node)
|
return this.module.get(this._pattern.node)
|
||||||
}
|
}
|
||||||
@ -650,7 +710,7 @@ export class Assignment extends Ast {
|
|||||||
return this.module.get(this._expression.node)
|
return this.module.get(this._expression.node)
|
||||||
}
|
}
|
||||||
constructor(
|
constructor(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
id: AstId | undefined,
|
id: AstId | undefined,
|
||||||
pattern: NodeChild<AstId>,
|
pattern: NodeChild<AstId>,
|
||||||
equals: NodeChild<Token>, // TODO: Edits (#8367): Allow undefined
|
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
|
newline: NodeChild<Token> // Edits (#8367): Allow undefined
|
||||||
expression: NodeChild<AstId> | null
|
expression: NodeChild<AstId> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BodyBlock extends Ast {
|
export class BodyBlock extends Ast {
|
||||||
private _lines: BlockLine[];
|
_lines: BlockLine[];
|
||||||
|
|
||||||
*expressions(): IterableIterator<Ast> {
|
*expressions(): IterableIterator<Ast> {
|
||||||
for (const line of this._lines) {
|
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)
|
super(module, id, RawAst.Tree.Type.BodyBlock)
|
||||||
this._lines = lines
|
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 = ''
|
let code = ''
|
||||||
for (const line of this._lines) {
|
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?.whitespace ?? ''
|
||||||
code += line.newline?.node.code() ?? '\n'
|
code += line.newline?.node.code() ?? '\n'
|
||||||
if (line.expression !== null) {
|
if (line.expression !== null) {
|
||||||
code += line.expression.whitespace ?? indent
|
code += line.expression.whitespace ?? indent
|
||||||
if (line.expression.node !== null) {
|
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 {
|
export class Ident extends Ast {
|
||||||
public token: NodeChild<Token>
|
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)
|
super(module, id, RawAst.Tree.Type.Ident)
|
||||||
this.token = token
|
this.token = token
|
||||||
}
|
}
|
||||||
@ -777,18 +846,16 @@ export class Ident extends Ast {
|
|||||||
export class Wildcard extends Ast {
|
export class Wildcard extends Ast {
|
||||||
public token: NodeChild<Token>
|
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)
|
super(module, id, RawAst.Tree.Type.Wildcard)
|
||||||
this.token = token
|
this.token = token
|
||||||
}
|
}
|
||||||
|
|
||||||
static new(): Wildcard {
|
static new(): Wildcard {
|
||||||
const module = new Committed()
|
const module = MutableModule.Transient()
|
||||||
const edit = new Edit(module)
|
const ast = new Wildcard(module, undefined, {
|
||||||
const ast = new Wildcard(edit, undefined, {
|
|
||||||
node: new Token('_', newTokenId(), RawAst.Token.Type.Wildcard),
|
node: new Token('_', newTokenId(), RawAst.Token.Type.Wildcard),
|
||||||
})
|
})
|
||||||
edit.commit()
|
|
||||||
return ast
|
return ast
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -798,9 +865,9 @@ export class Wildcard extends Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RawCode 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)
|
super(module, id)
|
||||||
this._code = code
|
this._code = code
|
||||||
}
|
}
|
||||||
@ -819,7 +886,7 @@ export class RawCode extends Ast {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function abstract(
|
function abstract(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
tree: RawAst.Tree,
|
tree: RawAst.Tree,
|
||||||
code: string,
|
code: string,
|
||||||
info: InfoMap | undefined,
|
info: InfoMap | undefined,
|
||||||
@ -830,8 +897,9 @@ function abstract(
|
|||||||
const tokenIds = info?.tokens ?? new Map()
|
const tokenIds = info?.tokens ?? new Map()
|
||||||
return abstractTree(module, tree, code, nodesExpected, tokenIds)
|
return abstractTree(module, tree, code, nodesExpected, tokenIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
function abstractTree(
|
function abstractTree(
|
||||||
module: Edit,
|
module: MutableModule,
|
||||||
tree: RawAst.Tree,
|
tree: RawAst.Tree,
|
||||||
code: string,
|
code: string,
|
||||||
nodesExpected: NodeSpanMap,
|
nodesExpected: NodeSpanMap,
|
||||||
@ -858,8 +926,9 @@ function abstractTree(
|
|||||||
const whitespaceEnd = whitespaceStart + tree.whitespaceLengthInCodeParsed
|
const whitespaceEnd = whitespaceStart + tree.whitespaceLengthInCodeParsed
|
||||||
const codeStart = whitespaceEnd
|
const codeStart = whitespaceEnd
|
||||||
const codeEnd = codeStart + tree.childrenLengthInCodeParsed
|
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
|
// All node types use this value in the same way to obtain the ID type,
|
||||||
// must pop the tree's span from the ID map *after* processing children.
|
// 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)
|
const spanKey = nodeKey(codeStart, codeEnd - codeStart, tree.type)
|
||||||
let node: AstId
|
let node: AstId
|
||||||
switch (tree.type) {
|
switch (tree.type) {
|
||||||
@ -1030,54 +1099,68 @@ function abstractToken(
|
|||||||
return { whitespace, node }
|
return { whitespace, node }
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeKey = string
|
declare const nodeKeyBrand: unique symbol
|
||||||
type TokenKey = string
|
type NodeKey = string & { [nodeKeyBrand]: never }
|
||||||
function nodeKey(start: number, length: number, type: RawAst.Tree.Type | undefined): NodeKey {
|
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() ?? '?'
|
const type_ = type?.toString() ?? '?'
|
||||||
return `${start}:${length}:${type_}`
|
return `${start}:${length}:${type_}` as NodeKey
|
||||||
}
|
}
|
||||||
function tokenKey(start: number, length: number): TokenKey {
|
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 NodeSpanMap = Map<NodeKey, AstId[]>
|
||||||
type TokenSpanMap = Map<TokenKey, TokenId>
|
type TokenSpanMap = Map<TokenKey, TokenId>
|
||||||
export type InfoMap = {
|
|
||||||
|
export interface InfoMap {
|
||||||
nodes: NodeSpanMap
|
nodes: NodeSpanMap
|
||||||
tokens: TokenSpanMap
|
tokens: TokenSpanMap
|
||||||
}
|
}
|
||||||
|
|
||||||
type PrintedSource = {
|
interface PrintedSource {
|
||||||
info: InfoMap
|
info: InfoMap
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return stringification with associated ID map. This is only exported for testing. */
|
/** 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 = {
|
const info: InfoMap = {
|
||||||
nodes: new Map(),
|
nodes: new Map(),
|
||||||
tokens: new Map(),
|
tokens: new Map(),
|
||||||
}
|
}
|
||||||
const code = ast._print(info, 0, '')
|
const code = ast._print(info, 0, '', module)
|
||||||
return { info, code }
|
return { info, code }
|
||||||
}
|
}
|
||||||
|
|
||||||
type DebugTree = (DebugTree | string)[]
|
export type TokenTree = (TokenTree | string)[]
|
||||||
export function debug(root: Ast, universe?: Map<AstId, Ast>): DebugTree {
|
|
||||||
|
export function tokenTree(root: Ast): TokenTree {
|
||||||
const module = root.module
|
const module = root.module
|
||||||
return Array.from(root.concreteChildren(), (child) => {
|
return Array.from(root.concreteChildren(), (child) => {
|
||||||
if (child.node instanceof Token) {
|
if (child.node instanceof Token) {
|
||||||
return child.node.code()
|
return child.node.code()
|
||||||
} else {
|
} else {
|
||||||
const node = module.get(child.node)
|
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.
|
// FIXME: We should use alias analysis to handle ambiguous names correctly.
|
||||||
export function findModuleMethod(module: Committed, name: string): Function | null {
|
export function findModuleMethod(module: Module, name: string): Function | null {
|
||||||
for (const node of module.nodes.values()) {
|
for (const node of module.raw.nodes.values()) {
|
||||||
if (node instanceof Function) {
|
if (node instanceof Function) {
|
||||||
if (node.name && node.name.code() === name) {
|
if (node.name && node.name.code() === name) {
|
||||||
return node
|
return node
|
||||||
@ -1087,7 +1170,7 @@ export function findModuleMethod(module: Committed, name: string): Function | nu
|
|||||||
return null
|
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)
|
const method = findModuleMethod(module, name)
|
||||||
if (!method || !(method.body instanceof BodyBlock)) return null
|
if (!method || !(method.body instanceof BodyBlock)) return null
|
||||||
return method.body
|
return method.body
|
||||||
@ -1120,7 +1203,7 @@ export function parseTransitional(code: string, idMap: IdMap): Ast {
|
|||||||
idMap.finishAndSynchronize()
|
idMap.finishAndSynchronize()
|
||||||
const nodes = new Map<NodeKey, AstId[]>()
|
const nodes = new Map<NodeKey, AstId[]>()
|
||||||
const tokens = new Map<TokenKey, TokenId>()
|
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>) => {
|
legacyAst.visitRecursive((nodeOrToken: RawAstExtended<RawAst.Tree | RawAst.Token>) => {
|
||||||
const start = nodeOrToken.span()[0]
|
const start = nodeOrToken.span()[0]
|
||||||
const length = nodeOrToken.span()[1] - 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
|
return true
|
||||||
})
|
})
|
||||||
const newRoot = Ast.parse({ info: { nodes, tokens }, code })
|
const newRoot = Ast.parse({ info: { nodes, tokens }, code })
|
||||||
newRoot.module.astExtended = astExtended
|
newRoot.module.raw.astExtended = astExtended
|
||||||
return newRoot
|
return newRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parse(source: PrintedSource | string): Ast {
|
export const parse = Ast.parse
|
||||||
return Ast.parse(source)
|
export const parseLine = Ast.parseLine
|
||||||
}
|
|
||||||
|
|
||||||
export function deserialize(serialized: string): Ast {
|
export function deserialize(serialized: string): Ast {
|
||||||
return Ast.deserialize(serialized)
|
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]> {
|
export function* zip<T, U>(left: Iterable<T>, right: Iterable<U>): Generator<[T, U]> {
|
||||||
const leftIterator = left[Symbol.iterator]()
|
const leftIterator = left[Symbol.iterator]()
|
||||||
const rightIterator = right[Symbol.iterator]()
|
const rightIterator = right[Symbol.iterator]()
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const leftResult = leftIterator.next()
|
const leftResult = leftIterator.next()
|
||||||
const rightResult = rightIterator.next()
|
const rightResult = rightIterator.next()
|
||||||
|
if (leftResult.done || rightResult.done) break
|
||||||
if (leftResult.done || rightResult.done) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
yield [leftResult.value, rightResult.value]
|
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
|
if (task == null) return
|
||||||
this.taskRunning = true
|
this.taskRunning = true
|
||||||
this.lastTask = this.lastTask
|
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(() => {
|
.finally(() => {
|
||||||
this.taskRunning = false
|
this.taskRunning = false
|
||||||
this.run()
|
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,
|
"corner_radius": 16,
|
||||||
"vertical_gap": 32,
|
"vertical_gap": 32,
|
||||||
"horizontal_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",
|
"src/**/*.vue",
|
||||||
"shared/**/*",
|
"shared/**/*",
|
||||||
"shared/**/*.vue",
|
"shared/**/*.vue",
|
||||||
"src/util/theme.json"
|
"src/util/theme.json",
|
||||||
|
"stories/mockSuggestions.json",
|
||||||
|
"mock/**/*"
|
||||||
],
|
],
|
||||||
"exclude": ["src/**/__tests__/*", "shared/**/__tests__/*", "public/**/__tests__/*"],
|
"exclude": ["src/**/__tests__/*", "shared/**/__tests__/*", "public/**/__tests__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
@ -15,6 +17,7 @@
|
|||||||
"composite": true,
|
"composite": true,
|
||||||
"outDir": "../../node_modules/.cache/tsc",
|
"outDir": "../../node_modules/.cache/tsc",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"noEmit": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
"histoire.config.ts",
|
"histoire.config.ts",
|
||||||
"e2e/**/*",
|
"e2e/**/*",
|
||||||
"parser-codegen/**/*",
|
"parser-codegen/**/*",
|
||||||
"node.env.d.ts"
|
"node.env.d.ts",
|
||||||
|
"mock/engine.ts"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
Loading…
Reference in New Issue
Block a user