remove clearScene from TS side (#4684)

* updates

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

* fix lint

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

* add failing tests

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

* more tests

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

* the scene is cleared

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

* create new clear scene and bust cache function from rust side

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

* pull thru

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

* set that we switched files

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>

* fix two dirties

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

* fix

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2024-12-06 14:56:53 -08:00
committed by GitHub
parent dd370a9365
commit dfc3d19677
12 changed files with 431 additions and 39 deletions

View File

@ -136,6 +136,335 @@ test(
}
)
test(
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
const errorDir = join(dir, 'broken-code')
await fsp.mkdir(errorDir, { recursive: true })
await fsp.copyFile(
executorInputPath('broken-code-test.kcl'),
join(errorDir, 'main.kcl')
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home.
await expect(page.getByText('broken-code')).toBeVisible()
await page.getByText('broken-code').click()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
const emptyDir = join(dir, 'empty')
await fsp.mkdir(emptyDir, { recursive: true })
await fsp.writeFile(join(emptyDir, 'main.kcl'), '')
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening empty code project should clear the scene', async () => {
// Go back home.
await expect(page.getByText('empty')).toBeVisible()
await page.getByText('empty').click()
// Ensure the code is empty.
await expect(u.codeLocator).toContainText('')
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
// planes colors means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open empty file, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.writeFile(join(bracketDir, 'empty.kcl'), '')
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('creating a empty file should clear the scene', async () => {
// open the file pane.
await page.getByTestId('files-pane-button').click()
// OPen the other file.
const file = page.getByRole('button', { name: 'empty.kcl' })
await expect(file).toBeVisible()
await file.click()
// planes colors means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
timeout: 10_000,
})
.toBeLessThan(15)
// Ensure the code is empty.
await expect(u.codeLocator).toContainText('')
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open another file in the same project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('broken-code-test.kcl'),
join(bracketDir, 'broken-code-test.kcl')
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('opening broken code file should clear the scene and show the error', async () => {
// open the file pane.
await page.getByTestId('files-pane-button').click()
// OPen the other file.
const file = page.getByRole('button', { name: 'broken-code-test.kcl' })
await expect(file).toBeVisible()
await file.click()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'when code with error first loads you get errors in console',
{ tag: '@electron' },

View File

@ -266,6 +266,7 @@ const FileTreeItem = ({
// Let the lsp servers know we closed a file.
onFileClose(currentFile?.path || null, project?.path || null)
onFileOpen(fileOrDir.path, project?.path || null)
kclManager.switchedFiles = true
// Open kcl files
navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`)

View File

@ -40,7 +40,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
<Menu.Item>
<button
onClick={() => kclManager.format()}
onClick={() => {
kclManager.format().catch(reportRejection)
}}
className={styles.button}
>
<span>Format code</span>

View File

@ -10,7 +10,7 @@ import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons'
import { engineCommandManager, kclManager } from 'lib/singletons'
import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
@ -68,8 +68,7 @@ function AppLogoLink({
data-testid="app-logo"
onClick={() => {
onProjectClose(file || null, project?.path || null, false)
// Clear the scene.
engineCommandManager.clearScene()
kclManager.switchedFiles = true
}}
to={PATHS.HOME}
className={wrapperClassName + ' hover:before:brightness-110'}
@ -190,8 +189,7 @@ function ProjectMenuPopover({
className: !isDesktop() ? 'hidden' : '',
onClick: () => {
onProjectClose(file || null, project?.path || null, true)
// Clear the scene.
engineCommandManager.clearScene()
kclManager.switchedFiles = true
},
},
].filter(

View File

@ -12,6 +12,7 @@ import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
import {
CallExpression,
clearSceneAndBustCache,
emptyExecState,
ExecState,
initPromise,
@ -60,6 +61,7 @@ export class KclManager {
private _executeIsStale: ExecuteArgs | null = null
private _wasmInitFailed = true
private _hasErrors = false
private _switchedFiles = false
engineCommandManager: EngineCommandManager
@ -79,6 +81,10 @@ export class KclManager {
this._astCallBack(ast)
}
set switchedFiles(switchedFiles: boolean) {
this._switchedFiles = switchedFiles
}
get programMemory() {
return this._programMemory
}
@ -166,8 +172,12 @@ export class KclManager {
this.engineCommandManager = engineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ensureWasmInit().then(() => {
this.ast = this.safeParse(codeManager.code) || this.ast
this.ensureWasmInit().then(async () => {
await this.safeParse(codeManager.code).then((ast) => {
if (ast) {
this.ast = ast
}
})
})
}
@ -211,7 +221,25 @@ export class KclManager {
}
}
safeParse(code: string): Node<Program> | null {
// (jess) I'm not in love with this, but it ensures we clear the scene and
// bust the cache on
// errors from parsing when opening new files.
// Why not just clear the cache on all parse errors, you ask? well its actually
// really nice to keep the cache on parse errors within the same file, and
// only bust on engine errors esp if they take a long time to execute and
// you hit the wrong key!
private async checkIfSwitchedFilesShouldClear() {
// If we were switching files and we hit an error on parse we need to bust
// the cache and clear the scene.
if (this._hasErrors && this._switchedFiles) {
await clearSceneAndBustCache(this.engineCommandManager)
} else if (this._switchedFiles) {
// Reset the switched files boolean.
this._switchedFiles = false
}
}
async safeParse(code: string): Promise<Node<Program> | null> {
const result = parse(code)
this.diagnostics = []
this._hasErrors = false
@ -220,6 +248,8 @@ export class KclManager {
const kclerror: KCLError = result as KCLError
this.diagnostics = kclErrorsToDiagnostics([kclerror])
this._hasErrors = true
await this.checkIfSwitchedFilesShouldClear()
return null
}
@ -228,6 +258,7 @@ export class KclManager {
if (result.errors.length > 0) {
this._hasErrors = true
await this.checkIfSwitchedFilesShouldClear()
return null
}
@ -353,7 +384,7 @@ export class KclManager {
console.error(newCode)
return
}
const newAst = this.safeParse(newCode)
const newAst = await this.safeParse(newCode)
if (!newAst) {
this.clearAst()
return
@ -408,7 +439,7 @@ export class KclManager {
})
}
async executeCode(zoomToFit?: boolean): Promise<void> {
const ast = this.safeParse(codeManager.code)
const ast = await this.safeParse(codeManager.code)
if (!ast) {
this.clearAst()
return
@ -416,9 +447,9 @@ export class KclManager {
this.ast = { ...ast }
return this.executeAst({ zoomToFit })
}
format() {
async format() {
const originalCode = codeManager.code
const ast = this.safeParse(originalCode)
const ast = await this.safeParse(originalCode)
if (!ast) {
this.clearAst()
return
@ -458,7 +489,7 @@ export class KclManager {
const newCode = recast(ast)
if (err(newCode)) return Promise.reject(newCode)
const astWithUpdatedSource = this.safeParse(newCode)
const astWithUpdatedSource = await this.safeParse(newCode)
if (!astWithUpdatedSource) return Promise.reject(new Error('bad ast'))
let returnVal: Selections | undefined = undefined

View File

@ -1879,17 +1879,6 @@ export class EngineCommandManager extends EventTarget {
}
return JSON.stringify(this.defaultPlanes)
}
clearScene(): void {
const deleteCmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'scene_clear_all',
},
}
this.clearDefaultPlanes()
this.engineConnection?.send(deleteCmd)
}
addCommandLog(message: CommandLog) {
if (this.commandLogs.length > 500) {
this.commandLogs.shift()

View File

@ -16,6 +16,7 @@ import init, {
parse_project_settings,
default_project_settings,
base64_decode,
clear_scene_and_bust_cache,
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -698,6 +699,21 @@ export function defaultAppSettings(): DeepPartial<Configuration> | Error {
return default_app_settings()
}
export async function clearSceneAndBustCache(
engineCommandManager: EngineCommandManager
): Promise<null | Error> {
try {
await clear_scene_and_bust_cache(engineCommandManager)
} catch (e: any) {
console.error('clear_scene_and_bust_cache: error', e)
return Promise.reject(
new Error(`Error on clear_scene_and_bust_cache: ${e}`)
)
}
return null
}
export function parseAppSettings(
toml: string
): DeepPartial<Configuration> | Error {

View File

@ -13,7 +13,6 @@ import {
listProjects,
readAppSettingsFile,
} from './desktop'
import { engineCommandManager } from './singletons'
export const isHidden = (fileOrDir: FileEntry) =>
!!fileOrDir.name?.startsWith('.')
@ -116,9 +115,6 @@ export async function createAndOpenNewTutorialProject({
) => void
navigate: (path: string) => void
}) {
// Clear the scene.
engineCommandManager.clearScene()
// Create a new project with the onboarding project name
const configuration = await readAppSettingsFile()
const projects = await listProjects(configuration)

View File

@ -5,7 +5,7 @@ import { isDesktop } from './isDesktop'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { parseProjectSettings } from 'lang/wasm'
import { err } from './trap'
import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
interface OnSubmitProps {
@ -28,7 +28,7 @@ export function kclCommands(
groupId: 'code',
icon: 'code',
onSubmit: () => {
kclManager.format()
kclManager.format().catch(reportRejection)
},
},
{

View File

@ -3,8 +3,10 @@ pub(crate) mod digest;
pub mod modify;
pub mod types;
use crate::parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject};
use crate::source_range::ModuleId;
use crate::{
parsing::ast::types::{BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberObject},
source_range::ModuleId,
};
impl BodyItem {
pub fn module_id(&self) -> ModuleId {

View File

@ -8,7 +8,6 @@ use std::{
};
use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

View File

@ -4,7 +4,9 @@ use std::{str::FromStr, sync::Arc};
use futures::stream::TryStreamExt;
use gloo_utils::format::JsValueSerdeExt;
use kcl_lib::{CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Program};
use kcl_lib::{
exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Program,
};
use tokio::sync::RwLock;
use tower_lsp::{LspService, Server};
use wasm_bindgen::prelude::*;
@ -22,6 +24,36 @@ async fn read_old_ast_memory() -> Option<OldAstState> {
lock.clone()
}
async fn bust_cache() {
// We don't use the cache in mock mode.
let mut current_cache = OLD_AST_MEMORY.write().await;
// Set the cache to None.
*current_cache = None;
}
// wasm_bindgen wrapper for clearing the scene and busting the cache.
#[wasm_bindgen]
pub async fn clear_scene_and_bust_cache(
engine_manager: kcl_lib::wasm_engine::EngineCommandManager,
) -> Result<(), String> {
console_error_panic_hook::set_once();
// Bust the cache.
bust_cache().await;
let engine = kcl_lib::wasm_engine::EngineConnection::new(engine_manager)
.await
.map_err(|e| format!("{:?}", e))?;
let mut id_generator: IdGenerator = Default::default();
engine
.clear_scene(&mut id_generator, Default::default())
.await
.map_err(|e| e.to_string())?;
Ok(())
}
// wasm_bindgen wrapper for execute
#[wasm_bindgen]
pub async fn execute_wasm(
@ -74,10 +106,7 @@ pub async fn execute_wasm(
.map_err(String::from)
{
if !is_mock {
// We don't use the cache in mock mode.
let mut current_cache = OLD_AST_MEMORY.write().await;
// Set the cache to None.
*current_cache = None;
bust_cache().await;
}
// Throw the error.