Editor singleton to prevent re-renders (#2163)

* move editor data into a singleton

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* debounce on update

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make select on extrude work

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* highlight range

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* highlight range

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix errors

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* almost forgot the error pane

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* loint

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* call out to codemirror

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix tauri;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more efficient

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* create the modals in the hook

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Revert "create the modals in the hook"

This reverts commit bbeba85030763cf7235a09fa24247dbf120f2a64.

* change todo

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-04-19 14:24:40 -07:00
committed by GitHub
parent f08d955d40
commit 537d86c8ff
44 changed files with 584 additions and 415 deletions

2
.nvmrc
View File

@ -1 +1 @@
v20.5.0 v21.7.1

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: ["@babel/preset-env"], presets: ['@babel/preset-env'],
} }

View File

@ -1673,3 +1673,42 @@ test('Can code mod a line length', async ({ page }) => {
`const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)` `const length001 = 20const part001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> line([0, 20], %) |> xLine(-length001, %)`
) )
}) })
test('Extrude from command bar selects extrude line after', async ({
page,
}) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const part001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> xLine(-20, %)
|> close(%)
`
)
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Click the line of code for xLine.
await page.getByText(`close(%)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Extrude' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
` |> extrude(5 + 7, %)`
)
})

View File

@ -84,8 +84,8 @@
"test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts", "test:e2e:tauri": "E2E_TAURI_ENABLED=true TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' wdio run wdio.conf.ts",
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &", "simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000", "simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./src && prettier --write ./e2e", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
"fmt-check": "prettier --check ./src && prettier --check ./e2e", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt", "build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt", "build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm", "build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
@ -132,6 +132,7 @@
"@types/wicg-file-system-access": "^2023.10.5", "@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@vitest/web-worker": "^1.5.0",
"@wdio/cli": "^8.24.3", "@wdio/cli": "^8.24.3",
"@wdio/globals": "^8.36.0", "@wdio/globals": "^8.36.0",
"@wdio/local-runner": "^8.36.0", "@wdio/local-runner": "^8.36.0",

View File

