start of fixing changing files and cleaning up after execute (#897)
* start of fixing changing files and cleaning up after execute * stop constraints from leaving artifacts * don't write file on initial load
This commit is contained in:
21
src/App.tsx
21
src/App.tsx
@ -31,7 +31,6 @@ import { TextEditor } from 'components/TextEditor'
|
||||
import { Themes, getSystemTheme } from 'lib/theme'
|
||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||
import { engineCommandManager } from './lang/std/engineConnection'
|
||||
import { kclManager } from 'lang/KclSinglton'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
|
||||
export function App() {
|
||||
@ -82,26 +81,6 @@ export function App() {
|
||||
? 'opacity-40'
|
||||
: ''
|
||||
|
||||
// Use file code loaded from disk
|
||||
// on mount, and overwrite any locally-stored code
|
||||
useEffect(() => {
|
||||
if (isTauri() && loadedCode !== null) {
|
||||
if (kclManager.engineCommandManager.engineConnection?.isReady()) {
|
||||
// If the engine is ready, promptly execute the loaded code
|
||||
kclManager.setCodeAndExecute(loadedCode)
|
||||
} else {
|
||||
// Otherwise, just set the code and wait for the connection to complete
|
||||
kclManager.setCode(loadedCode)
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
// Clear code on unmount if in desktop app
|
||||
if (isTauri()) {
|
||||
kclManager.setCode('')
|
||||
}
|
||||
}
|
||||
}, [loadedCode])
|
||||
|
||||
useEngineConnectionSubscriptions()
|
||||
|
||||
const debounceSocketSend = throttle<EngineCommand>((message) => {
|
||||
|
@ -42,7 +42,7 @@ import CommandBarProvider from 'components/CommandBar'
|
||||
import { TEST, VITE_KC_SENTRY_DSN } from './env'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import ModelingMachineProvider from 'components/ModelingMachineProvider'
|
||||
import { KclContextProvider } from 'lang/KclSinglton'
|
||||
import { KclContextProvider, kclManager } from 'lang/KclSinglton'
|
||||
import FileMachineProvider from 'components/FileMachineProvider'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
|
||||
@ -207,6 +207,7 @@ const router = createBrowserRouter(
|
||||
projectPath + sep + PROJECT_ENTRYPOINT
|
||||
)
|
||||
const children = await readDir(projectPath, { recursive: true })
|
||||
kclManager.setCodeAndExecute(code, false)
|
||||
|
||||
return {
|
||||
code,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { parse, BinaryPart, Value, executor } from '../lang/wasm'
|
||||
import { parse, BinaryPart, Value } from '../lang/wasm'
|
||||
import {
|
||||
createIdentifier,
|
||||
createLiteral,
|
||||
@ -10,6 +10,7 @@ import { findAllPreviousVariables, PrevVariable } from '../lang/queryAst'
|
||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { kclManager, useKclContext } from 'lang/KclSinglton'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { executeAst } from 'useStore'
|
||||
|
||||
export const AvailableVars = ({
|
||||
onVarClick,
|
||||
@ -130,27 +131,29 @@ export function useCalc({
|
||||
if (!programMemory || !selectionRange) return
|
||||
const varInfo = findAllPreviousVariables(
|
||||
kclManager.ast,
|
||||
programMemory,
|
||||
kclManager.programMemory,
|
||||
selectionRange
|
||||
)
|
||||
setAvailableVarInfo(varInfo)
|
||||
}, [kclManager.ast, programMemory, selectionRange])
|
||||
}, [kclManager.ast, kclManager.programMemory, selectionRange])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const code = `const __result__ = ${value}\nshow(__result__)`
|
||||
const code = `const __result__ = ${value}`
|
||||
const ast = parse(code)
|
||||
const _programMem: any = { root: {}, return: null }
|
||||
availableVarInfo.variables.forEach(({ key, value }) => {
|
||||
_programMem.root[key] = { type: 'userVal', value, __meta: [] }
|
||||
})
|
||||
|
||||
executor(
|
||||
executeAst({
|
||||
ast,
|
||||
_programMem,
|
||||
engineCommandManager,
|
||||
kclManager.defaultPlanes
|
||||
).then((programMemory) => {
|
||||
defaultPlanes: kclManager.defaultPlanes,
|
||||
useFakeExecutor: true,
|
||||
programMemoryOverride: JSON.parse(
|
||||
JSON.stringify(kclManager.programMemory)
|
||||
),
|
||||
}).then(({ programMemory }) => {
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
@ -167,7 +170,7 @@ export function useCalc({
|
||||
setCalcResult('NAN')
|
||||
setValueNode(null)
|
||||
}
|
||||
}, [value])
|
||||
}, [value, availableVarInfo])
|
||||
|
||||
return {
|
||||
valueNode,
|
||||
@ -212,7 +215,10 @@ export const CreateNewVariable = ({
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor="create-new-variable" className="block mt-3 font-mono">
|
||||
<label
|
||||
htmlFor="create-new-variable"
|
||||
className="block mt-3 font-mono text-gray-900"
|
||||
>
|
||||
Create new variable
|
||||
</label>
|
||||
<div className="mt-1 flex gap-2 items-center">
|
||||
@ -223,6 +229,7 @@ export const CreateNewVariable = ({
|
||||
onChange={(e) => {
|
||||
setShouldCreateVariable(e.target.checked)
|
||||
}}
|
||||
className="bg-white text-gray-900"
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
|
@ -163,7 +163,6 @@ const FileTreeItem = ({
|
||||
|
||||
function openFile() {
|
||||
if (fileOrDir.children !== undefined) return // Don't open directories
|
||||
kclManager.setCode('')
|
||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||
closePanel()
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ export const ModelingMachineProvider = ({
|
||||
engineCommandManager.artifactMap[sketchEnginePathId] = {
|
||||
type: 'result',
|
||||
range: [startProfileAtCallExp.start, startProfileAtCallExp.end],
|
||||
commandType: 'extend_path',
|
||||
commandType: 'start_path',
|
||||
data: null,
|
||||
raw: {} as any,
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ export const SetAngleLengthModal = ({
|
||||
</label>
|
||||
<div className="mt-1 flex">
|
||||
<button
|
||||
className="border border-gray-300 px-2"
|
||||
className="border border-gray-300 px-2 text-gray-900"
|
||||
onClick={() => setSign(-sign)}
|
||||
>
|
||||
{sign > 0 ? '+' : '-'}
|
||||
@ -118,7 +118,7 @@ export const SetAngleLengthModal = ({
|
||||
type="text"
|
||||
name="val"
|
||||
id="val"
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1"
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono pl-1 text-gray-900"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
|
@ -87,7 +87,7 @@ export const GetInfoModal = ({
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white/90 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
@ -109,7 +109,7 @@ export const GetInfoModal = ({
|
||||
</label>
|
||||
<div className="mt-1 flex">
|
||||
<button
|
||||
className="border border-gray-300 px-2 mr-1"
|
||||
className="border border-gray-400 px-2 mr-1 text-gray-900"
|
||||
onClick={() => setSign(-sign)}
|
||||
>
|
||||
{sign > 0 ? '+' : '-'}
|
||||
@ -119,7 +119,7 @@ export const GetInfoModal = ({
|
||||
name="val"
|
||||
id="val"
|
||||
ref={inputRef}
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono"
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm text-gray-900 border-gray-300 rounded-md font-mono"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
@ -139,7 +139,7 @@ export const GetInfoModal = ({
|
||||
name="segName"
|
||||
id="segName"
|
||||
disabled={!isSegNameEditable}
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono"
|
||||
className="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm text-gray-900 border-gray-300 rounded-md font-mono"
|
||||
value={segName}
|
||||
onChange={(e) => {
|
||||
setSegName(e.target.value)
|
||||
|
@ -17,15 +17,7 @@ import { useStore } from 'useStore'
|
||||
import { processCodeMirrorRanges } from 'lib/selections'
|
||||
import { LanguageServerClient } from 'editor/lsp'
|
||||
import kclLanguage from 'editor/lsp/language'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
EditorView,
|
||||
addLineHighlight,
|
||||
lineHighlightField,
|
||||
} from 'editor/highlightextension'
|
||||
import { EditorView, lineHighlightField } from 'editor/highlightextension'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { kclErrToDiagnostic } from 'lang/errors'
|
||||
import { CSSRuleObject } from 'tailwindcss/types/config'
|
||||
@ -50,7 +42,6 @@ export const TextEditor = ({
|
||||
}: {
|
||||
theme: Themes.Light | Themes.Dark
|
||||
}) => {
|
||||
const pathParams = useParams()
|
||||
const { editorView, isLSPServerReady, setEditorView, setIsLSPServerReady } =
|
||||
useStore((s) => ({
|
||||
editorView: s.editorView,
|
||||
@ -113,18 +104,6 @@ export const TextEditor = ({
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (newCode: string) => {
|
||||
kclManager.setCodeAndExecute(newCode)
|
||||
if (isTauri() && pathParams.id) {
|
||||
// Save the file to disk
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
writeTextFile(pathParams.id, newCode).catch((err) => {
|
||||
// TODO: add Sentry 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')
|
||||
})
|
||||
}
|
||||
if (editorView) {
|
||||
editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) })
|
||||
}
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
|
@ -18,7 +18,11 @@ import { bracket } from 'lib/exampleKcl'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { getNodeFromPath } from './queryAst'
|
||||
import { IndexLoaderData } from 'Router'
|
||||
import { useLoaderData } from 'react-router-dom'
|
||||
import { Params, useLoaderData } from 'react-router-dom'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { writeTextFile } from '@tauri-apps/api/fs'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const PERSIST_CODE_TOKEN = 'persistCode'
|
||||
|
||||
@ -41,6 +45,7 @@ class KclManager {
|
||||
private _kclErrors: KCLError[] = []
|
||||
private _isExecuting = false
|
||||
private _wasmInitFailed = true
|
||||
private _params: Params<string> = {}
|
||||
|
||||
engineCommandManager: EngineCommandManager
|
||||
private _defferer = deferExecution((code: string) => {
|
||||
@ -71,6 +76,21 @@ class KclManager {
|
||||
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
|
||||
// Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files
|
||||
this._params.id &&
|
||||
writeTextFile(this._params.id, code).catch((err) => {
|
||||
// TODO: add Sentry 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 {
|
||||
localStorage.setItem(PERSIST_CODE_TOKEN, code)
|
||||
}
|
||||
}
|
||||
|
||||
get programMemory() {
|
||||
@ -117,8 +137,17 @@ 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 = localStorage.getItem(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
|
||||
@ -130,6 +159,7 @@ class KclManager {
|
||||
zustandStore.state.code = ''
|
||||
localStorage.setItem('store', JSON.stringify(zustandStore))
|
||||
} else if (storedCode === null) {
|
||||
console.log('stored brack thing')
|
||||
this.code = bracket
|
||||
} else {
|
||||
this.code = storedCode
|
||||
@ -189,8 +219,7 @@ class KclManager {
|
||||
this._programMemory = programMemory
|
||||
this._ast = { ...ast }
|
||||
if (updateCode) {
|
||||
this._code = recast(ast)
|
||||
this._codeCallBack(this._code)
|
||||
this.code = recast(ast)
|
||||
}
|
||||
this._executeCallback()
|
||||
}
|
||||
@ -233,13 +262,17 @@ class KclManager {
|
||||
this.ast = ast
|
||||
if (code) this.code = code
|
||||
}
|
||||
setCode(code: string) {
|
||||
setCode(code: string, shouldWriteFile = true) {
|
||||
if (shouldWriteFile) {
|
||||
// use the normal code setter
|
||||
this.code = code
|
||||
return
|
||||
}
|
||||
this._code = code
|
||||
this._codeCallBack(code)
|
||||
localStorage.setItem(PERSIST_CODE_TOKEN, code)
|
||||
}
|
||||
setCodeAndExecute(code: string) {
|
||||
this.setCode(code)
|
||||
setCodeAndExecute(code: string, shouldWriteFile = true) {
|
||||
this.setCode(code, shouldWriteFile)
|
||||
if (code.trim()) {
|
||||
this._defferer(code)
|
||||
return
|
||||
@ -270,15 +303,12 @@ class KclManager {
|
||||
execute: boolean,
|
||||
optionalParams?: {
|
||||
focusPath?: PathToNode
|
||||
callBack?: (ast: Program) => void
|
||||
}
|
||||
): Promise<Selections | null> {
|
||||
const newCode = recast(ast)
|
||||
const astWithUpdatedSource = parse(newCode)
|
||||
optionalParams?.callBack?.(astWithUpdatedSource)
|
||||
let returnVal: Selections | null = null
|
||||
|
||||
this.code = newCode
|
||||
if (optionalParams?.focusPath) {
|
||||
const { node } = getNodeFromPath<any>(
|
||||
astWithUpdatedSource,
|
||||
@ -299,12 +329,12 @@ class KclManager {
|
||||
|
||||
if (execute) {
|
||||
// Call execute on the set ast.
|
||||
await this.executeAst(astWithUpdatedSource)
|
||||
await this.executeAst(astWithUpdatedSource, true)
|
||||
} 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)
|
||||
await this.executeAstMock(astWithUpdatedSource, true)
|
||||
}
|
||||
return returnVal
|
||||
}
|
||||
@ -369,6 +399,11 @@ export function KclContextProvider({
|
||||
setWasmInitFailed,
|
||||
})
|
||||
}, [])
|
||||
|
||||
const params = useParams()
|
||||
useEffect(() => {
|
||||
kclManager.setParams(params)
|
||||
}, [params])
|
||||
return (
|
||||
<KclContext.Provider
|
||||
value={{
|
||||
|
@ -889,6 +889,7 @@ export class EngineCommandManager {
|
||||
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
|
||||
// we need to loop over them each individually because if the engine doesn't recognise a single
|
||||
// id the whole command fails.
|
||||
const artifactsToDelete: any = {}
|
||||
Object.entries(this.artifactMap).forEach(([id, artifact]) => {
|
||||
const artifactTypesToDelete: ArtifactMap[string]['commandType'][] = [
|
||||
// 'start_path' creates a new scene object for the path, which is why it needs to be deleted,
|
||||
@ -898,7 +899,9 @@ export class EngineCommandManager {
|
||||
'start_path',
|
||||
]
|
||||
if (!artifactTypesToDelete.includes(artifact.commandType)) return
|
||||
|
||||
artifactsToDelete[id] = artifact
|
||||
})
|
||||
Object.keys(artifactsToDelete).forEach((id) => {
|
||||
const deletCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
|
@ -26,5 +26,4 @@ const bracket = startSketchOn('XY')
|
||||
|> close(%)
|
||||
|> extrude(width, %)
|
||||
|
||||
show(bracket)
|
||||
`
|
||||
|
@ -9,7 +9,6 @@ import { documentDir, homeDir, sep } from '@tauri-apps/api/path'
|
||||
import { isTauri } from './isTauri'
|
||||
import { ProjectWithEntryPointMetadata } from '../Router'
|
||||
import { metadata } from 'tauri-plugin-fs-extra-api'
|
||||
import { bracket } from './exampleKcl'
|
||||
|
||||
const PROJECT_FOLDER = 'kittycad-modeling-projects'
|
||||
export const FILE_EXT = '.kcl'
|
||||
@ -211,7 +210,8 @@ export function sortProject(project: FileEntry[]): FileEntry[] {
|
||||
// Creates a new file in the default directory with the default project name
|
||||
// Returns the path to the new file
|
||||
export async function createNewProject(
|
||||
path: string
|
||||
path: string,
|
||||
initCode = ''
|
||||
): Promise<ProjectWithEntryPointMetadata> {
|
||||
if (!isTauri) {
|
||||
throw new Error('createNewProject() can only be called from a Tauri app')
|
||||
@ -225,10 +225,12 @@ export async function createNewProject(
|
||||
})
|
||||
}
|
||||
|
||||
await writeTextFile(path + sep + PROJECT_ENTRYPOINT, bracket).catch((err) => {
|
||||
await writeTextFile(path + sep + PROJECT_ENTRYPOINT, initCode).catch(
|
||||
(err) => {
|
||||
console.error('Error creating new file:', err)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const m = await metadata(path)
|
||||
|
||||
|
@ -43,7 +43,10 @@ function OnboardingWithNewFile() {
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
nextIndex
|
||||
)
|
||||
const newFile = await createNewProject(defaultDirectory + sep + name)
|
||||
const newFile = await createNewProject(
|
||||
defaultDirectory + sep + name,
|
||||
bracket
|
||||
)
|
||||
navigate(
|
||||
`${paths.FILE}/${encodeURIComponent(
|
||||
newFile.path + sep + PROJECT_ENTRYPOINT
|
||||
@ -79,7 +82,7 @@ function OnboardingWithNewFile() {
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
kclManager.setCode(bracket)
|
||||
kclManager.setCodeAndExecute(bracket)
|
||||
next()
|
||||
}}
|
||||
icon={{ icon: faArrowRight }}
|
||||
|
@ -32,6 +32,7 @@ import {
|
||||
} from 'lib/tauriFS'
|
||||
import { ONBOARDING_PROJECT_NAME } from './Onboarding'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
|
||||
export const Settings = () => {
|
||||
const loaderData =
|
||||
@ -96,7 +97,10 @@ export const Settings = () => {
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
nextIndex
|
||||
)
|
||||
const newFile = await createNewProject(defaultDirectory + sep + name)
|
||||
const newFile = await createNewProject(
|
||||
defaultDirectory + sep + name,
|
||||
bracket
|
||||
)
|
||||
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
|
||||
}
|
||||
|
||||
|
@ -253,11 +253,13 @@ export async function executeAst({
|
||||
engineCommandManager,
|
||||
defaultPlanes,
|
||||
useFakeExecutor = false,
|
||||
programMemoryOverride,
|
||||
}: {
|
||||
ast: Program
|
||||
engineCommandManager: EngineCommandManager
|
||||
defaultPlanes: DefaultPlanes
|
||||
useFakeExecutor?: boolean
|
||||
programMemoryOverride?: ProgramMemory
|
||||
}): Promise<{
|
||||
logs: string[]
|
||||
errors: KCLError[]
|
||||
@ -269,10 +271,13 @@ export async function executeAst({
|
||||
engineCommandManager.startNewSession()
|
||||
}
|
||||
const programMemory = await (useFakeExecutor
|
||||
? enginelessExecutor(ast, {
|
||||
? enginelessExecutor(
|
||||
ast,
|
||||
programMemoryOverride || {
|
||||
root: defaultProgramMemory,
|
||||
return: null,
|
||||
})
|
||||
}
|
||||
)
|
||||
: _executor(
|
||||
ast,
|
||||
{
|
||||
|
Reference in New Issue
Block a user