Do not write to file or update code editor a ridiculous amount of times and update them both at the most appropriate moments. (#4479)

* Reapply "Deflake project settings override on desktop (#4370)" (#4450)

This reverts commit b11040c23c.

* Refactor writeToFile and updateCodeEditor to happen at appropriate times

* Turn error into warning about out of date AST.

* Rename setUp to setup

* ONLY reload current file on changes.

* If value is falsey then don't try to executeAst

* Fix up code based selections after constraints

* Correct any last missing code mods

* Update src/clientSideScene/ClientSideSceneComp.tsx

Remove eslint rule no-floating-promises

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* Fixups

* Fix FileTree failing

---------

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
This commit is contained in:
49fl
2024-11-16 16:49:44 -05:00
committed by GitHub
parent fbb7b08b62
commit 05f4f34269
15 changed files with 288 additions and 141 deletions

View File

@ -344,14 +344,13 @@ test.describe('Testing settings', () => {
await test.step('Refresh the application and see project setting applied', async () => { await test.step('Refresh the application and see project setting applied', async () => {
// Make sure we're done navigating before we reload // Make sure we're done navigating before we reload
await expect(settingsCloseButton).not.toBeVisible() await expect(settingsCloseButton).not.toBeVisible()
await page.reload({ waitUntil: 'domcontentloaded' })
await page.reload({ waitUntil: 'domcontentloaded' })
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor) await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
}) })
await test.step(`Navigate back to the home view and see user setting applied`, async () => { await test.step(`Navigate back to the home view and see user setting applied`, async () => {
await logoLink.click() await logoLink.click()
await page.screenshot({ path: 'out.png' })
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor) await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
}) })

View File

@ -202,12 +202,20 @@ const Overlay = ({
let xAlignment = overlay.angle < 0 ? '0%' : '-100%' let xAlignment = overlay.angle < 0 ? '0%' : '-100%'
let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%' let yAlignment = overlay.angle < -90 || overlay.angle >= 90 ? '0%' : '-100%'
// It's possible for the pathToNode to request a newer AST node
// than what's available in the AST at the moment of query.
// It eventually settles on being updated.
const _node1 = getNodeFromPath<Node<CallExpression>>( const _node1 = getNodeFromPath<Node<CallExpression>>(
kclManager.ast, kclManager.ast,
overlay.pathToNode, overlay.pathToNode,
'CallExpression' 'CallExpression'
) )
if (err(_node1)) return
// For that reason, to prevent console noise, we do not use err here.
if (_node1 instanceof Error) {
console.warn('ast older than pathToNode, not fatal, eventually settles', '')
return
}
const callExpression = _node1.node const callExpression = _node1.node
const constraints = getConstraintInfo( const constraints = getConstraintInfo(
@ -637,10 +645,16 @@ const ConstraintSymbol = ({
kclManager.ast, kclManager.ast,
kclManager.programMemory kclManager.programMemory
) )
if (!transform) return if (!transform) return
const { modifiedAst } = transform const { modifiedAst } = transform
// eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.updateAst(modifiedAst, true) await kclManager.updateAst(modifiedAst, true)
// Code editor will be updated in the modelingMachine.
const newCode = recast(modifiedAst)
if (err(newCode)) return
await codeManager.updateCodeEditor(newCode)
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)
} }

View File

@ -453,6 +453,7 @@ export class SceneEntities {
const { modifiedAst } = addStartProfileAtRes const { modifiedAst } = addStartProfileAtRes
await kclManager.updateAst(modifiedAst, false) await kclManager.updateAst(modifiedAst, false)
this.removeIntersectionPlane() this.removeIntersectionPlane()
this.scene.remove(draftPointGroup) this.scene.remove(draftPointGroup)
@ -685,7 +686,7 @@ export class SceneEntities {
}) })
return nextAst return nextAst
} }
setUpDraftSegment = async ( setupDraftSegment = async (
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
forward: [number, number, number], forward: [number, number, number],
up: [number, number, number], up: [number, number, number],
@ -856,10 +857,11 @@ export class SceneEntities {
} }
await kclManager.executeAstMock(modifiedAst) await kclManager.executeAstMock(modifiedAst)
if (intersectsProfileStart) { if (intersectsProfileStart) {
sceneInfra.modelingSend({ type: 'CancelSketch' }) sceneInfra.modelingSend({ type: 'CancelSketch' })
} else { } else {
await this.setUpDraftSegment( await this.setupDraftSegment(
sketchPathToNode, sketchPathToNode,
forward, forward,
up, up,
@ -867,6 +869,8 @@ export class SceneEntities {
segmentName segmentName
) )
} }
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
}, },
onMove: (args) => { onMove: (args) => {
this.onDragSegment({ this.onDragSegment({
@ -991,43 +995,51 @@ export class SceneEntities {
if (trap(_node)) return if (trap(_node)) return
const sketchInit = _node.node?.declarations?.[0]?.init const sketchInit = _node.node?.declarations?.[0]?.init
if (sketchInit.type === 'PipeExpression') { if (sketchInit.type !== 'PipeExpression') {
updateRectangleSketch(sketchInit, x, y, tags[0]) return
let _recastAst = parse(recast(_ast))
if (trap(_recastAst)) return
_ast = _recastAst
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' })
const { execState } = await executeAst({
ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
// Update the rest of the segments of the THREEjs scene
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
} }
updateRectangleSketch(sketchInit, x, y, tags[0])
const newCode = recast(_ast)
let _recastAst = parse(newCode)
if (trap(_recastAst)) return
_ast = _recastAst
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish rectangle' })
// lee: I had this at the bottom of the function, but it's
// possible sketchFromKclValue "fails" when sketching on a face,
// and this couldn't wouldn't run.
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
const { execState } = await executeAst({
ast: _ast,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
idGenerator: kclManager.execState.idGenerator,
})
const programMemory = execState.memory
// Prepare to update the THREEjs scene
this.sceneProgramMemory = programMemory
const sketch = sketchFromKclValue(
programMemory.get(variableDeclarationName),
variableDeclarationName
)
if (err(sketch)) return
const sgPaths = sketch.paths
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
// Update the starting segment of the THREEjs scene
this.updateSegment(sketch.start, 0, 0, _ast, orthoFactor, sketch)
// Update the rest of the segments of the THREEjs scene
sgPaths.forEach((seg, index) =>
this.updateSegment(seg, index, 0, _ast, orthoFactor, sketch)
)
}, },
}) })
} }
@ -1187,13 +1199,17 @@ export class SceneEntities {
if (err(moddedResult)) return if (err(moddedResult)) return
modded = moddedResult.modifiedAst modded = moddedResult.modifiedAst
let _recastAst = parse(recast(modded)) const newCode = recast(modded)
if (err(newCode)) return
let _recastAst = parse(newCode)
if (trap(_recastAst)) return Promise.reject(_recastAst) if (trap(_recastAst)) return Promise.reject(_recastAst)
_ast = _recastAst _ast = _recastAst
// Update the primary AST and unequip the rectangle tool // Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(_ast) await kclManager.executeAstMock(_ast)
sceneInfra.modelingSend({ type: 'Finish circle' }) sceneInfra.modelingSend({ type: 'Finish circle' })
await codeManager.updateEditorWithAstAndWriteToFile(_ast)
} }
}, },
}) })
@ -1229,6 +1245,7 @@ export class SceneEntities {
forward, forward,
position, position,
}) })
await codeManager.writeToFile()
} }
}, },
onDrag: async ({ onDrag: async ({

View File

@ -22,6 +22,7 @@ import usePlatform from 'hooks/usePlatform'
import { FileEntry } from 'lib/project' import { FileEntry } from 'lib/project'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { normalizeLineEndings } from 'lib/codeEditor' import { normalizeLineEndings } from 'lib/codeEditor'
import { reportRejection } from 'lib/trap'
function getIndentationCSS(level: number) { function getIndentationCSS(level: number) {
return `calc(1rem * ${level + 1})` return `calc(1rem * ${level + 1})`
@ -189,15 +190,14 @@ const FileTreeItem = ({
// the ReactNodes are destroyed, so is this listener :) // the ReactNodes are destroyed, so is this listener :)
useFileSystemWatcher( useFileSystemWatcher(
async (eventType, path) => { async (eventType, path) => {
// Don't try to read a file that was removed. // Prevents a cyclic read / write causing editor problems such as
if (isCurrentFile && eventType !== 'unlink') { // misplaced cursor positions.
// Prevents a cyclic read / write causing editor problems such as if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
// misplaced cursor positions. codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) { return
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false }
return
}
if (isCurrentFile && eventType === 'change') {
let code = await window.electron.readFile(path, { encoding: 'utf-8' }) let code = await window.electron.readFile(path, { encoding: 'utf-8' })
code = normalizeLineEndings(code) code = normalizeLineEndings(code)
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
@ -242,11 +242,11 @@ const FileTreeItem = ({
// Show the renaming form // Show the renaming form
addCurrentItemToRenaming() addCurrentItemToRenaming()
} else if (e.code === 'Space') { } else if (e.code === 'Space') {
handleClick() void handleClick().catch(reportRejection)
} }
} }
function handleClick() { async function handleClick() {
setTreeSelection(fileOrDir) setTreeSelection(fileOrDir)
if (fileOrDir.children !== null) return // Don't open directories if (fileOrDir.children !== null) return // Don't open directories
@ -258,12 +258,10 @@ const FileTreeItem = ({
`import("${fileOrDir.path.replace(project.path, '.')}")\n` + `import("${fileOrDir.path.replace(project.path, '.')}")\n` +
codeManager.code codeManager.code
) )
// eslint-disable-next-line @typescript-eslint/no-floating-promises await codeManager.writeToFile()
codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files // Prevent seeing the model built one piece at a time when changing files
// eslint-disable-next-line @typescript-eslint/no-floating-promises await kclManager.executeCode(true)
kclManager.executeCode(true)
} else { } else {
// Let the lsp servers know we closed a file. // Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null) onFileClose(currentFile?.path || null, project?.path || null)
@ -295,7 +293,7 @@ const FileTreeItem = ({
style={{ paddingInlineStart: getIndentationCSS(level) }} style={{ paddingInlineStart: getIndentationCSS(level) }}
onClick={(e) => { onClick={(e) => {
e.currentTarget.focus() e.currentTarget.focus()
handleClick() void handleClick().catch(reportRejection)
}} }}
onKeyUp={handleKeyUp} onKeyUp={handleKeyUp}
> >
@ -655,6 +653,13 @@ export const FileTreeInner = ({
const isCurrentFile = loaderData.file?.path === path const isCurrentFile = loaderData.file?.path === path
const hasChanged = eventType === 'change' const hasChanged = eventType === 'change'
if (isCurrentFile && hasChanged) return if (isCurrentFile && hasChanged) return
// If it's a settings file we wrote to already from the app ignore it.
if (codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher) {
codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = false
return
}
fileSend({ type: 'Refresh' }) fileSend({ type: 'Refresh' })
}, },
[loaderData?.project?.path, fileContext.selectedDirectory.path].filter( [loaderData?.project?.path, fileContext.selectedDirectory.path].filter(

View File

@ -304,6 +304,7 @@ export const ModelingMachineProvider = ({
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
if (!editorManager.editorView) return if (!editorManager.editorView) return
setTimeout(() => { setTimeout(() => {
if (!editorManager.editorView) return if (!editorManager.editorView) return
editorManager.editorView.dispatch({ editorManager.editorView.dispatch({
@ -732,6 +733,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -768,6 +774,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -813,6 +824,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -846,6 +862,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -881,6 +902,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -917,6 +943,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -953,6 +984,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -999,6 +1035,11 @@ export const ModelingMachineProvider = ({
sketchDetails.origin sketchDetails.origin
) )
if (err(updatedAst)) return Promise.reject(updatedAst) if (err(updatedAst)) return Promise.reject(updatedAst)
await codeManager.updateEditorWithAstAndWriteToFile(
updatedAst.newAst
)
const selection = updateSelections( const selection = updateSelections(
{ 0: pathToReplacedNode }, { 0: pathToReplacedNode },
selectionRanges, selectionRanges,

View File

@ -41,6 +41,7 @@ import { reportRejection } from 'lib/trap'
import { getAppSettingsFilePath } from 'lib/desktop' import { getAppSettingsFilePath } from 'lib/desktop'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher' import { useFileSystemWatcher } from 'hooks/useFileSystemWatcher'
import { codeManager } from 'lib/singletons'
import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig' import { createRouteCommands } from 'lib/commandBarConfigs/routeCommandConfig'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
@ -201,13 +202,13 @@ export const SettingsAuthProviderBase = ({
console.error('Error executing AST after settings change', e) console.error('Error executing AST after settings change', e)
} }
}, },
persistSettings: ({ context, event }) => { async persistSettings({ context, event }) {
// Without this, when a user changes the file, it'd // Without this, when a user changes the file, it'd
// create a detection loop with the file-system watcher. // create a detection loop with the file-system watcher.
if (event.doNotPersist) return if (event.doNotPersist) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
saveSettings(context, loadedProject?.project?.path) return saveSettings(context, loadedProject?.project?.path)
}, },
}, },
}), }),
@ -221,7 +222,7 @@ export const SettingsAuthProviderBase = ({
}, []) }, [])
useFileSystemWatcher( useFileSystemWatcher(
async () => { async (eventType: string) => {
// If there is a projectPath but it no longer exists it means // If there is a projectPath but it no longer exists it means
// it was exterally removed. If we let the code past this condition // it was exterally removed. If we let the code past this condition
// execute it will recreate the directory due to code in // execute it will recreate the directory due to code in
@ -235,6 +236,9 @@ export const SettingsAuthProviderBase = ({
} }
} }
// Only reload if there are changes. Ignore everything else.
if (eventType !== 'change') return
const data = await loadAndValidateSettings(loadedProject?.project?.path) const data = await loadAndValidateSettings(loadedProject?.project?.path)
settingsSend({ settingsSend({
type: 'Set all settings', type: 'Set all settings',

View File

@ -96,10 +96,10 @@ export class KclPlugin implements PluginValue {
const newCode = viewUpdate.state.doc.toString() const newCode = viewUpdate.state.doc.toString()
codeManager.code = newCode codeManager.code = newCode
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.writeToFile()
this.scheduleUpdateDoc() void codeManager.writeToFile().then(() => {
this.scheduleUpdateDoc()
})
} }
scheduleUpdateDoc() { scheduleUpdateDoc() {

View File

@ -26,6 +26,7 @@ export function useRefreshSettings(routeId: string = PATHS.INDEX) {
ctx.settings.send({ ctx.settings.send({
type: 'Set all settings', type: 'Set all settings',
settings: routeData, settings: routeData,
doNotPersist: true,
}) })
}, []) }, [])
} }

View File

@ -2,13 +2,13 @@ import {
SetVarNameModal, SetVarNameModal,
createSetVarNameModal, createSetVarNameModal,
} from 'components/SetVarNameModal' } from 'components/SetVarNameModal'
import { editorManager, kclManager } from 'lib/singletons' import { editorManager, kclManager, codeManager } from 'lib/singletons'
import { reportRejection, trap } from 'lib/trap' import { reportRejection, trap, err } from 'lib/trap'
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'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange } from 'lang/wasm' import { PathToNode, SourceRange, recast } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
@ -57,6 +57,11 @@ export function useConvertToVariable(range?: SourceRange) {
) )
await kclManager.updateAst(_modifiedAst, true) await kclManager.updateAst(_modifiedAst, true)
const newCode = recast(_modifiedAst)
if (err(newCode)) return
codeManager.updateCodeEditor(newCode)
return pathToReplacedNode return pathToReplacedNode
} catch (e) { } catch (e) {
console.log('error', e) console.log('error', e)

View File

@ -357,9 +357,6 @@ export class KclManager {
this.clearAst() this.clearAst()
return return
} }
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
this._ast = { ...newAst } this._ast = { ...newAst }
const { logs, errors, execState } = await executeAst({ const { logs, errors, execState } = await executeAst({
@ -434,13 +431,9 @@ export class KclManager {
// Update the code state and the editor. // Update the code state and the editor.
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
// Write back to the file system.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.writeToFile()
// execute the code. // Write back to the file system.
// eslint-disable-next-line @typescript-eslint/no-floating-promises void codeManager.writeToFile().then(() => this.executeCode())
this.executeCode()
} }
// There's overlapping responsibility between updateAst and executeAst. // 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. // updateAst was added as it was used a lot before xState migration so makes the port easier.
@ -501,11 +494,6 @@ export class KclManager {
} }
if (execute) { if (execute) {
// Call execute on the set ast.
// Update the code state and editor.
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
await this.executeAst({ await this.executeAst({
ast: astWithUpdatedSource, ast: astWithUpdatedSource,
zoomToFit: optionalParams?.zoomToFit, zoomToFit: optionalParams?.zoomToFit,

View File

@ -7,6 +7,8 @@ import toast from 'react-hot-toast'
import { editorManager } from 'lib/singletons' import { editorManager } from 'lib/singletons'
import { Annotation, Transaction } from '@codemirror/state' import { Annotation, Transaction } from '@codemirror/state'
import { KeyBinding } from '@codemirror/view' import { KeyBinding } from '@codemirror/view'
import { recast, Program } from 'lang/wasm'
import { err } from 'lib/trap'
const PERSIST_CODE_KEY = 'persistCode' const PERSIST_CODE_KEY = 'persistCode'
@ -121,24 +123,39 @@ export default class CodeManager {
// Only write our buffer contents to file once per second. Any faster // Only write our buffer contents to file once per second. Any faster
// and file-system watchers which read, will receive empty data during // and file-system watchers which read, will receive empty data during
// writes. // writes.
clearTimeout(this.timeoutWriter) clearTimeout(this.timeoutWriter)
this.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true this.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true
this.timeoutWriter = setTimeout(() => {
// Wait one event loop to give a chance for params to be set return new Promise((resolve, reject) => {
// Save the file to disk this.timeoutWriter = setTimeout(() => {
this._currentFilePath && if (!this._currentFilePath)
return reject(new Error('currentFilePath not set'))
// Wait one event loop to give a chance for params to be set
// Save the file to disk
window.electron window.electron
.writeFile(this._currentFilePath, this.code ?? '') .writeFile(this._currentFilePath, this.code ?? '')
.then(resolve)
.catch((err: Error) => { .catch((err: Error) => {
// 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')
reject(err)
}) })
}, 1000) }, 1000)
})
} else { } else {
safeLSSetItem(PERSIST_CODE_KEY, this.code) safeLSSetItem(PERSIST_CODE_KEY, this.code)
} }
} }
async updateEditorWithAstAndWriteToFile(ast: Program) {
const newCode = recast(ast)
if (err(newCode)) return
this.updateCodeStateEditor(newCode)
await this.writeToFile()
}
} }
function safeLSGetItem(key: string) { function safeLSGetItem(key: string) {

View File

@ -35,7 +35,12 @@ import {
ArtifactGraph, ArtifactGraph,
getSweepFromSuspectedPath, getSweepFromSuspectedPath,
} from 'lang/std/artifactGraph' } from 'lang/std/artifactGraph'
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons' import {
kclManager,
engineCommandManager,
editorManager,
codeManager,
} from 'lib/singletons'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
// Apply Fillet To Selection // Apply Fillet To Selection
@ -253,6 +258,9 @@ async function updateAstAndFocus(
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode, focusPath: pathToFilletNode,
}) })
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) { if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }

View File

@ -178,6 +178,7 @@ export async function loadAndValidateSettings(
if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload) if (err(appSettingsPayload)) return Promise.reject(appSettingsPayload)
let settingsNext = createSettings() let settingsNext = createSettings()
// Because getting the default directory is async, we need to set it after // Because getting the default directory is async, we need to set it after
if (onDesktop) { if (onDesktop) {
settings.app.projectDirectory.default = await getInitialDefaultDir() settings.app.projectDirectory.default = await getInitialDefaultDir()

View File

@ -115,6 +115,7 @@ export function useCalculateKclExpression({
setCalcResult(typeof result === 'number' ? String(result) : 'NAN') setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
init && setValueNode(init) init && setValueNode(init)
} }
if (!value) return
execAstAndSetResult().catch(() => { execAstAndSetResult().catch(() => {
setCalcResult('NAN') setCalcResult('NAN')
setValueNode(null) setValueNode(null)

View File

@ -18,6 +18,7 @@ import {
sceneEntitiesManager, sceneEntitiesManager,
engineCommandManager, engineCommandManager,
editorManager, editorManager,
codeManager,
} from 'lib/singletons' } from 'lib/singletons'
import { import {
horzVertInfo, horzVertInfo,
@ -531,8 +532,10 @@ export const modelingMachine = setup({
} }
} }
), ),
// eslint-disable-next-line @typescript-eslint/no-misused-promises 'hide default planes': () => {
'hide default planes': () => kclManager.hidePlanes(), // eslint-disable-next-line @typescript-eslint/no-floating-promises
kclManager.hidePlanes()
},
'reset sketch metadata': assign({ 'reset sketch metadata': assign({
sketchDetails: null, sketchDetails: null,
sketchEnginePathId: '', sketchEnginePathId: '',
@ -595,7 +598,6 @@ export const modelingMachine = setup({
if (trap(extrudeSketchRes)) return if (trap(extrudeSketchRes)) return
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
store.videoElement?.pause()
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToExtrudeArg], focusPath: [pathToExtrudeArg],
zoomToFit: true, zoomToFit: true,
@ -604,11 +606,9 @@ export const modelingMachine = setup({
type: 'path', type: 'path',
}, },
}) })
if (!engineCommandManager.engineConnection?.idleMode) {
store.videoElement?.play().catch((e) => { await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
console.warn('Video playing was prevented', e)
})
}
if (updatedAst?.selections) { if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }
@ -642,7 +642,6 @@ export const modelingMachine = setup({
if (trap(revolveSketchRes)) return if (trap(revolveSketchRes)) return
const { modifiedAst, pathToRevolveArg } = revolveSketchRes const { modifiedAst, pathToRevolveArg } = revolveSketchRes
store.videoElement?.pause()
const updatedAst = await kclManager.updateAst(modifiedAst, true, { const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToRevolveArg], focusPath: [pathToRevolveArg],
zoomToFit: true, zoomToFit: true,
@ -651,11 +650,9 @@ export const modelingMachine = setup({
type: 'path', type: 'path',
}, },
}) })
if (!engineCommandManager.engineConnection?.idleMode) {
store.videoElement?.play().catch((e) => { await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
console.warn('Video playing was prevented', e)
})
}
if (updatedAst?.selections) { if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }
@ -685,6 +682,7 @@ export const modelingMachine = setup({
} }
await kclManager.updateAst(modifiedAst, true) await kclManager.updateAst(modifiedAst, true)
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
})().catch(reportRejection) })().catch(reportRejection)
}, },
'AST fillet': ({ event }) => { 'AST fillet': ({ event }) => {
@ -702,6 +700,9 @@ export const modelingMachine = setup({
radius radius
) )
if (err(applyFilletToSelectionResult)) return applyFilletToSelectionResult if (err(applyFilletToSelectionResult)) return applyFilletToSelectionResult
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
}, },
'set selection filter to curves only': () => { 'set selection filter to curves only': () => {
;(async () => { ;(async () => {
@ -758,25 +759,35 @@ export const modelingMachine = setup({
'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(), 'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
'set up draft line': ({ context: { sketchDetails } }) => { 'set up draft line': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setUpDraftSegment( sceneEntitiesManager
sketchDetails.sketchPathToNode, .setupDraftSegment(
sketchDetails.zAxis, sketchDetails.sketchPathToNode,
sketchDetails.yAxis, sketchDetails.zAxis,
sketchDetails.origin, sketchDetails.yAxis,
'line' sketchDetails.origin,
) 'line'
)
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'set up draft arc': ({ context: { sketchDetails } }) => { 'set up draft arc': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setUpDraftSegment( sceneEntitiesManager
sketchDetails.sketchPathToNode, .setupDraftSegment(
sketchDetails.zAxis, sketchDetails.sketchPathToNode,
sketchDetails.yAxis, sketchDetails.zAxis,
sketchDetails.origin, sketchDetails.yAxis,
'tangentialArcTo' sketchDetails.origin,
) 'tangentialArcTo'
)
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'listen for rectangle origin': ({ context: { sketchDetails } }) => { 'listen for rectangle origin': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
@ -834,38 +845,53 @@ export const modelingMachine = setup({
'set up draft rectangle': ({ context: { sketchDetails }, event }) => { 'set up draft rectangle': ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Add rectangle origin') return if (event.type !== 'Add rectangle origin') return
if (!sketchDetails || !event.data) return if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setupDraftRectangle( sceneEntitiesManager
sketchDetails.sketchPathToNode, .setupDraftRectangle(
sketchDetails.zAxis, sketchDetails.sketchPathToNode,
sketchDetails.yAxis, sketchDetails.zAxis,
sketchDetails.origin, sketchDetails.yAxis,
event.data sketchDetails.origin,
) event.data
)
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'set up draft circle': ({ context: { sketchDetails }, event }) => { 'set up draft circle': ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Add circle origin') return if (event.type !== 'Add circle origin') return
if (!sketchDetails || !event.data) return if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setupDraftCircle( sceneEntitiesManager
sketchDetails.sketchPathToNode, .setupDraftCircle(
sketchDetails.zAxis, sketchDetails.sketchPathToNode,
sketchDetails.yAxis, sketchDetails.zAxis,
sketchDetails.origin, sketchDetails.yAxis,
event.data sketchDetails.origin,
) event.data
)
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'set up draft line without teardown': ({ context: { sketchDetails } }) => { 'set up draft line without teardown': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.setUpDraftSegment( sceneEntitiesManager
sketchDetails.sketchPathToNode, .setupDraftSegment(
sketchDetails.zAxis, sketchDetails.sketchPathToNode,
sketchDetails.yAxis, sketchDetails.zAxis,
sketchDetails.origin, sketchDetails.yAxis,
'line', sketchDetails.origin,
false 'line',
) false
)
.then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
})
}, },
'show default planes': () => { 'show default planes': () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
@ -882,12 +908,17 @@ export const modelingMachine = setup({
'add axis n grid': ({ context: { sketchDetails } }) => { 'add axis n grid': ({ context: { sketchDetails } }) => {
if (!sketchDetails) return if (!sketchDetails) return
if (localStorage.getItem('disableAxis')) return if (localStorage.getItem('disableAxis')) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneEntitiesManager.createSketchAxis( sceneEntitiesManager.createSketchAxis(
sketchDetails.sketchPathToNode || [], sketchDetails.sketchPathToNode || [],
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
sketchDetails.origin sketchDetails.origin
) )
// eslint-disable-next-line @typescript-eslint/no-floating-promises
codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
}, },
'reset client scene mouse handlers': () => { 'reset client scene mouse handlers': () => {
// when not in sketch mode we don't need any mouse listeners // when not in sketch mode we don't need any mouse listeners
@ -916,10 +947,13 @@ export const modelingMachine = setup({
'Delete segment': ({ context: { sketchDetails }, event }) => { 'Delete segment': ({ context: { sketchDetails }, event }) => {
if (event.type !== 'Delete segment') return if (event.type !== 'Delete segment') return
if (!sketchDetails || !event.data) return if (!sketchDetails || !event.data) return
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
deleteSegment({ deleteSegment({
pathToNode: event.data, pathToNode: event.data,
sketchDetails, sketchDetails,
}).then(() => {
return codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
}) })
}, },
'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(), 'Reset Segment Overlays': () => sceneEntitiesManager.resetOverlays(),
@ -984,6 +1018,9 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
@ -1018,6 +1055,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
@ -1052,6 +1090,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection: updateSelections( selection: updateSelections(
@ -1084,6 +1123,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1117,6 +1157,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1150,6 +1191,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1183,6 +1225,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1220,6 +1263,8 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1252,6 +1297,7 @@ export const modelingMachine = setup({
) )
if (trap(updatedAst, { suppress: true })) return if (trap(updatedAst, { suppress: true })) return
if (!updatedAst) return if (!updatedAst) return
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
const updatedSelectionRanges = updateSelections( const updatedSelectionRanges = updateSelections(
pathToNodeMap, pathToNodeMap,
selectionRanges, selectionRanges,
@ -1556,7 +1602,7 @@ export const modelingMachine = setup({
}, },
}, },
entry: 'setup client side sketch segments', entry: ['setup client side sketch segments'],
}, },
'Await horizontal distance info': { 'Await horizontal distance info': {
@ -1801,7 +1847,7 @@ export const modelingMachine = setup({
onError: 'SketchIdle', onError: 'SketchIdle',
onDone: { onDone: {
target: 'SketchIdle', target: 'SketchIdle',
actions: ['Set selection'], actions: 'Set selection',
}, },
}, },
}, },