refactor code storage (#2144)

* refactor code storage

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

* typo

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

* updates

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

* for now dont do onupdate its lagging the editor

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

* way smaller delay

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

* turn abck on on update

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

* dont be fancy

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

* fix linter

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

* updates

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

* empty

* empty

* good things

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

* empty

* empty

* updates

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

* updates

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

* updates

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

* updates

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

* fixes

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

* make less flakey

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

* go abck to errors for now

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-17 20:18:07 -07:00
committed by GitHub
parent 168fed038d
commit 3f8c4e7b5a
32 changed files with 314 additions and 370 deletions

View File

@ -560,7 +560,7 @@ test('Auto complete works', async ({ page }) => {
// expect there to be three auto complete options
await expect(page.locator('.cm-completionLabel')).toHaveCount(3)
await page.getByText('startSketchOn').click()
await page.keyboard.type("'XY'")
await page.keyboard.type("'XZ'")
await page.keyboard.press('Tab')
await page.keyboard.press('Enter')
await page.keyboard.type(' |> startProfi')
@ -570,6 +570,7 @@ test('Auto complete works', async ({ page }) => {
await page.keyboard.press('Enter') // accepting the auto complete, not a new line
await page.keyboard.press('Tab')
await page.keyboard.type('12')
await page.keyboard.press('Tab')
await page.keyboard.press('Tab')
await page.keyboard.press('Tab')
@ -592,8 +593,8 @@ test('Auto complete works', async ({ page }) => {
await expect(page.locator('.cm-completionLabel')).not.toBeVisible()
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('XY')
|> startProfileAt([3.14, 3.14], %)
.toHaveText(`const part001 = startSketchOn('XZ')
|> startProfileAt([3.14, 12], %)
|> xLine(5, %) // lin`)
})
@ -1239,7 +1240,7 @@ fn yohey = (pos) => {
},
selectionsSnippets
)
await page.setViewportSize({ width: 1200, height: 500 })
await page.setViewportSize({ width: 1200, height: 1000 })
await page.goto('/')
await u.waitForAuthSkipAppStart()

View File

@ -336,10 +336,7 @@ const part001 = startSketchOn('-XZ')
}
})
test('extrude on each default plane should be stable', async ({
page,
context,
}) => {
const extrudeDefaultPlane = async (context: any, page: any, plane: string) => {
await context.addInitScript(async () => {
localStorage.setItem(
'SETTINGS_PERSIST_KEY',
@ -356,8 +353,8 @@ test('extrude on each default plane should be stable', async ({
})
)
})
const u = getUtils(page)
const makeCode = (plane = 'XY') => `const part001 = startSketchOn('${plane}')
const code = `const part001 = startSketchOn('${plane}')
|> startProfileAt([7.00, 4.40], %)
|> line([6.60, -0.20], %)
|> line([2.80, 5.00], %)
@ -366,9 +363,11 @@ test('extrude on each default plane should be stable', async ({
|> close(%)
|> extrude(10.00, %)
`
await context.addInitScript(async (code) => {
await page.addInitScript(async (code: string) => {
localStorage.setItem('persistCode', code)
}, makeCode('XY'))
})
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
@ -378,14 +377,11 @@ test('extrude on each default plane should be stable', async ({
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await page.waitForTimeout(200)
const runSnapshotsForOtherPlanes = async (plane = 'XY') => {
// clear code
await u.removeCurrentCode()
// add makeCode('XZ')
await u.openAndClearDebugPanel()
await u.doAndWaitForImageDiff(
() => page.locator('.cm-content').fill(makeCode(plane)),
() => page.locator('.cm-content').fill(code),
200
)
// wait for execution done
@ -398,14 +394,30 @@ test('extrude on each default plane should be stable', async ({
})
await u.openKclCodePanel()
}
await runSnapshotsForOtherPlanes('XY')
await runSnapshotsForOtherPlanes('-XY')
test.describe('extrude on default planes should be stable', () => {
test('XY', async ({ page, context }) => {
await extrudeDefaultPlane(context, page, 'XY')
})
await runSnapshotsForOtherPlanes('XZ')
await runSnapshotsForOtherPlanes('-XZ')
test('XZ', async ({ page, context }) => {
await extrudeDefaultPlane(context, page, 'XZ')
})
await runSnapshotsForOtherPlanes('YZ')
await runSnapshotsForOtherPlanes('-YZ')
test('YZ', async ({ page, context }) => {
await extrudeDefaultPlane(context, page, 'YZ')
})
test('-XY', async ({ page, context }) => {
await extrudeDefaultPlane(context, page, '-XY')
})
test('-XZ', async ({ page, context }) => {
await extrudeDefaultPlane(context, page, '-XZ')
})
test('-YZ', async ({ page, context }) => {
await extrudeDefaultPlane(context, page, '-YZ')
})
})
test('Draft segments should look right', async ({ page, context }) => {

View File

@ -52,7 +52,12 @@ import {
VariableDeclaration,
VariableDeclarator,
} from 'lang/wasm'
import { engineCommandManager, kclManager, sceneInfra } from 'lib/singletons'
import {
engineCommandManager,
kclManager,
sceneInfra,
codeManager,
} from 'lib/singletons'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst, useStore } from 'useStore'
import {
@ -542,7 +547,7 @@ export class SceneEntities {
return
}
await kclManager.executeAstMock(modifiedAst, { updates: 'code' })
await kclManager.executeAstMock(modifiedAst)
this.setUpDraftSegment(
sketchPathToNode,
forward,
@ -637,9 +642,7 @@ export class SceneEntities {
spliceBetween: true,
})
addingNewSegmentStatus = 'pending'
await kclManager.executeAstMock(mod.modifiedAst, {
updates: 'code',
})
await kclManager.executeAstMock(mod.modifiedAst)
await this.tearDownSketch({ removeAxis: false })
this.setupSketch({
sketchPathToNode: pathToNode,
@ -784,9 +787,9 @@ export class SceneEntities {
;(async () => {
const code = recast(modifiedAst)
if (!draftInfo)
// don't want to mode the user's code yet as they have't committed to the change yet
// don't want to mod the user's code yet as they have't committed to the change yet
// plus this would be the truncated ast being recast, it would be wrong
kclManager.setCode(code, false)
codeManager.updateCodeStateEditor(code)
const { programMemory } = await executeAst({
ast: truncatedAst,
useFakeExecutor: true,

View File

@ -13,7 +13,7 @@ import styles from './FileTree.module.css'
import { sortProject } from 'lib/tauriFS'
import { FILE_EXT } from 'lib/constants'
import { CustomIcon } from './CustomIcon'
import { kclManager } from 'lib/singletons'
import { codeManager, kclManager } from 'lib/singletons'
import { useDocumentHasFocus } from 'hooks/useDocumentHasFocus'
import { useLspContext } from './LspProvider'
@ -171,10 +171,13 @@ const FileTreeItem = ({
if (fileOrDir.name?.endsWith(FILE_EXT) === false && project?.path) {
// Import non-kcl files
kclManager.setCodeAndExecute(
// We want to update both the state and editor here.
codeManager.updateCodeStateEditor(
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
kclManager.code
codeManager.code
)
codeManager.writeToFile()
kclManager.executeCode(true)
} else {
// Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null)

View File

@ -12,8 +12,12 @@ import { SetSelections, modelingMachine } from 'machines/modelingMachine'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { isCursorInSketchCommandRange } from 'lang/util'
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
import { useKclContext } from 'lang/KclProvider'
import {
kclManager,
sceneInfra,
engineCommandManager,
codeManager,
} from 'lib/singletons'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import {
angleBetweenInfo,
@ -38,7 +42,7 @@ import {
getSketchQuaternion,
} from 'clientSideScene/sceneEntities'
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
import { Program, coreDump, parse } from 'lang/wasm'
import { Program, coreDump } from 'lang/wasm'
import { getNodePathFromSourceRange, isSingleCursorInPipe } from 'lang/queryAst'
import { TEST } from 'env'
import { exportFromEngine } from 'lib/exportFromEngine'
@ -74,7 +78,6 @@ export const ModelingMachineProvider = ({
},
},
} = useSettingsAuthContext()
const { code } = useKclContext()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
useSetupEngineManager(streamRef, token, theme.current)
@ -117,11 +120,7 @@ export const ModelingMachineProvider = ({
{
actions: {
'sketch exit execute': () => {
try {
kclManager.executeAst(parse(kclManager.code))
} catch (e) {
kclManager.executeAst()
}
kclManager.executeCode(true)
},
'Set mouse state': assign({
mouseState: (_, event) => event.data,
@ -270,7 +269,8 @@ export const ModelingMachineProvider = ({
if (selectionRanges.codeBasedSelections.length < 1) return false
const isPipe = isSketchPipe(selectionRanges)
if (isSelectionLastLine(selectionRanges, code)) return true
if (isSelectionLastLine(selectionRanges, codeManager.code))
return true
if (!isPipe) return false
return canExtrudeSelection(selectionRanges)
@ -294,7 +294,7 @@ export const ModelingMachineProvider = ({
const varDecIndex = sketchDetails.sketchPathToNode[1][0]
// remove body item at varDecIndex
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
await kclManager.executeAstMock(newAst, { updates: 'code' })
await kclManager.executeAstMock(newAst)
sceneInfra.setCallbacks({
onClick: () => {},
onDrag: () => {},
@ -309,7 +309,7 @@ export const ModelingMachineProvider = ({
kclManager.programMemory,
data.cap
)
await kclManager.executeAstMock(modifiedAst, { updates: 'code' })
await kclManager.executeAstMock(modifiedAst)
const forward = new Vector3(...data.zAxis)
const up = new Vector3(...data.yAxis)

View File

@ -57,10 +57,6 @@ import {
completionKeymap,
hasNextSnippetField,
} from '@codemirror/autocomplete'
import {
NetworkHealthState,
useNetworkStatus,
} from 'components/NetworkHealthIndicator'
import { kclErrorsToDiagnostics } from 'lang/errors'
export const editorShortcutMeta = {
@ -86,16 +82,14 @@ export const KclEditorPane = () => {
setEditorView: s.setEditorView,
isShiftDown: s.isShiftDown,
}))
const { code, errors } = useKclContext()
const { editorCode, errors } = useKclContext()
const lastEvent = useRef({ event: '', time: Date.now() })
const { copilotLSP, kclLSP } = useLspContext()
const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok
const navigate = useNavigate()
useEffect(() => {
if (typeof window === 'undefined') return
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
const onlineCallback = () => kclManager.executeCode(true)
window.addEventListener('online', onlineCallback)
return () => window.removeEventListener('online', onlineCallback)
}, [])
@ -126,19 +120,6 @@ export const KclEditorPane = () => {
const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable()
const onChange = async (newCode: string) => {
// 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 (editorView && hasNextSnippetField(editorView.state)) {
return
}
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
else kclManager.setCode(newCode)
}
const lastSelection = useRef('')
const onUpdate = (viewUpdate: ViewUpdate) => {
// If we are just fucking around in a snippet, return early and don't
@ -307,7 +288,13 @@ export const KclEditorPane = () => {
}
return extensions
}, [kclLSP, textWrapping.current, cursorBlinking.current, convertCallback])
}, [
kclLSP,
copilotLSP,
textWrapping.current,
cursorBlinking.current,
convertCallback,
])
return (
<div
@ -315,9 +302,8 @@ export const KclEditorPane = () => {
className={'absolute inset-0 ' + (cursorBlinking.current ? 'blink' : '')}
>
<ReactCodeMirror
value={code}
value={editorCode}
extensions={editorExtensions}
onChange={onChange}
onUpdate={onUpdate}
theme={theme}
onCreateEditor={(_editorView) => setEditorView(_editorView)}

View File

@ -138,7 +138,7 @@ export const SettingsAuthProviderBase = ({
id: `${event.type}.success`,
})
},
'Execute AST': () => kclManager.executeAst(),
'Execute AST': () => kclManager.executeCode(true),
persistSettings: (context) =>
saveSettings(context, loadedProject?.project?.path),
},

View File

@ -1,8 +1,4 @@
import {
completeFromList,
hasNextSnippetField,
snippetCompletion,
} from '@codemirror/autocomplete'
import { completeFromList, snippetCompletion } from '@codemirror/autocomplete'
import { setDiagnostics } from '@codemirror/lint'
import { Facet } from '@codemirror/state'
import { EditorView, Tooltip } from '@codemirror/view'
@ -25,7 +21,7 @@ import { LanguageServerClient } from 'editor/plugins/lsp'
import { Marked } from '@ts-stack/markdown'
import { posToOffset } from 'editor/plugins/lsp/util'
import { Program, ProgramMemory } from 'lang/wasm'
import { kclManager } from 'lib/singletons'
import { codeManager, kclManager } from 'lib/singletons'
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
@ -53,6 +49,7 @@ export class LanguageServerPlugin implements PluginValue {
private foldingRanges: LSP.FoldingRange[] | null = null
private _defferer = deferExecution((code: string) => {
try {
// Update the state (not the editor) with the new code.
this.client.textDocumentDidChange({
textDocument: {
uri: this.documentUri,
@ -83,21 +80,16 @@ export class LanguageServerPlugin implements PluginValue {
})
}
update({ docChanged, state }: ViewUpdate) {
update({ docChanged }: ViewUpdate) {
if (!docChanged) return
// 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.
// We only care about this for the 'kcl' plugin.
if (this.client.name === 'kcl' && hasNextSnippetField(state)) {
return
}
const newCode = this.view.state.doc.toString()
codeManager.code = newCode
codeManager.writeToFile()
kclManager.executeCode()
this.sendChange({
documentText: this.view.state.doc.toString(),
documentText: newCode,
})
}
@ -122,17 +114,6 @@ export class LanguageServerPlugin implements PluginValue {
async sendChange({ documentText }: { documentText: string }) {
if (!this.client.ready) return
if (documentText.length > 5000) {
// Clear out the text it thinks we have, large documents will throw a stack error.
// This is obviously not a good fix but it works for now til we figure
// out the stack limits in wasm and also rewrite the parser.
// Since this is only for hover and completions it will be fine,
// completions will still work for stdlib but hover will not.
// That seems like a fine trade-off for a working editor for the time
// being.
documentText = ''
}
this._defferer(documentText)
}
@ -372,17 +353,19 @@ export class LanguageServerPlugin implements PluginValue {
return completeFromList(options)(context)
}
processNotification(notification: LSP.NotificationMessage) {
async processNotification(notification: LSP.NotificationMessage) {
try {
switch (notification.method) {
case 'textDocument/publishDiagnostics':
const params = notification.params as PublishDiagnosticsParams
this.processDiagnostics(params)
// Update the kcl errors pane.
/*kclManager.kclErrors = lspDiagnosticsToKclErrors(
/*if (!kclManager.isExecuting) {
kclManager.kclErrors = lspDiagnosticsToKclErrors(
this.view.state.doc,
params.diagnostics
)*/
)
}*/
break
case 'window/logMessage':
console.log(
@ -402,9 +385,17 @@ export class LanguageServerPlugin implements PluginValue {
// The server has updated the AST, we should update elsewhere.
let updatedAst = notification.params as Program
console.log('[lsp]: Updated AST', updatedAst)
// Since we aren't using the lsp server for executing the program
// we don't update the ast here.
//kclManager.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.
// This is a hack since codemirror does not support async foldService.

View File

@ -3,7 +3,7 @@ import { useStore } from '../useStore'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme'
import { makeDefaultPlanes, parse } from 'lang/wasm'
import { makeDefaultPlanes } from 'lang/wasm'
export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
@ -40,9 +40,10 @@ export function useSetupEngineManager(
setIsStreamReady,
width: quadWidth,
height: quadHeight,
executeCode: (code?: string) => {
const _ast = parse(code || kclManager.code)
return kclManager.executeAst(_ast, true)
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
return kclManager.executeCode(true)
},
token,
theme,

View File

@ -3,10 +3,11 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { type IndexLoaderData } from 'lib/types'
import { useLoaderData } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { kclManager } from 'lib/singletons'
import { codeManager, kclManager } from 'lib/singletons'
const KclContext = createContext({
code: kclManager?.code || '',
code: codeManager?.code || '',
editorCode: codeManager?.code || '',
programMemory: kclManager?.programMemory,
ast: kclManager?.ast,
isExecuting: kclManager?.isExecuting,
@ -27,7 +28,10 @@ export function KclContextProvider({
// If we try to use this component anywhere but under the paths.FILE route it will fail
// Because useLoaderData assumes we are on within it's context.
const { code: loadedCode } = useLoaderData() as IndexLoaderData
const [code, setCode] = useState(loadedCode || kclManager.code)
// Both the code state and the editor state start off with the same code.
const [code, setCode] = useState(loadedCode || codeManager.code)
const [editorCode, setEditorCode] = useState(code)
const [programMemory, setProgramMemory] = useState(kclManager.programMemory)
const [ast, setAst] = useState(kclManager.ast)
const [isExecuting, setIsExecuting] = useState(false)
@ -36,8 +40,11 @@ export function KclContextProvider({
const [wasmInitFailed, setWasmInitFailed] = useState(false)
useEffect(() => {
kclManager.registerCallBacks({
codeManager.registerCallBacks({
setCode,
setEditorCode,
})
kclManager.registerCallBacks({
setProgramMemory,
setAst,
setLogs,
@ -49,12 +56,13 @@ export function KclContextProvider({
const params = useParams()
useEffect(() => {
kclManager.setParams(params)
codeManager.setParams(params)
}, [params])
return (
<KclContext.Provider
value={{
code,
editorCode,
programMemory,
ast,
isExecuting,

View File

@ -1,4 +1,4 @@
import { executeAst, executeCode } from 'useStore'
import { executeAst } from 'useStore'
import { Selections } from 'lib/selections'
import { KCLError } from './errors'
import { uuidv4 } from 'lib/utils'
@ -16,17 +16,10 @@ import {
SketchGroup,
ExtrudeGroup,
} from 'lang/wasm'
import { bracket } from 'lib/exampleKcl'
import { getNodeFromPath } from './queryAst'
import { Params } from 'react-router-dom'
import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs'
import { toast } from 'react-hot-toast'
const PERSIST_CODE_TOKEN = 'persistCode'
import { codeManager } from 'lib/singletons'
export class KclManager {
private _code = bracket
private _ast: Program = {
body: [],
start: 0,
@ -44,7 +37,6 @@ export class KclManager {
private _kclErrors: KCLError[] = []
private _isExecuting = false
private _wasmInitFailed = true
private _params: Params<string> = {}
engineCommandManager: EngineCommandManager
private _defferer = deferExecution((code: string) => {
@ -62,7 +54,6 @@ export class KclManager {
}, 600)
private _isExecutingCallback: (arg: boolean) => void = () => {}
private _codeCallBack: (arg: string) => void = () => {}
private _astCallBack: (arg: Program) => void = () => {}
private _programMemoryCallBack: (arg: ProgramMemory) => void = () => {}
private _logsCallBack: (arg: string[]) => void = () => {}
@ -78,28 +69,6 @@ export class KclManager {
this._astCallBack(ast)
}
get code() {
return this._code
}
set code(code) {
this._code = code
this._codeCallBack(code)
if (isTauri()) {
setTimeout(() => {
// Wait one event loop to give a chance for params to be set
// Save the file to disk
this._params.id &&
writeTextFile(this._params.id, code).catch((err) => {
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
})
})
} else {
safteLSSetItem(PERSIST_CODE_TOKEN, code)
}
}
get programMemory() {
return this._programMemory
}
@ -140,38 +109,15 @@ export class KclManager {
this._wasmInitFailedCallback(wasmInitFailed)
}
setParams(params: Params<string>) {
this._params = params
}
constructor(engineCommandManager: EngineCommandManager) {
this.engineCommandManager = engineCommandManager
if (isTauri()) {
this.code = ''
return
}
const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN) || ''
// TODO #819 remove zustand persistence logic in a few months
// short term migration, shouldn't make a difference for tauri app users
// anyway since that's filesystem based.
const zustandStore = JSON.parse(safeLSGetItem('store') || '{}')
if (storedCode === null && zustandStore?.state?.code) {
this.code = zustandStore.state.code
safteLSSetItem(PERSIST_CODE_TOKEN, this._code)
zustandStore.state.code = ''
safteLSSetItem('store', JSON.stringify(zustandStore))
} else if (storedCode === null) {
this.code = bracket
} else {
this.code = storedCode
}
this.ensureWasmInit().then(() => {
this.ast = this.safeParse(this.code) || this.ast
this.ast = this.safeParse(codeManager.code) || this.ast
})
}
registerCallBacks({
setCode,
setProgramMemory,
setAst,
setLogs,
@ -179,7 +125,6 @@ export class KclManager {
setIsExecuting,
setWasmInitFailed,
}: {
setCode: (arg: string) => void
setProgramMemory: (arg: ProgramMemory) => void
setAst: (arg: Program) => void
setLogs: (arg: string[]) => void
@ -187,7 +132,6 @@ export class KclManager {
setIsExecuting: (arg: boolean) => void
setWasmInitFailed: (arg: boolean) => void
}) {
this._codeCallBack = setCode
this._programMemoryCallBack = setProgramMemory
this._astCallBack = setAst
this._logsCallBack = setLogs
@ -227,12 +171,11 @@ export class KclManager {
private _cancelTokens: Map<number, boolean> = new Map()
async executeAst(
ast: Program = this._ast,
updateCode = false,
executionId?: number
) {
if (!this.engineCommandManager.engineConnection?.isReady()) return
// This NEVER updates the code, if you want to update the code DO NOT add to
// this function, too many other things that don't want it exist.
// just call to codeManager from wherever you want in other files.
async executeAst(ast: Program = this._ast, executionId?: number) {
await this?.engineCommandManager?.waitForReady
const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
@ -253,9 +196,6 @@ export class KclManager {
this.kclErrors = errors
this.programMemory = programMemory
this.ast = { ...ast }
if (updateCode) {
this.code = recast(ast)
}
this._executeCallback()
this.engineCommandManager.addCommandLog({
type: 'execution-done',
@ -263,22 +203,24 @@ export class KclManager {
})
this._cancelTokens.delete(currentExecutionId)
}
// NOTE: this always updates the code state and editor.
// DO NOT CALL THIS from codemirror ever.
async executeAstMock(
ast: Program = this._ast,
{
updates,
}: {
updates: 'none' | 'code' | 'codeAndArtifactRanges'
updates: 'none' | 'artifactRanges'
} = { updates: 'none' }
) {
await this.ensureWasmInit()
const newCode = recast(ast)
const newAst = this.safeParse(newCode)
if (!newAst) return
codeManager.updateCodeStateEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
await this?.engineCommandManager?.waitForReady
if (updates !== 'none') {
this.setCode(recast(ast))
}
this._ast = { ...newAst }
const { logs, errors, programMemory } = await executeAst({
@ -289,7 +231,7 @@ export class KclManager {
this._logs = logs
this._kclErrors = errors
this._programMemory = programMemory
if (updates !== 'codeAndArtifactRanges') return
if (updates !== 'artifactRanges') return
Object.entries(this.engineCommandManager.artifactMap).forEach(
([commandId, artifact]) => {
if (!artifact.pathToNode) return
@ -309,79 +251,32 @@ export class KclManager {
}
)
}
executeCode = async (code?: string, executionId?: number) => {
const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
if (this._cancelTokens.get(currentExecutionId)) {
this._cancelTokens.delete(currentExecutionId)
return
}
await this.ensureWasmInit()
await this?.engineCommandManager?.waitForReady
const result = await executeCode({
engineCommandManager: this.engineCommandManager,
code: code || this._code,
lastAst: this._ast,
force: false,
})
// Check the cancellation token for this execution before applying side effects
if (this._cancelTokens.get(currentExecutionId)) {
this._cancelTokens.delete(currentExecutionId)
return
}
if (!result.isChange) return
const { logs, errors, programMemory, ast } = result
enterEditMode(programMemory, this.engineCommandManager)
this.logs = logs
this.kclErrors = errors
this.programMemory = programMemory
this.ast = ast
if (code) this.code = code
this._cancelTokens.delete(currentExecutionId)
}
cancelAllExecutions() {
this._cancelTokens.forEach((_, key) => {
this._cancelTokens.set(key, true)
})
}
setCode(code: string, shouldWriteFile = true) {
if (shouldWriteFile) {
// use the normal code setter
this.code = code
return
}
this._code = code
this._codeCallBack(code)
}
setCodeAndExecute(code: string, shouldWriteFile = true) {
this.setCode(code, shouldWriteFile)
if (code.trim()) {
this._defferer(code)
return
}
this._ast = {
body: [],
start: 0,
end: 0,
nonCodeMeta: {
nonCodeNodes: {},
start: [],
},
}
this._programMemory = {
root: {},
return: null,
}
this.engineCommandManager.endSession()
executeCode(force?: boolean) {
// If we want to force it we don't want to defer it.
if (!force) return this._defferer(codeManager.code)
return this.executeAst()
}
format() {
const ast = this.safeParse(this.code)
const originalCode = codeManager.code
const ast = this.safeParse(originalCode)
if (!ast) return
this.code = recast(ast)
const code = recast(ast)
if (originalCode === code) return
// Update the code state and the editor.
codeManager.updateCodeStateEditor(code)
// Write back to the file system.
codeManager.writeToFile()
}
// There's overlapping responsibility between updateAst and executeAst.
// updateAst was added as it was used a lot before xState migration so makes the port easier.
// but should probably have think about which of the function to keep
// This always updates the code state and editor and writes to the file system.
async updateAst(
ast: Program,
execute: boolean,
@ -414,12 +309,17 @@ export class KclManager {
if (execute) {
// Call execute on the set ast.
await this.executeAst(astWithUpdatedSource, true)
// Update the code state and editor.
codeManager.updateCodeStateEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
await this.executeAst(astWithUpdatedSource)
} else {
// When we don't re-execute, we still want to update the program
// memory with the new ast. So we will hit the mock executor
// instead.
await this.executeAstMock(astWithUpdatedSource, { updates: 'code' })
// instead..
// Execute ast mock will update the code state and editor.
await this.executeAstMock(astWithUpdatedSource)
}
return returnVal
}
@ -443,16 +343,6 @@ export class KclManager {
}
}
function safeLSGetItem(key: string) {
if (typeof window === 'undefined') return null
return localStorage?.getItem(key)
}
function safteLSSetItem(key: string, value: string) {
if (typeof window === 'undefined') return
localStorage?.setItem(key, value)
}
function enterEditMode(
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager

115
src/lang/codeManager.ts Normal file
View File

@ -0,0 +1,115 @@
// A little class for updating the code state when we need to and explicitly
// NOT updating the code state when we don't need to.
// This prevents re-renders of the codemirror editor, when typing.
import { bracket } from 'lib/exampleKcl'
import { isTauri } from 'lib/isTauri'
import { writeTextFile } from '@tauri-apps/plugin-fs'
import toast from 'react-hot-toast'
import { Params } from 'react-router-dom'
const PERSIST_CODE_TOKEN = 'persistCode'
export default class CodeManager {
private _code: string = bracket
private _updateState: (arg: string) => void = () => {}
private _updateEditor: (arg: string) => void = () => {}
private _params: Params<string> = {}
constructor() {
if (isTauri()) {
this.code = ''
return
}
const storedCode = safeLSGetItem(PERSIST_CODE_TOKEN) || ''
// TODO #819 remove zustand persistence logic in a few months
// short term migration, shouldn't make a difference for tauri app users
// anyway since that's filesystem based.
const zustandStore = JSON.parse(safeLSGetItem('store') || '{}')
if (storedCode === null && zustandStore?.state?.code) {
this.code = zustandStore.state.code
zustandStore.state.code = ''
safeLSSetItem('store', JSON.stringify(zustandStore))
} else if (storedCode === null) {
this.code = bracket
} else {
this.code = storedCode
}
}
set code(code: string) {
this._code = code
}
get code(): string {
return this._code
}
registerCallBacks({
setCode,
setEditorCode,
}: {
setCode: (arg: string) => void
setEditorCode: (arg: string) => void
}) {
this._updateState = setCode
this._updateEditor = setEditorCode
}
setParams(params: Params<string>) {
this._params = params
}
// This updates the code state and calls the updateState function.
updateCodeState(code: string): void {
if (this._code !== code) {
this.code = code
this._updateState(code)
}
}
// Update the code in the editor.
updateCodeEditor(code: string): void {
if (this._code !== code) {
this.code = code
this._updateEditor(code)
}
this._updateEditor(code)
}
// Update the code, state, and the code the code mirror editor sees.
updateCodeStateEditor(code: string): void {
if (this._code !== code) {
this.code = code
this._updateState(code)
this._updateEditor(code)
}
}
async writeToFile() {
if (isTauri()) {
setTimeout(() => {
// Wait one event loop to give a chance for params to be set
// Save the file to disk
this._params.id &&
writeTextFile(this._params.id, this.code).catch((err) => {
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
console.error('error saving file', err)
toast.error('Error saving file, please check file permissions')
})
})
} else {
safeLSSetItem(PERSIST_CODE_TOKEN, this.code)
}
}
}
function safeLSGetItem(key: string) {
if (typeof window === 'undefined') return null
return localStorage?.getItem(key)
}
function safeLSSetItem(key: string, value: string) {
if (typeof window === 'undefined') return
localStorage?.setItem(key, value)
}

View File

@ -932,7 +932,7 @@ export class EngineCommandManager {
setIsStreamReady: (isStreamReady: boolean) => void
width: number
height: number
executeCode: (code?: string, force?: boolean) => void
executeCode: () => void
token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes>
theme?: Themes
@ -1007,7 +1007,7 @@ export class EngineCommandManager {
this.initPlanes().then(() => {
this.resolveReady()
setIsStreamReady(true)
executeCode(undefined, true)
executeCode()
})
},
onClose: () => {

View File

@ -22,7 +22,7 @@ import {
import makeUrlPathRelative from './makeUrlPathRelative'
import { join, sep } from '@tauri-apps/api/path'
import { readTextFile, stat } from '@tauri-apps/plugin-fs'
import { kclManager } from 'lib/singletons'
import { codeManager, kclManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import { invoke } from '@tauri-apps/api/core'
@ -100,7 +100,9 @@ export const fileLoader: LoaderFunction = async ({
const children = await invoke<FileEntry[]>('read_dir_recursive', {
path: projectPath,
})
kclManager.setCodeAndExecute(code, false)
// Update both the state and the editor's code.
codeManager.updateCodeStateEditor(code)
kclManager.executeCode(true)
// Set the file system manager to the project path
// So that WASM gets an updated path for operations

View File

@ -1,5 +1,6 @@
import { Models } from '@kittycad/lib'
import {
codeManager,
engineCommandManager,
kclManager,
sceneEntitiesManager,
@ -142,7 +143,7 @@ export function getEventForSegmentSelection(
// previous drags don't update ast for efficiency reasons
// So we want to make sure we have and updated ast with
// accurate source ranges
const updatedAst = parse(kclManager.code)
const updatedAst = parse(codeManager.code)
const node = getNodeFromPath<CallExpression>(
updatedAst,
pathToNode,
@ -192,7 +193,7 @@ export function handleSelectionBatch({
return {
codeMirrorSelection: EditorSelection.create(
[EditorSelection.cursor(kclManager.code.length)],
[EditorSelection.cursor(codeManager.code.length)],
0
),
engineEvents,

View File

@ -1,8 +1,11 @@
import { SceneEntities } from 'clientSideScene/sceneEntities'
import { SceneInfra } from 'clientSideScene/sceneInfra'
import { KclManager } from 'lang/KclSingleton'
import CodeManager from 'lang/codeManager'
import { EngineCommandManager } from 'lang/std/engineConnection'
export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager()
export const kclManager = new KclManager(engineCommandManager)

View File

@ -1,7 +1,7 @@
import { OnboardingButtons, useDismiss } from '.'
import { useEffect } from 'react'
import { bracket } from 'lib/exampleKcl'
import { kclManager } from 'lib/singletons'
import { codeManager, kclManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext'
import { APP_NAME } from 'lib/constants'
import { onboardingPaths } from './paths'
@ -11,12 +11,11 @@ export default function FutureWork() {
const dismiss = useDismiss()
useEffect(() => {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket)
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
// If the engine is ready, promptly execute the loaded code
kclManager.setCodeAndExecute(bracket)
} else {
// Otherwise, just set the code and wait for the connection to complete
kclManager.setCode(bracket)
kclManager.executeCode(true)
}
send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode

View File

@ -18,7 +18,7 @@ import { isTauri } from 'lib/isTauri'
import { useNavigate } from 'react-router-dom'
import { paths } from 'lib/paths'
import { useEffect } from 'react'
import { kclManager } from 'lib/singletons'
import { codeManager, kclManager } from 'lib/singletons'
import { join } from '@tauri-apps/api/path'
import { APP_NAME, PROJECT_ENTRYPOINT } from 'lib/constants'
@ -70,7 +70,9 @@ function OnboardingWithNewFile() {
className="mt-6"
dismiss={dismiss}
next={() => {
kclManager.setCodeAndExecute(bracket)
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor(bracket)
kclManager.executeCode(true)
next()
}}
nextText="Overwrite code and continue"
@ -93,7 +95,7 @@ function OnboardingWithNewFile() {
dismiss={dismiss}
next={() => {
void createAndOpenNewProject()
kclManager.setCode(bracket, false)
codeManager.updateCodeStateEditor(bracket)
dismiss()
}}
nextText="Make a new project"
@ -122,10 +124,11 @@ export default function Introduction() {
: ''
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.CAMERA)
const isStarterCode = kclManager.code === '' || kclManager.code === bracket
const currentCode = codeManager.code
const isStarterCode = currentCode === '' || currentCode === bracket
useEffect(() => {
if (kclManager.code === '') kclManager.setCode(bracket)
if (codeManager.code === '') codeManager.updateCodeStateEditor(bracket)
}, [])
return isStarterCode ? (

View File

@ -2,7 +2,7 @@ import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from 'useStore'
import { useEffect } from 'react'
import { kclManager } from 'lib/singletons'
import { codeManager, kclManager } from 'lib/singletons'
export default function Sketching() {
const buttonDownInStream = useStore((s) => s.buttonDownInStream)
@ -10,12 +10,11 @@ export default function Sketching() {
const next = useNextClick(onboardingPaths.FUTURE_WORK)
useEffect(() => {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor('')
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
// If the engine is ready, promptly execute the loaded code
kclManager.setCodeAndExecute('')
} else {
// Otherwise, just set the code and wait for the connection to complete
kclManager.setCode('')
kclManager.executeCode(true)
}
}, [])

View File

@ -2,7 +2,6 @@ import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { addLineHighlight, EditorView } from './editor/highlightextension'
import {
parse,
Program,
_executor,
ProgramMemory,
@ -175,74 +174,6 @@ export const useStore = create<StoreState>()(
)
)
export async function executeCode({
engineCommandManager,
code,
lastAst,
force,
}: {
code: string
lastAst: Program
engineCommandManager: EngineCommandManager
force?: boolean
}): Promise<
| {
logs: string[]
errors: KCLError[]
programMemory: ProgramMemory
ast: Program
isChange: true
}
| { isChange: false }
> {
let ast: Program
try {
ast = parse(code)
} catch (e) {
let errors: KCLError[] = []
let logs: string[] = [JSON.stringify(e)]
if (e instanceof KCLError) {
errors = [e]
logs = []
if (e.msg === 'file is empty') engineCommandManager.endSession()
}
return {
isChange: true,
logs,
errors,
programMemory: {
root: {},
return: null,
},
ast: {
start: 0,
end: 0,
body: [],
nonCodeMeta: {
nonCodeNodes: {},
start: [],
},
},
}
}
// Check if the ast we have is equal to the ast in the storage.
// If it is, we don't need to update the ast.
if (JSON.stringify(ast) === JSON.stringify(lastAst) && !force)
return { isChange: false }
const { logs, errors, programMemory } = await executeAst({
ast,
engineCommandManager,
})
return {
ast,
logs,
errors,
programMemory,
isChange: true,
}
}
export async function executeAst({
ast,
engineCommandManager,
@ -268,10 +199,6 @@ export async function executeAst({
: _executor(ast, programMemoryInit(), engineCommandManager, false))
await engineCommandManager.waitForAllCommands()
engineCommandManager.addCommandLog({
type: 'execution-done',
data: null,
})
return {
logs: [],
errors: [],