@ -49,8 +49,6 @@ export default defineConfig({
// use: { ...devices['Desktop Chrome'] }, // use: { ...devices['Desktop Chrome'] },
// }, // },
/* Test against mobile viewports. */ /* Test against mobile viewports. */
// { // {
// name: 'Mobile Chrome', // name: 'Mobile Chrome',
@ -78,4 +76,4 @@ export default defineConfig({
// url: 'http://127.0.0.1:3000', // url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}) })

View File

@ -3,7 +3,6 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useStore } from 'useStore'
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra' import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls' import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils' import { throttle } from 'lib/utils'
@ -47,10 +46,6 @@ export const ClientSideScene = ({
const canvasRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLDivElement>(null)
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { hideClient, hideServer } = useShouldHideScene() const { hideClient, hideServer } = useShouldHideScene()
const { setHighlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
// Listen for changes to the camera controls setting // Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly. // and update the client-side scene's controls accordingly.
@ -69,7 +64,6 @@ export const ClientSideScene = ({
const canvas = canvasRef.current const canvas = canvasRef.current
canvas.appendChild(sceneInfra.renderer.domElement) canvas.appendChild(sceneInfra.renderer.domElement)
sceneInfra.animate() sceneInfra.animate()
sceneInfra.setHighlightCallback(setHighlightRange)
canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false) canvas.addEventListener('mousemove', sceneInfra.onMouseMove, false)
canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false) canvas.addEventListener('mousedown', sceneInfra.onMouseDown, false)
canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false) canvas.addEventListener('mouseup', sceneInfra.onMouseUp, false)

View File

@ -57,6 +57,7 @@ import {
kclManager, kclManager,
sceneInfra, sceneInfra,
codeManager, codeManager,
editorManager,
} from 'lib/singletons' } from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst, useStore } from 'useStore' import { executeAst, useStore } from 'useStore'
@ -1423,7 +1424,7 @@ export class SceneEntities {
parent.userData.pathToNode, parent.userData.pathToNode,
'CallExpression' 'CallExpression'
).node ).node
sceneInfra.highlightCallback([node.start, node.end]) editorManager.setHighlightRange([node.start, node.end])
const yellow = 0xffff00 const yellow = 0xffff00
colorSegment(selected, yellow) colorSegment(selected, yellow)
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE) const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
@ -1459,10 +1460,10 @@ export class SceneEntities {
} }
return return
} }
sceneInfra.highlightCallback([0, 0]) editorManager.setHighlightRange([0, 0])
}, },
onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => { onMouseLeave: ({ selected, ...rest }: OnMouseEnterLeaveArgs) => {
sceneInfra.highlightCallback([0, 0]) editorManager.setHighlightRange([0, 0])
const parent = getParentGroup(selected, [ const parent = getParentGroup(selected, [
STRAIGHT_SEGMENT, STRAIGHT_SEGMENT,
TANGENTIAL_ARC_TO_SEGMENT, TANGENTIAL_ARC_TO_SEGMENT,

View File

@ -24,7 +24,6 @@ import {
import { compareVec2Epsilon2 } from 'lang/std/sketch' import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import * as TWEEN from '@tweenjs/tween.js' import * as TWEEN from '@tweenjs/tween.js'
import { SourceRange } from 'lang/wasm'
import { Axis } from 'lib/selections' import { Axis } from 'lib/selections'
import { type BaseUnit } from 'lib/settings/settingsTypes' import { type BaseUnit } from 'lib/settings/settingsTypes'
import { CameraControls } from './CameraControls' import { CameraControls } from './CameraControls'
@ -149,10 +148,6 @@ export class SceneInfra {
onMouseLeave: () => {}, onMouseLeave: () => {},
}) })
} }
highlightCallback: (a: SourceRange) => void = () => {}
setHighlightCallback(cb: (a: SourceRange) => void) {
this.highlightCallback = cb
}
modelingSend: SendType = (() => {}) as any modelingSend: SendType = (() => {}) as any
setSend(send: SendType) { setSend(send: SendType) {

View File

@ -1,11 +1,9 @@
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { kclManager } from 'lib/singletons' import { editorManager, kclManager } from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst' import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useStore } from 'useStore'
export function AstExplorer() { export function AstExplorer() {
const setHighlightRange = useStore((s) => s.setHighlightRange)
const { context } = useModelingContext() const { context } = useModelingContext()
const pathToNode = getNodePathFromSourceRange( const pathToNode = getNodePathFromSourceRange(
// TODO maybe need to have callback to make sure it stays in sync // TODO maybe need to have callback to make sure it stays in sync
@ -42,7 +40,7 @@ export function AstExplorer() {
<div <div
className="h-full relative" className="h-full relative"
onMouseLeave={(e) => { onMouseLeave={(e) => {
setHighlightRange([0, 0]) editorManager.setHighlightRange([0, 0])
}} }}
> >
<pre className="text-xs"> <pre className="text-xs">
@ -88,7 +86,6 @@ function DisplayObj({
filterKeys: string[] filterKeys: string[]
node: any node: any
}) { }) {
const setHighlightRange = useStore((s) => s.setHighlightRange)
const { send } = useModelingContext() const { send } = useModelingContext()
const ref = useRef<HTMLPreElement>(null) const ref = useRef<HTMLPreElement>(null)
const [hasCursor, setHasCursor] = useState(false) const [hasCursor, setHasCursor] = useState(false)
@ -112,12 +109,12 @@ function DisplayObj({
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : '' hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
}`} }`}
onMouseEnter={(e) => { onMouseEnter={(e) => {
setHighlightRange([obj?.start || 0, obj.end]) editorManager.setHighlightRange([obj?.start || 0, obj.end])
e.stopPropagation() e.stopPropagation()
}} }}
onMouseMove={(e) => { onMouseMove={(e) => {
e.stopPropagation() e.stopPropagation()
setHighlightRange([obj?.start || 0, obj.end]) editorManager.setHighlightRange([obj?.start || 0, obj.end])
}} }}
onClick={(e) => { onClick={(e) => {
send({ send({

View File

@ -1,6 +1,7 @@
import { useMachine } from '@xstate/react' import { useMachine } from '@xstate/react'
import { editorManager } from 'lib/singletons'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { createContext } from 'react' import { createContext, useEffect } from 'react'
import { EventFrom, StateFrom } from 'xstate' import { EventFrom, StateFrom } from 'xstate'
type CommandsContextType = { type CommandsContextType = {
@ -30,6 +31,10 @@ export const CommandBarProvider = ({
}, },
}) })
useEffect(() => {
editorManager.setCommandBarSend(commandBarSend)
})
return ( return (
<CommandsContext.Provider <CommandsContext.Provider
value={{ value={{

View File

@ -17,6 +17,7 @@ import {
sceneInfra, sceneInfra,
engineCommandManager, engineCommandManager,
codeManager, codeManager,
editorManager,
} from 'lib/singletons' } from 'lib/singletons'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import { import {
@ -98,17 +99,6 @@ export const ModelingMachineProvider = ({
) )
useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true)) useHotkeys('meta + shift + .', () => coreDump(coreDumpManager, true))
const {
isShiftDown,
editorView,
setLastCodeMirrorSelectionUpdatedFromScene,
} = useStore((s) => ({
isShiftDown: s.isShiftDown,
editorView: s.editorView,
setLastCodeMirrorSelectionUpdatedFromScene:
s.setLastCodeMirrorSelectionUpdatedFromScene,
}))
// Settings machine setup // Settings machine setup
// const retrievedSettings = useRef( // const retrievedSettings = useRef(
// localStorage?.getItem(MODELING_PERSIST_KEY) || '{}' // localStorage?.getItem(MODELING_PERSIST_KEY) || '{}'
@ -135,29 +125,33 @@ export const ModelingMachineProvider = ({
'Set selection': assign(({ selectionRanges }, event) => { 'Set selection': assign(({ selectionRanges }, event) => {
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
const setSelections = event.data const setSelections = event.data
if (!editorView) return {} if (!editorManager.editorView) return {}
const dispatchSelection = (selection?: EditorSelection) => { const dispatchSelection = (selection?: EditorSelection) => {
if (!selection) return // TODO less of hack for the below please if (!selection) return // TODO less of hack for the below please
setLastCodeMirrorSelectionUpdatedFromScene(Date.now()) editorManager.lastSelectionEvent = Date.now()
setTimeout(() => editorView.dispatch({ selection })) setTimeout(() => {
if (editorManager.editorView) {
editorManager.editorView.dispatch({ selection })
}
})
} }
let selections: Selections = { let selections: Selections = {
codeBasedSelections: [], codeBasedSelections: [],
otherSelections: [], otherSelections: [],
} }
if (setSelections.selectionType === 'singleCodeCursor') { if (setSelections.selectionType === 'singleCodeCursor') {
if (!setSelections.selection && isShiftDown) { if (!setSelections.selection && editorManager.isShiftDown) {
} else if (!setSelections.selection && !isShiftDown) { } else if (!setSelections.selection && !editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: [], codeBasedSelections: [],
otherSelections: [], otherSelections: [],
} }
} else if (setSelections.selection && !isShiftDown) { } else if (setSelections.selection && !editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: [setSelections.selection], codeBasedSelections: [setSelections.selection],
otherSelections: [], otherSelections: [],
} }
} else if (setSelections.selection && isShiftDown) { } else if (setSelections.selection && editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: [ codeBasedSelections: [
...selectionRanges.codeBasedSelections, ...selectionRanges.codeBasedSelections,
@ -180,6 +174,7 @@ export const ModelingMachineProvider = ({
engineCommandManager.sendSceneCommand(event) engineCommandManager.sendSceneCommand(event)
) )
updateSceneObjectColors() updateSceneObjectColors()
return { return {
selectionRanges: selections, selectionRanges: selections,
} }
@ -192,7 +187,7 @@ export const ModelingMachineProvider = ({
} }
if (setSelections.selectionType === 'otherSelection') { if (setSelections.selectionType === 'otherSelection') {
if (isShiftDown) { if (editorManager.isShiftDown) {
selections = { selections = {
codeBasedSelections: selectionRanges.codeBasedSelections, codeBasedSelections: selectionRanges.codeBasedSelections,
otherSelections: [setSelections.selection], otherSelections: [setSelections.selection],
@ -516,6 +511,19 @@ export const ModelingMachineProvider = ({
}) })
}, [modelingSend]) }, [modelingSend])
// Give the state back to the editorManager.
useEffect(() => {
editorManager.modelingSend = modelingSend
}, [modelingSend])
useEffect(() => {
editorManager.modelingEvent = modelingState.event
}, [modelingState.event])
useEffect(() => {
editorManager.selectionRanges = modelingState.context.selectionRanges
}, [modelingState.context.selectionRanges])
useStateMachineCommands({ useStateMachineCommands({
machineId: 'modeling', machineId: 'modeling',
state: modelingState, state: modelingState,

View File

@ -1,13 +1,8 @@
import { undo, redo } from '@codemirror/commands'
import ReactCodeMirror from '@uiw/react-codemirror' import ReactCodeMirror from '@uiw/react-codemirror'
import { TEST } from 'env' import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo } from 'react'
import { useStore } from 'useStore'
import { processCodeMirrorRanges } from 'lib/selections'
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search' import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
import { lineHighlightField } from 'editor/highlightextension' import { lineHighlightField } from 'editor/highlightextension'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
@ -29,7 +24,7 @@ import {
historyKeymap, historyKeymap,
history, history,
} from '@codemirror/commands' } from '@codemirror/commands'
import { lintGutter, lintKeymap, linter } from '@codemirror/lint' import { lintGutter, lintKeymap } from '@codemirror/lint'
import { import {
foldGutter, foldGutter,
foldKeymap, foldKeymap,
@ -39,25 +34,20 @@ import {
syntaxHighlighting, syntaxHighlighting,
defaultHighlightStyle, defaultHighlightStyle,
} from '@codemirror/language' } from '@codemirror/language'
import { useModelingContext } from 'hooks/useModelingContext'
import interact from '@replit/codemirror-interact' import interact from '@replit/codemirror-interact'
import { engineCommandManager, sceneInfra, kclManager } from 'lib/singletons' import { kclManager, editorManager, codeManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import makeUrlPathRelative from 'lib/makeUrlPathRelative' import makeUrlPathRelative from 'lib/makeUrlPathRelative'
import { useLspContext } from 'components/LspProvider' import { useLspContext } from 'components/LspProvider'
import { Prec, EditorState, Extension, SelectionRange } from '@codemirror/state' import { Prec, EditorState, Extension } from '@codemirror/state'
import { import {
closeBrackets, closeBrackets,
closeBracketsKeymap, closeBracketsKeymap,
completionKeymap, completionKeymap,
hasNextSnippetField,
} from '@codemirror/autocomplete' } from '@codemirror/autocomplete'
import { kclErrorsToDiagnostics } from 'lang/errors'
export const editorShortcutMeta = { export const editorShortcutMeta = {
formatCode: { formatCode: {
@ -77,13 +67,6 @@ export const KclEditorPane = () => {
context.app.theme.current === Themes.System context.app.theme.current === Themes.System
? getSystemTheme() ? getSystemTheme()
: context.app.theme.current : context.app.theme.current
const { editorView, setEditorView, isShiftDown } = useStore((s) => ({
editorView: s.editorView,
setEditorView: s.setEditorView,
isShiftDown: s.isShiftDown,
}))
const { editorCode, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() })
const { copilotLSP, kclLSP } = useLspContext() const { copilotLSP, kclLSP } = useLspContext()
const navigate = useNavigate() const navigate = useNavigate()
@ -96,90 +79,15 @@ export const KclEditorPane = () => {
useHotkeys('mod+z', (e) => { useHotkeys('mod+z', (e) => {
e.preventDefault() e.preventDefault()
if (editorView) { editorManager.undo()
undo(editorView)
}
}) })
useHotkeys('mod+shift+z', (e) => { useHotkeys('mod+shift+z', (e) => {
e.preventDefault() e.preventDefault()
if (editorView) { editorManager.redo()
redo(editorView)
}
}) })
const { const textWrapping = context.textEditor.textWrapping
context: { selectionRanges }, const cursorBlinking = context.textEditor.blinkingCursor
send,
state,
} = useModelingContext()
const { settings } = useSettingsAuthContext()
const textWrapping = settings.context.textEditor.textWrapping
const cursorBlinking = settings.context.textEditor.blinkingCursor
const { commandBarSend } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable()
const lastSelection = useRef('')
const onUpdate = (viewUpdate: ViewUpdate) => {
// If we are just fucking around in a snippet, return early and don't
// trigger stuff below that might cause the component to re-render.
// Otherwise we will not be able to tab thru the snippet portions.
// We explicitly dont check HasPrevSnippetField because we always add
// a ${} to the end of the function so that's fine.
if (hasNextSnippetField(viewUpdate.view.state)) {
return
}
if (!editorView) {
setEditorView(viewUpdate.view)
}
const selString = stringifyRanges(
viewUpdate?.state?.selection?.ranges || []
)
if (selString === lastSelection.current) {
// onUpdate is noisy and is fired a lot by extensions
// since we're only interested in selections changes we can ignore most of these.
return
}
lastSelection.current = selString
if (
// TODO find a less lazy way of getting the last
Date.now() - useStore.getState().lastCodeMirrorSelectionUpdatedFromScene <
150
)
return // update triggered by scene selection
if (sceneInfra.selected) return // mid drag
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
]
if (ignoreEvents.includes(state.event.type)) return
const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges,
isShiftDown,
})
if (!eventInfo) return
const deterministicEventInfo = {
...eventInfo,
engineEvents: eventInfo.engineEvents.map((e) => ({
...e,
cmd_id: 'static',
})),
}
const stringEvent = JSON.stringify(deterministicEventInfo)
if (
stringEvent === lastEvent.current.event &&
Date.now() - lastEvent.current.time < 500
)
return // don't repeat events
lastEvent.current = { event: stringEvent, time: Date.now() }
send(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) =>
engineCommandManager.sendSceneCommand(event)
)
}
const editorExtensions = useMemo(() => { const editorExtensions = useMemo(() => {
const extensions = [ const extensions = [
@ -202,7 +110,7 @@ export const KclEditorPane = () => {
{ {
key: 'Meta-k', key: 'Meta-k',
run: () => { run: () => {
commandBarSend({ type: 'Open' }) editorManager.commandBarSend({ type: 'Open' })
return false return false
}, },
}, },
@ -216,11 +124,7 @@ export const KclEditorPane = () => {
{ {
key: editorShortcutMeta.convertToVariable.codeMirror, key: editorShortcutMeta.convertToVariable.codeMirror,
run: () => { run: () => {
if (convertEnabled) { return editorManager.convertToVariable()
convertCallback()
return true
}
return false
}, },
}, },
]), ]),
@ -233,9 +137,6 @@ export const KclEditorPane = () => {
if (!TEST) { if (!TEST) {
extensions.push( extensions.push(
lintGutter(), lintGutter(),
linter((_view: EditorView) => {
return kclErrorsToDiagnostics(errors)
}),
lineNumbers(), lineNumbers(),
highlightActiveLineGutter(), highlightActiveLineGutter(),
highlightSpecialChars(), highlightSpecialChars(),
@ -288,13 +189,10 @@ export const KclEditorPane = () => {
} }
return extensions return extensions
}, [ }, [kclLSP, copilotLSP, textWrapping.current, cursorBlinking.current])
kclLSP,
copilotLSP, let debounceTimer: ReturnType<typeof setTimeout> | null = null
textWrapping.current, const updateDelay = 100
cursorBlinking.current,
convertCallback,
])
return ( return (
<div <div
@ -302,18 +200,26 @@ export const KclEditorPane = () => {
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')} className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
> >
<ReactCodeMirror <ReactCodeMirror
value={editorCode} value={codeManager.code}
extensions={editorExtensions} extensions={editorExtensions}
onUpdate={onUpdate}
theme={theme} theme={theme}
onCreateEditor={(_editorView) => setEditorView(_editorView)} onCreateEditor={(_editorView) =>
editorManager.setEditorView(_editorView)
}
onUpdate={(view: ViewUpdate) => {
// debounce the view update.
// otherwise it is laggy for typing.
if (debounceTimer) {
clearTimeout(debounceTimer)
}
debounceTimer = setTimeout(() => {
editorManager.handleOnViewUpdate(view)
}, updateDelay)
}}
indentWithTab={false} indentWithTab={false}
basicSetup={false} basicSetup={false}
/> />
</div> </div>
) )
} }
function stringifyRanges(ranges: readonly SelectionRange[]): string {
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
}

View File

@ -2,7 +2,9 @@ import { processMemory } from './MemoryPane'
import { enginelessExecutor } from '../../../lib/testHelpers' import { enginelessExecutor } from '../../../lib/testHelpers'
import { initPromise, parse } from '../../../lang/wasm' import { initPromise, parse } from '../../../lang/wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('processMemory', () => { describe('processMemory', () => {
it('should grab the values and remove and geo data', async () => { it('should grab the values and remove and geo data', async () => {

238
src/editor/manager.ts Normal file
View File

@ -0,0 +1,238 @@
import { hasNextSnippetField } from '@codemirror/autocomplete'
import { EditorView, ViewUpdate } from '@codemirror/view'
import { EditorSelection, SelectionRange } from '@codemirror/state'
import { engineCommandManager, sceneInfra } from 'lib/singletons'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { Selections, processCodeMirrorRanges, Selection } from 'lib/selections'
import { undo, redo } from '@codemirror/commands'
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { addLineHighlight } from './highlightextension'
import { setDiagnostics, Diagnostic } from '@codemirror/lint'
export default class EditorManager {
private _editorView: EditorView | null = null
private _isShiftDown: boolean = false
private _selectionRanges: Selections = {
otherSelections: [],
codeBasedSelections: [],
}
private _lastSelectionEvent: number | null = null
private _lastSelection: string = ''
private _lastEvent: { event: string; time: number } | null = null
private _modelingSend: (eventInfo: ModelingMachineEvent) => void = () => {}
private _modelingEvent: ModelingMachineEvent | null = null
private _commandBarSend: (eventInfo: CommandBarMachineEvent) => void =
() => {}
private _convertToVariableEnabled: boolean = false
private _convertToVariableCallback: () => void = () => {}
private _highlightRange: [number, number] = [0, 0]
setEditorView(editorView: EditorView) {
this._editorView = editorView
}
get editorView(): EditorView | null {
return this._editorView
}
get isShiftDown(): boolean {
return this._isShiftDown
}
setIsShiftDown(isShiftDown: boolean) {
this._isShiftDown = isShiftDown
}
set selectionRanges(selectionRanges: Selections) {
this._selectionRanges = selectionRanges
}
set lastSelectionEvent(time: number) {
this._lastSelectionEvent = time
}
set modelingSend(send: (eventInfo: ModelingMachineEvent) => void) {
this._modelingSend = send
}
set modelingEvent(event: ModelingMachineEvent) {
this._modelingEvent = event
}
setCommandBarSend(send: (eventInfo: CommandBarMachineEvent) => void) {
this._commandBarSend = send
}
commandBarSend(eventInfo: CommandBarMachineEvent): void {
return this._commandBarSend(eventInfo)
}
get highlightRange(): [number, number] {
return this._highlightRange
}
setHighlightRange(selection: Selection['range']): void {
this._highlightRange = selection
const editorView = this.editorView
const safeEnd = Math.min(
selection[1],
editorView?.state.doc.length || selection[1]
)
if (editorView) {
editorView.dispatch({
effects: addLineHighlight.of([selection[0], safeEnd]),
})
}
}
setDiagnostics(diagnostics: Diagnostic[]): void {
if (!this.editorView) return
this.editorView.dispatch(setDiagnostics(this.editorView.state, diagnostics))
}
undo() {
if (this._editorView) {
undo(this._editorView)
}
}
redo() {
if (this._editorView) {
redo(this._editorView)
}
}
set convertToVariableEnabled(enabled: boolean) {
this._convertToVariableEnabled = enabled
}
set convertToVariableCallback(callback: () => void) {
this._convertToVariableCallback = callback
}
convertToVariable() {
if (this._convertToVariableEnabled) {
this._convertToVariableCallback()
return true
}
return false
}
selectRange(selections: Selections) {
if (selections.codeBasedSelections.length === 0) {
return
}
if (!this.editorView) {
return
}
let codeBasedSelections = []
for (const selection of selections.codeBasedSelections) {
codeBasedSelections.push(
EditorSelection.range(selection.range[0], selection.range[1])
)
}
codeBasedSelections.push(
EditorSelection.cursor(
selections.codeBasedSelections[
selections.codeBasedSelections.length - 1
].range[1]
)
)
this.editorView.dispatch({
selection: EditorSelection.create(codeBasedSelections, 1),
})
}
handleOnViewUpdate(viewUpdate: ViewUpdate): void {
// If we are just fucking around in a snippet, return early and don't
// trigger stuff below that might cause the component to re-render.
// Otherwise we will not be able to tab thru the snippet portions.
// We explicitly dont check HasPrevSnippetField because we always add
// a ${} to the end of the function so that's fine.
if (hasNextSnippetField(viewUpdate.view.state)) {
return
}
if (this.editorView === null) {
this.setEditorView(viewUpdate.view)
}
const selString = stringifyRanges(
viewUpdate?.state?.selection?.ranges || []
)
if (selString === this._lastSelection) {
// onUpdate is noisy and is fired a lot by extensions
// since we're only interested in selections changes we can ignore most of these.
return
}
this._lastSelection = selString
if (
this._lastSelectionEvent &&
Date.now() - this._lastSelectionEvent < 150
) {
return // update triggered by scene selection
}
if (sceneInfra.selected) {
return // mid drag
}
const ignoreEvents: ModelingMachineEvent['type'][] = [
'Equip Line tool',
'Equip tangential arc to',
]
if (!this._modelingEvent) {
return
}
if (ignoreEvents.includes(this._modelingEvent.type)) {
return
}
const eventInfo = processCodeMirrorRanges({
codeMirrorRanges: viewUpdate.state.selection.ranges,
selectionRanges: this._selectionRanges,
isShiftDown: this._isShiftDown,
})
if (!eventInfo) {
return
}
const deterministicEventInfo = {
...eventInfo,
engineEvents: eventInfo.engineEvents.map((e) => ({
...e,
cmd_id: 'static',
})),
}
const stringEvent = JSON.stringify(deterministicEventInfo)
if (
this._lastEvent &&
stringEvent === this._lastEvent.event &&
Date.now() - this._lastEvent.time < 500
) {
return // don't repeat events
}
this._lastEvent = { event: stringEvent, time: Date.now() }
this._modelingSend(eventInfo.modelingEvent)
eventInfo.engineEvents.forEach((event) =>
engineCommandManager.sendSceneCommand(event)
)
}
}
function stringifyRanges(ranges: readonly SelectionRange[]): string {
return ranges.map(({ to, from }) => `${to}->${from}`).join('&')
}

View File

@ -21,7 +21,7 @@ import { LanguageServerClient } from 'editor/plugins/lsp'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import { posToOffset } from 'editor/plugins/lsp/util' import { posToOffset } from 'editor/plugins/lsp/util'
import { Program, ProgramMemory } from 'lang/wasm' import { Program, ProgramMemory } from 'lang/wasm'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, editorManager, kclManager } from 'lib/singletons'
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength' import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse' import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse' import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
@ -57,6 +57,9 @@ export class LanguageServerPlugin implements PluginValue {
}, },
contentChanges: [{ text: code }], contentChanges: [{ text: code }],
}) })
if (editorManager.editorView) {
//editorManager.handleOnViewUpdate(editorManager.editorView)
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@ -357,15 +360,9 @@ export class LanguageServerPlugin implements PluginValue {
try { try {
switch (notification.method) { switch (notification.method) {
case 'textDocument/publishDiagnostics': case 'textDocument/publishDiagnostics':
const params = notification.params as PublishDiagnosticsParams //const params = notification.params as PublishDiagnosticsParams
this.processDiagnostics(params) // this is sometimes slower than our actual typing.
// Update the kcl errors pane. //this.processDiagnostics(params)
/*if (!kclManager.isExecuting) {
kclManager.kclErrors = lspDiagnosticsToKclErrors(
this.view.state.doc,
params.diagnostics
)
}*/
break break
case 'window/logMessage': case 'window/logMessage':
console.log( console.log(
@ -385,17 +382,6 @@ export class LanguageServerPlugin implements PluginValue {
// The server has updated the AST, we should update elsewhere. // The server has updated the AST, we should update elsewhere.
let updatedAst = notification.params as Program let updatedAst = notification.params as Program
console.log('[lsp]: Updated AST', updatedAst) console.log('[lsp]: Updated AST', updatedAst)
// Update the ast when we are not already executing.
/* if (!kclManager.isExecuting) {
kclManager.ast = updatedAst
// Execute the ast.
console.log('[lsp]: executing ast')
await kclManager.executeAst(updatedAst)
console.log('[lsp]: executed ast', kclManager.kclErrors)
let diagnostics = kclErrorsToDiagnostics(kclManager.kclErrors)
this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
console.log('[lsp]: updated diagnostics')
}*/
// Update the folding ranges, since the AST has changed. // Update the folding ranges, since the AST has changed.
// This is a hack since codemirror does not support async foldService. // This is a hack since codemirror does not support async foldService.

View File

@ -1,14 +1,9 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { useStore } from 'useStore' import { editorManager, engineCommandManager } from 'lib/singletons'
import { engineCommandManager } from 'lib/singletons'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { getEventForSelectWithPoint } from 'lib/selections' import { getEventForSelectWithPoint } from 'lib/selections'
export function useEngineConnectionSubscriptions() { export function useEngineConnectionSubscriptions() {
const { setHighlightRange, highlightRange } = useStore((s) => ({
setHighlightRange: s.setHighlightRange,
highlightRange: s.highlightRange,
}))
const { send, context } = useModelingContext() const { send, context } = useModelingContext()
useEffect(() => { useEffect(() => {
@ -21,12 +16,13 @@ export function useEngineConnectionSubscriptions() {
if (data?.entity_id) { if (data?.entity_id) {
const sourceRange = const sourceRange =
engineCommandManager.artifactMap?.[data.entity_id]?.range engineCommandManager.artifactMap?.[data.entity_id]?.range
setHighlightRange(sourceRange) editorManager.setHighlightRange(sourceRange)
} else if ( } else if (
!highlightRange || !editorManager.highlightRange ||
(highlightRange[0] !== 0 && highlightRange[1] !== 0) (editorManager.highlightRange[0] !== 0 &&
editorManager.highlightRange[1] !== 0)
) { ) {
setHighlightRange([0, 0]) editorManager.setHighlightRange([0, 0])
} }
}, },
}) })
@ -43,10 +39,5 @@ export function useEngineConnectionSubscriptions() {
unSubHover() unSubHover()
unSubClick() unSubClick()
} }
}, [ }, [engineCommandManager, context?.sketchEnginePathId])
engineCommandManager,
setHighlightRange,
highlightRange,
context?.sketchEnginePathId,
])
} }

View File

@ -1,4 +1,4 @@
import { useStore } from '../useStore' import { editorManager } from 'lib/singletons'
import { useEffect } from 'react' import { useEffect } from 'react'
// Kurt's note: codeMirror styling overrides were needed to make this work // Kurt's note: codeMirror styling overrides were needed to make this work
@ -6,20 +6,17 @@ import { useEffect } from 'react'
// search for code-mirror-override in the repo to find the relevant styles // search for code-mirror-override in the repo to find the relevant styles
export function useHotKeyListener() { export function useHotKeyListener() {
const { setIsShiftDown } = useStore((s) => ({
setIsShiftDown: s.setIsShiftDown,
}))
const keyName = 'Shift' const keyName = 'Shift'
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => const handleKeyDown = (event: KeyboardEvent) =>
event.key === keyName && setIsShiftDown(true) event.key === keyName && editorManager.setIsShiftDown(true)
const handleKeyUp = (event: KeyboardEvent) => const handleKeyUp = (event: KeyboardEvent) =>
event.key === keyName && setIsShiftDown(false) event.key === keyName && editorManager.setIsShiftDown(false)
window.addEventListener('keydown', handleKeyDown) window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp) window.addEventListener('keyup', handleKeyUp)
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp) window.removeEventListener('keyup', handleKeyUp)
} }
}, [setIsShiftDown]) })
} }

View File

@ -2,7 +2,7 @@ import {
SetVarNameModal, SetVarNameModal,
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { kclManager } from 'lib/singletons' import { editorManager, kclManager } from 'lib/singletons'
import { moveValueIntoNewVariable } from 'lang/modifyAst' import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -13,6 +13,11 @@ const getModalInfo = createSetVarNameModal(SetVarNameModal)
export function useConvertToVariable() { export function useConvertToVariable() {
const { context } = useModelingContext() const { context } = useModelingContext()
const [enable, setEnabled] = useState(false) const [enable, setEnabled] = useState(false)
useEffect(() => {
editorManager.convertToVariableEnabled = enable
}, [enable])
useEffect(() => { useEffect(() => {
const { isSafe, value } = isNodeSafeToReplace( const { isSafe, value } = isNodeSafeToReplace(
kclManager.ast, kclManager.ast,
@ -45,5 +50,7 @@ export function useConvertToVariable() {
} }
} }
editorManager.convertToVariableCallback = handleClick
return { enable, handleClick } return { enable, handleClick }
} }

View File

@ -2,12 +2,10 @@ import { KCLError } from './errors'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom' import { useLoaderData } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { codeManager, kclManager } from 'lib/singletons' import { codeManager, kclManager } from 'lib/singletons'
const KclContext = createContext({ const KclContext = createContext({
code: codeManager?.code || '', code: codeManager?.code || '',
editorCode: codeManager?.code || '',
programMemory: kclManager?.programMemory, programMemory: kclManager?.programMemory,
ast: kclManager?.ast, ast: kclManager?.ast,
isExecuting: kclManager?.isExecuting, isExecuting: kclManager?.isExecuting,
@ -30,7 +28,6 @@ export function KclContextProvider({
const { code: loadedCode } = useLoaderData() as IndexLoaderData const { code: loadedCode } = useLoaderData() as IndexLoaderData
// Both the code state and the editor state start off with the same code. // Both the code state and the editor state start off with the same code.
const [code, setCode] = useState(loadedCode || codeManager.code) const [code, setCode] = useState(loadedCode || codeManager.code)
const [editorCode, setEditorCode] = useState(code)
const [programMemory, setProgramMemory] = useState(kclManager.programMemory) const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
const [ast, setAst] = useState(kclManager.ast) const [ast, setAst] = useState(kclManager.ast)
@ -42,7 +39,6 @@ export function KclContextProvider({
useEffect(() => { useEffect(() => {
codeManager.registerCallBacks({ codeManager.registerCallBacks({
setCode, setCode,
setEditorCode,
}) })
kclManager.registerCallBacks({ kclManager.registerCallBacks({
setProgramMemory, setProgramMemory,
@ -54,15 +50,10 @@ export function KclContextProvider({
}) })
}, []) }, [])
const params = useParams()
useEffect(() => {
codeManager.setParams(params)
}, [params])
return ( return (
<KclContext.Provider <KclContext.Provider
value={{ value={{
code, code,
editorCode,
programMemory, programMemory,
ast, ast,
isExecuting, isExecuting,

View File

@ -1,6 +1,6 @@
import { executeAst } from 'useStore' import { executeAst } from 'useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { KCLError } from './errors' import { KCLError, kclErrorsToDiagnostics } from './errors'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EngineCommandManager } from './std/engineConnection' import { EngineCommandManager } from './std/engineConnection'
@ -17,7 +17,7 @@ import {
ExtrudeGroup, ExtrudeGroup,
} from 'lang/wasm' } from 'lang/wasm'
import { getNodeFromPath } from './queryAst' import { getNodeFromPath } from './queryAst'
import { codeManager } from 'lib/singletons' import { codeManager, editorManager } from 'lib/singletons'
export class KclManager { export class KclManager {
private _ast: Program = { private _ast: Program = {
@ -90,6 +90,8 @@ export class KclManager {
} }
set kclErrors(kclErrors) { set kclErrors(kclErrors) {
this._kclErrors = kclErrors this._kclErrors = kclErrors
let diagnostics = kclErrorsToDiagnostics(kclErrors)
editorManager.setDiagnostics(diagnostics)
this._kclErrorsCallBack(kclErrors) this._kclErrorsCallBack(kclErrors)
} }

View File

@ -1,7 +1,9 @@
import { KCLError } from './errors' import { KCLError } from './errors'
import { initPromise, parse } from './wasm' import { initPromise, parse } from './wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing AST', () => { describe('testing AST', () => {
test('5 + 6', () => { test('5 + 6', () => {

View File

@ -1,7 +1,9 @@
import { parse, initPromise } from './wasm' import { parse, initPromise } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing artifacts', () => { describe('testing artifacts', () => {
// Enable rotations #152 // Enable rotations #152

View File

@ -5,7 +5,7 @@ import { bracket } from 'lib/exampleKcl'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs' import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { Params } from 'react-router-dom' import { editorManager } from 'lib/singletons'
const PERSIST_CODE_TOKEN = 'persistCode' const PERSIST_CODE_TOKEN = 'persistCode'
@ -13,7 +13,7 @@ export default class CodeManager {
private _code: string = bracket private _code: string = bracket
private _updateState: (arg: string) => void = () => {} private _updateState: (arg: string) => void = () => {}
private _updateEditor: (arg: string) => void = () => {} private _updateEditor: (arg: string) => void = () => {}
private _params: Params<string> = {} private _currentFilePath: string | null = null
constructor() { constructor() {
if (isTauri()) { if (isTauri()) {
@ -45,19 +45,12 @@ export default class CodeManager {
return this._code return this._code
} }
registerCallBacks({ registerCallBacks({ setCode }: { setCode: (arg: string) => void }) {
setCode,
setEditorCode,
}: {
setCode: (arg: string) => void
setEditorCode: (arg: string) => void
}) {
this._updateState = setCode this._updateState = setCode
this._updateEditor = setEditorCode
} }
setParams(params: Params<string>) { updateCurrentFilePath(path: string) {
this._params = params this._currentFilePath = path
} }
// This updates the code state and calls the updateState function. // This updates the code state and calls the updateState function.
@ -70,11 +63,14 @@ export default class CodeManager {
// Update the code in the editor. // Update the code in the editor.
updateCodeEditor(code: string): void { updateCodeEditor(code: string): void {
if (this._code !== code) { const lastCode = this._code
this.code = code this.code = code
this._updateEditor(code)
}
this._updateEditor(code) this._updateEditor(code)
if (editorManager.editorView) {
editorManager.editorView.dispatch({
changes: { from: 0, to: lastCode.length, insert: code },
})
}
} }
// Update the code, state, and the code the code mirror editor sees. // Update the code, state, and the code the code mirror editor sees.
@ -91,8 +87,8 @@ export default class CodeManager {
setTimeout(() => { setTimeout(() => {
// Wait one event loop to give a chance for params to be set // Wait one event loop to give a chance for params to be set
// Save the file to disk // Save the file to disk
this._params.id && this._currentFilePath &&
writeTextFile(this._params.id, this.code).catch((err) => { writeTextFile(this._currentFilePath, this.code).catch((err) => {
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254) // TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err) console.error('error saving file', err)
toast.error('Error saving file, please check file permissions') toast.error('Error saving file, please check file permissions')

View File

@ -4,7 +4,9 @@ import { parse, ProgramMemory, SketchGroup, initPromise } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { KCLError } from './errors' import { KCLError } from './errors'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('test executor', () => { describe('test executor', () => {
it('test assigning two variables, the second summing with the first', async () => { it('test assigning two variables, the second summing with the first', async () => {

View File

@ -1,7 +1,9 @@
import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst' import { getNodePathFromSourceRange, getNodeFromPath } from './queryAst'
import { Identifier, parse, initPromise, Parameter } from './wasm' import { Identifier, parse, initPromise, Parameter } from './wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing getNodePathFromSourceRange', () => { describe('testing getNodePathFromSourceRange', () => {
it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => { it('test it gets the right path for a `lineTo` CallExpression within a SketchExpression', () => {

View File

@ -17,7 +17,9 @@ import {
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { getNodePathFromSourceRange } from './queryAst' import { getNodePathFromSourceRange } from './queryAst'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('Testing createLiteral', () => { describe('Testing createLiteral', () => {
it('should create a literal', () => { it('should create a literal', () => {

View File

@ -15,7 +15,9 @@ import {
createPipeSubstitution, createPipeSubstitution,
} from './modifyAst' } from './modifyAst'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('findAllPreviousVariables', () => { describe('findAllPreviousVariables', () => {
it('should find all previous variables', async () => { it('should find all previous variables', async () => {

View File

@ -1,7 +1,9 @@
import { parse, Program, recast, initPromise } from './wasm' import { parse, Program, recast, initPromise } from './wasm'
import fs from 'node:fs' import fs from 'node:fs'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('recast', () => { describe('recast', () => {
it('recasts a simple program', () => { it('recasts a simple program', () => {

View File

@ -25,7 +25,9 @@ const eachQuad: [number, [number, number]][] = [
[675, [1, -1]], [675, [1, -1]],
] ]
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing getYComponent', () => { describe('testing getYComponent', () => {
it('should return the vertical component of a vector correctly when given angles in each quadrant (and with angles < 0, or > 360)', () => { it('should return the vertical component of a vector correctly when given angles in each quadrant (and with angles < 0, or > 360)', () => {

View File

@ -8,7 +8,9 @@ import { getSketchSegmentFromSourceRange } from './sketchConstraints'
import { Selection } from 'lib/selections' import { Selection } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
// testing helper function // testing helper function
async function testingSwapSketchFnCall({ async function testingSwapSketchFnCall({

View File

@ -11,7 +11,9 @@ import { ToolTip } from '../../useStore'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing getConstraintType', () => { describe('testing getConstraintType', () => {
const helper = getConstraintTypeFromSourceHelper const helper = getConstraintTypeFromSourceHelper

View File

@ -1,7 +1,9 @@
import { parse, initPromise } from '../wasm' import { parse, initPromise } from '../wasm'
import { enginelessExecutor } from '../../lib/testHelpers' import { enginelessExecutor } from '../../lib/testHelpers'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing angledLineThatIntersects', () => { describe('testing angledLineThatIntersects', () => {
it('angledLineThatIntersects should intersect with another line', async () => { it('angledLineThatIntersects should intersect with another line', async () => {

View File

@ -1,6 +1,8 @@
import { lexer, initPromise } from './wasm' import { lexer, initPromise } from './wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('testing lexer', () => { describe('testing lexer', () => {
it('async lexer works too', async () => { it('async lexer works too', async () => {

View File

@ -25,6 +25,7 @@ import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow' import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value' export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -95,10 +96,15 @@ export const wasmUrl = () => {
// Initialise the wasm module. // Initialise the wasm module.
const initialise = async () => { const initialise = async () => {
const fullUrl = wasmUrl() try {
const input = await fetch(fullUrl) const fullUrl = wasmUrl()
const buffer = await input.arrayBuffer() const input = await fetch(fullUrl)
return init(buffer) const buffer = await input.arrayBuffer()
return await init(buffer)
} catch (e) {
console.log('Error initialising WASM', e)
throw e
}
} }
export const initPromise = initialise() export const initPromise = initialise()
@ -153,10 +159,6 @@ export const executor = async (
return _programMemory return _programMemory
} }
const getSettingsState = import('components/SettingsAuthProvider').then(
(module) => module.getSettingsState
)
export const _executor = async ( export const _executor = async (
node: Program, node: Program,
programMemory: ProgramMemory = { root: {}, return: null }, programMemory: ProgramMemory = { root: {}, return: null },
@ -164,8 +166,14 @@ export const _executor = async (
isMock: boolean isMock: boolean
): Promise<ProgramMemory> => { ): Promise<ProgramMemory> => {
try { try {
const baseUnit = let baseUnit = 'mm'
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm' if (!TEST) {
const getSettingsState = import('components/SettingsAuthProvider').then(
(module) => module.getSettingsState
)
baseUnit =
(await getSettingsState)()?.modeling.defaultUnit.current || 'mm'
}
const memory: ProgramMemory = await execute_wasm( const memory: ProgramMemory = await execute_wasm(
JSON.stringify(node), JSON.stringify(node),
JSON.stringify(programMemory), JSON.stringify(programMemory),

View File

@ -103,6 +103,7 @@ export const fileLoader: LoaderFunction = async ({
// Update both the state and the editor's code. // Update both the state and the editor's code.
// We explicitly do not write to the file here since we are loading from // We explicitly do not write to the file here since we are loading from
// the file system and not the editor. // the file system and not the editor.
codeManager.updateCurrentFilePath(currentFilePath)
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
kclManager.executeCode(true) kclManager.executeCode(true)

View File

@ -1,5 +1,6 @@
import { SceneEntities } from 'clientSideScene/sceneEntities' import { SceneEntities } from 'clientSideScene/sceneEntities'
import { SceneInfra } from 'clientSideScene/sceneInfra' import { SceneInfra } from 'clientSideScene/sceneInfra'
import EditorManager from 'editor/manager'
import { KclManager } from 'lang/KclSingleton' import { KclManager } from 'lang/KclSingleton'
import CodeManager from 'lang/codeManager' import CodeManager from 'lang/codeManager'
import { EngineCommandManager } from 'lang/std/engineConnection' import { EngineCommandManager } from 'lang/std/engineConnection'
@ -8,6 +9,7 @@ export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager() export const engineCommandManager = new EngineCommandManager()
// This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager) export const kclManager = new KclManager(engineCommandManager)
engineCommandManager.getAstCb = () => kclManager.ast engineCommandManager.getAstCb = () => kclManager.ast
@ -15,3 +17,6 @@ export const sceneInfra = new SceneInfra(engineCommandManager)
engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange
export const sceneEntitiesManager = new SceneEntities(engineCommandManager) export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
// This needs to be after sceneInfra and engineCommandManager are is created.
export const editorManager = new EditorManager()

View File

@ -1,7 +1,9 @@
import { Coords2d } from 'lang/std/sketch' import { Coords2d } from 'lang/std/sketch'
import { isPointsCCW, initPromise } from 'lang/wasm' import { isPointsCCW, initPromise } from 'lang/wasm'
beforeAll(() => initPromise) beforeAll(async () => {
await initPromise
})
describe('test isPointsCW', () => { describe('test isPointsCW', () => {
test('basic test', () => { test('basic test', () => {

View File

@ -16,6 +16,54 @@ export type CommandBarContext = {
argumentsToSubmit: { [x: string]: unknown } argumentsToSubmit: { [x: string]: unknown }
} }
export type CommandBarMachineEvent =
| { type: 'Open' }
| { type: 'Close' }
| { type: 'Clear' }
| {
type: 'Select command'
data: { command: Command }
}
| { type: 'Deselect command' }
| { type: 'Submit command'; data: { [x: string]: unknown } }
| {
type: 'Add argument'
data: { argument: CommandArgumentWithName<unknown> }
}
| {
type: 'Remove argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
}
| {
type: 'Edit argument'
data: { arg: CommandArgumentWithName<unknown> }
}
| {
type: 'Add commands'
data: { commands: Command[] }
}
| {
type: 'Remove commands'
data: { commands: Command[] }
}
| { type: 'Submit argument'; data: { [x: string]: unknown } }
| {
type: 'done.invoke.validateArguments'
data: { [x: string]: unknown }
}
| {
type: 'error.platform.validateArguments'
data: { message: string; arg: CommandArgumentWithName<unknown> }
}
| {
type: 'Find and select command'
data: { name: string; ownerMachine: string }
}
| {
type: 'Change current argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
}
export const commandBarMachine = createMachine( export const commandBarMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */ /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */
@ -227,53 +275,7 @@ export const commandBarMachine = createMachine(
}, },
}, },
schema: { schema: {
events: {} as events: {} as CommandBarMachineEvent,
| { type: 'Open' }
| { type: 'Close' }
| { type: 'Clear' }
| {
type: 'Select command'
data: { command: Command }
}
| { type: 'Deselect command' }
| { type: 'Submit command'; data: { [x: string]: unknown } }
| {
type: 'Add argument'
data: { argument: CommandArgumentWithName<unknown> }
}
| {
type: 'Remove argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
}
| {
type: 'Edit argument'
data: { arg: CommandArgumentWithName<unknown> }
}
| {
type: 'Add commands'
data: { commands: Command[] }
}
| {
type: 'Remove commands'
data: { commands: Command[] }
}
| { type: 'Submit argument'; data: { [x: string]: unknown } }
| {
type: 'done.invoke.validateArguments'
data: { [x: string]: unknown }
}
| {
type: 'error.platform.validateArguments'
data: { message: string; arg: CommandArgumentWithName<unknown> }
}
| {
type: 'Find and select command'
data: { name: string; ownerMachine: string }
}
| {
type: 'Change current argument'
data: { [x: string]: CommandArgumentWithName<unknown> }
},
}, },
preserveActionOrder: true, preserveActionOrder: true,
}, },

View File

@ -7,6 +7,7 @@ import {
sceneInfra, sceneInfra,
sceneEntitiesManager, sceneEntitiesManager,
engineCommandManager, engineCommandManager,
editorManager,
} from 'lib/singletons' } from 'lib/singletons'
import { import {
horzVertInfo, horzVertInfo,
@ -800,7 +801,7 @@ export const modelingMachine = createMachine(
sketchDetails.origin sketchDetails.origin
) )
}, },
'AST extrude': (_, event) => { 'AST extrude': async (_, event) => {
if (!event.data) return if (!event.data) return
const { selection, distance } = event.data const { selection, distance } = event.data
let ast = kclManager.ast let ast = kclManager.ast
@ -829,10 +830,12 @@ export const modelingMachine = createMachine(
? distance.variableIdentifierAst ? distance.variableIdentifierAst
: distance.valueAst : distance.valueAst
) )
// TODO not handling focusPath correctly I think const selections = await kclManager.updateAst(modifiedAst, true, {
kclManager.updateAst(modifiedAst, true, {
focusPath: pathToExtrudeArg, focusPath: pathToExtrudeArg,
}) })
if (selections) {
editorManager.selectRange(selections)
}
}, },
'conditionally equip line tool': (_, { type }) => { 'conditionally equip line tool': (_, { type }) => {
if (type === 'done.invoke.animate-to-face') { if (type === 'done.invoke.animate-to-face') {

View File

@ -1,13 +1,11 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { addLineHighlight, EditorView } from './editor/highlightextension'
import { import {
Program, Program,
_executor, _executor,
ProgramMemory, ProgramMemory,
programMemoryInit, programMemoryInit,
} from './lang/wasm' } from './lang/wasm'
import { Selection } from 'lib/selections'
import { enginelessExecutor } from './lib/testHelpers' import { enginelessExecutor } from './lib/testHelpers'
import { EngineCommandManager } from './lang/std/engineConnection' import { EngineCommandManager } from './lang/std/engineConnection'
import { KCLError } from './lang/errors' import { KCLError } from './lang/errors'
@ -55,12 +53,6 @@ export type PaneType =
| 'lspMessages' | 'lspMessages'
export interface StoreState { export interface StoreState {
editorView: EditorView | null
setEditorView: (editorView: EditorView) => void
highlightRange: [number, number]
setHighlightRange: (range: Selection['range']) => void
isShiftDown: boolean
setIsShiftDown: (isShiftDown: boolean) => void
mediaStream?: MediaStream mediaStream?: MediaStream
setMediaStream: (mediaStream: MediaStream) => void setMediaStream: (mediaStream: MediaStream) => void
isStreamReady: boolean isStreamReady: boolean
@ -92,34 +84,12 @@ export interface StoreState {
path: string path: string
}[] }[]
setHomeMenuItems: (items: { name: string; path: string }[]) => void setHomeMenuItems: (items: { name: string; path: string }[]) => void
lastCodeMirrorSelectionUpdatedFromScene: number
setLastCodeMirrorSelectionUpdatedFromScene: (time: number) => void
} }
export const useStore = create<StoreState>()( export const useStore = create<StoreState>()(
persist( persist(
(set, get) => { (set, get) => {
return { return {
editorView: null,
setEditorView: (editorView) => {
set({ editorView })
},
highlightRange: [0, 0],
setHighlightRange: (selection) => {
set({ highlightRange: selection })
const editorView = get().editorView
const safeEnd = Math.min(
selection[1],
editorView?.state.doc.length || selection[1]
)
if (editorView) {
editorView.dispatch({
effects: addLineHighlight.of([selection[0], safeEnd]),
})
}
},
isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
setMediaStream: (mediaStream) => set({ mediaStream }), setMediaStream: (mediaStream) => set({ mediaStream }),
isStreamReady: false, isStreamReady: false,
setIsStreamReady: (isStreamReady) => set({ isStreamReady }), setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
@ -159,9 +129,6 @@ export const useStore = create<StoreState>()(
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }), setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [], homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }), setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
lastCodeMirrorSelectionUpdatedFromScene: Date.now(),
setLastCodeMirrorSelectionUpdatedFromScene: (time) =>
set({ lastCodeMirrorSelectionUpdatedFromScene: time }),
} }
}, },
{ {

View File

@ -4,13 +4,14 @@
"paths": { "paths": {
"/*": ["src/*"] "/*": ["src/*"]
}, },
"types": ["vite/client", "@types/wicg-file-system-access", "node", "@wdio/globals/types"], "types": [
"target": "esnext", "vite/client",
"lib": [ "@types/wicg-file-system-access",
"dom", "node",
"dom.iterable", "@wdio/globals/types"
"esnext"
], ],
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -25,10 +26,6 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": ["src", "e2e", "./*.ts"],
"src",
"e2e",
"./*.ts"
],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -5,7 +5,5 @@
"moduleResolution": "Node", "moduleResolution": "Node",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"include": [ "include": ["vite.config.ts"]
"vite.config.ts" }
]
}

View File

@ -1,15 +1,9 @@
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import viteTsconfigPaths from 'vite-tsconfig-paths' import viteTsconfigPaths from 'vite-tsconfig-paths'
import eslint from 'vite-plugin-eslint' import eslint from 'vite-plugin-eslint'
import dns from 'dns'
import { defineConfig, configDefaults } from 'vitest/config' import { defineConfig, configDefaults } from 'vitest/config'
import version from 'vite-plugin-package-version' import version from 'vite-plugin-package-version'
// Only needed because we run Node < 17
// and we want to open `localhost` not `127.0.0.1` on server start
// reference: https://vitejs.dev/config/server-options.html#server-host
dns.setDefaultResultOrder('verbatim')
const config = defineConfig({ const config = defineConfig({
server: { server: {
open: true, open: true,
@ -25,32 +19,38 @@ const config = defineConfig({
forks: { forks: {
maxForks: 2, maxForks: 2,
minForks: 1, minForks: 1,
} },
}, },
setupFiles: 'src/setupTests.ts', setupFiles: ['src/setupTests.ts', '@vitest/web-worker'],
environment: 'happy-dom', environment: 'happy-dom',
coverage: { coverage: {
provider: 'istanbul' // or 'v8' provider: 'istanbul', // or 'v8'
}, },
exclude: [...configDefaults.exclude, '**/e2e/playwright/**/*'], exclude: [...configDefaults.exclude, '**/e2e/playwright/**/*'],
deps: { deps: {
inline: ['vitest-canvas-mock'] optimizer: {
} web: {
include: ['vitest-canvas-mock'],
},
},
},
clearMocks: true,
restoreMocks: true,
mockReset: true,
reporters: process.env.GITHUB_ACTIONS
? ['dot', 'github-actions']
: ['verbose', 'hanging-process'],
testTimeout: 1000,
hookTimeout: 1000,
teardownTimeout: 1000,
}, },
build: { build: {
outDir: 'build', outDir: 'build',
}, },
plugins: [ plugins: [react(), viteTsconfigPaths(), eslint(), version()],
react(),
viteTsconfigPaths(),
eslint(),
version(),
],
worker: { worker: {
plugins: () => [ plugins: () => [viteTsconfigPaths()],
viteTsconfigPaths(), },
],
}
}) })
export default config export default config

View File

@ -1703,9 +1703,9 @@
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/object-schema@^2.0.2": "@humanwhocodes/object-schema@^2.0.2":
version "2.0.2" version "2.0.3"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@iarna/toml@^2.2.5": "@iarna/toml@^2.2.5":
version "2.2.5" version "2.2.5"
@ -2360,19 +2360,14 @@
integrity sha512-awNxydYSU+E2vL7EiOAMtiSOfL5gUM5X4YSE2A92qpxDJQ/rXz6oMPYBFDcDywlUmvIDI6zsqgq17cGm5CITQw== integrity sha512-awNxydYSU+E2vL7EiOAMtiSOfL5gUM5X4YSE2A92qpxDJQ/rXz6oMPYBFDcDywlUmvIDI6zsqgq17cGm5CITQw==
"@types/eslint@^8.4.5": "@types/eslint@^8.4.5":
version "8.44.1" version "8.56.10"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.1.tgz#d1811559bb6bcd1a76009e3f7883034b78a0415e" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d"
integrity sha512-XpNDc4Z5Tb4x+SW1MriMVeIsMoONHCkWFMkR/aPJbzEsxqHy+4Glu/BqTdPrApfDeMaXbtNh6bseNgl5KaWrSg== integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==
dependencies: dependencies:
"@types/estree" "*" "@types/estree" "*"
"@types/json-schema" "*" "@types/json-schema" "*"
"@types/estree@*": "@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/estree@1.0.5", "@types/estree@^1.0.0":
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
@ -2417,7 +2412,12 @@
expect "^29.0.0" expect "^29.0.0"
pretty-format "^29.0.0" pretty-format "^29.0.0"
"@types/json-schema@*", "@types/json-schema@^7.0.9": "@types/json-schema@*":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/json-schema@^7.0.9":
version "7.0.12" version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
@ -2764,6 +2764,13 @@
loupe "^2.3.7" loupe "^2.3.7"
pretty-format "^29.7.0" pretty-format "^29.7.0"
"@vitest/web-worker@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@vitest/web-worker/-/web-worker-1.5.0.tgz#9de0e20b4a9cfed2a4958d757bd0594a76a2d4e9"
integrity sha512-WxX5VAgp8knJGTZknTDUICAVtNiDsBDQN1z/ldsDFqhRUFp09WLhRxWnKqCQc+CXk1W+kpUZmWDjxaD3i8QBmA==
dependencies:
debug "^4.3.4"
"@wdio/cli@^8.24.3": "@wdio/cli@^8.24.3":
version "8.24.3" version "8.24.3"
resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-8.24.3.tgz#305adc66cd2264ed4ef4549266bbb327826e10ef" resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-8.24.3.tgz#305adc66cd2264ed4ef4549266bbb327826e10ef"
@ -5080,11 +5087,12 @@ find-up@^6.3.0:
path-exists "^5.0.0" path-exists "^5.0.0"
flat-cache@^3.0.4: flat-cache@^3.0.4:
version "3.0.4" version "3.2.0"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
dependencies: dependencies:
flatted "^3.1.0" flatted "^3.2.9"
keyv "^4.5.3"
rimraf "^3.0.2" rimraf "^3.0.2"
flat@^5.0.2: flat@^5.0.2:
@ -5092,10 +5100,10 @@ flat@^5.0.2:
resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
flatted@^3.1.0: flatted@^3.2.9:
version "3.2.7" version "3.3.1"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
flux@^4.0.1: flux@^4.0.1:
version "4.0.4" version "4.0.4"
@ -5436,9 +5444,9 @@ globals@^11.1.0:
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^13.19.0: globals@^13.19.0:
version "13.20.0" version "13.24.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
dependencies: dependencies:
type-fest "^0.20.2" type-fest "^0.20.2"
@ -8482,9 +8490,9 @@ tinybench@^2.5.1:
integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA== integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==
tinypool@^0.8.3: tinypool@^0.8.3:
version "0.8.3" version "0.8.4"
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.3.tgz#e17d0a5315a7d425f875b05f7af653c225492d39" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.4.tgz#e217fe1270d941b39e98c625dcecebb1408c9aa8"
integrity sha512-Ud7uepAklqRH1bvwy22ynrliC7Dljz7Tm8M/0RBUW+YRa4YHhZ6e4PpgE+fu1zr/WqB1kbeuVrdfeuyIBpy4tw== integrity sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==
tinyspy@^2.2.0: tinyspy@^2.2.0:
version "2.2.1" version "2.2.1"