From dd1534a61d861c12c4622465d85111fa74933ac9 Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Mon, 17 Mar 2025 18:26:11 -0700 Subject: [PATCH] Cleanup rust/ts interface a but more w new rustContext (#5848) * do the rust side Signed-off-by: Jess Frazelle * cleanup ts side Signed-off-by: Jess Frazelle * updates Signed-off-by: Jess Frazelle * typo Signed-off-by: Jess Frazelle --------- Signed-off-by: Jess Frazelle --- rust/kcl-lib/src/execution/mod.rs | 17 +- rust/kcl-lib/src/lib.rs | 4 + rust/kcl-wasm-lib/src/context.rs | 45 +++++- rust/kcl-wasm-lib/src/lib.rs | 4 + rust/kcl-wasm-lib/src/lsp.rs | 147 +++++++++++++++++ rust/kcl-wasm-lib/src/wasm.rs | 171 -------------------- src/clientSideScene/ClientSideSceneComp.tsx | 8 +- src/clientSideScene/sceneEntities.ts | 27 +--- src/editor/plugins/lsp/worker.ts | 16 +- src/lang/KclSingleton.ts | 10 +- src/lang/langHelpers.ts | 122 ++++++++------ src/lang/modifyAst/deleteSelection.ts | 8 +- src/lang/wasm.ts | 31 +--- src/lib/kclHelpers.ts | 13 +- src/lib/rustContext.ts | 39 +++++ src/lib/testHelpers.ts | 83 +--------- src/lib/wasm_lib_wrapper.ts | 4 - 17 files changed, 351 insertions(+), 398 deletions(-) create mode 100644 rust/kcl-wasm-lib/src/lsp.rs diff --git a/rust/kcl-lib/src/execution/mod.rs b/rust/kcl-lib/src/execution/mod.rs index be8969c7d..3372317ee 100644 --- a/rust/kcl-lib/src/execution/mod.rs +++ b/rust/kcl-lib/src/execution/mod.rs @@ -430,21 +430,14 @@ impl ExecutorContext { } #[cfg(target_arch = "wasm32")] - pub async fn new_mock( - fs_manager: crate::fs::wasm::FileSystemManager, - settings: ExecutorSettings, - ) -> Result { - Ok(ExecutorContext { - engine: Arc::new(Box::new( - crate::engine::conn_mock::EngineConnection::new() - .await - .map_err(|e| format!("{:?}", e))?, - )), - fs: Arc::new(FileManager::new(fs_manager)), + pub fn new_mock(engine: Arc>, fs: Arc, settings: ExecutorSettings) -> Self { + ExecutorContext { + engine, + fs, stdlib: Arc::new(StdLib::new()), settings, context_type: ContextType::Mock, - }) + } } #[cfg(not(target_arch = "wasm32"))] diff --git a/rust/kcl-lib/src/lib.rs b/rust/kcl-lib/src/lib.rs index 0a855eebe..ef15040be 100644 --- a/rust/kcl-lib/src/lib.rs +++ b/rust/kcl-lib/src/lib.rs @@ -114,6 +114,10 @@ pub mod wasm_engine { }; } +pub mod mock_engine { + pub use crate::engine::conn_mock::EngineConnection; +} + #[cfg(not(target_arch = "wasm32"))] pub mod native_engine { pub use crate::engine::conn::EngineConnection; diff --git a/rust/kcl-wasm-lib/src/context.rs b/rust/kcl-wasm-lib/src/context.rs index dfa0dadcd..9faf16aa2 100644 --- a/rust/kcl-wasm-lib/src/context.rs +++ b/rust/kcl-wasm-lib/src/context.rs @@ -10,6 +10,7 @@ use wasm_bindgen::prelude::*; pub struct Context { engine: Arc>, fs: Arc, + mock_engine: Arc>, } #[wasm_bindgen] @@ -28,16 +29,34 @@ impl Context { .map_err(|e| format!("{:?}", e))?, )), fs: Arc::new(FileManager::new(fs_manager)), + mock_engine: Arc::new(Box::new( + kcl_lib::mock_engine::EngineConnection::new() + .await + .map_err(|e| format!("{:?}", e))?, + )), }) } - fn create_executor_ctx(&self, settings: &str, path: Option) -> Result { + fn create_executor_ctx( + &self, + settings: &str, + path: Option, + is_mock: bool, + ) -> Result { let config: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?; let mut settings: kcl_lib::ExecutorSettings = config.into(); if let Some(path) = path { settings.with_current_file(std::path::PathBuf::from(path)); } + if is_mock { + return Ok(kcl_lib::ExecutorContext::new_mock( + self.mock_engine.clone(), + self.fs.clone(), + settings.into(), + )); + } + Ok(kcl_lib::ExecutorContext::new( self.engine.clone(), self.fs.clone(), @@ -57,7 +76,7 @@ impl Context { let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?; - let ctx = self.create_executor_ctx(settings, path)?; + let ctx = self.create_executor_ctx(settings, path, false)?; match ctx.run_with_caching(program).await { // The serde-wasm-bindgen does not work here because of weird HashMap issues. // DO NOT USE serde_wasm_bindgen::to_value it will break the frontend. @@ -65,4 +84,26 @@ impl Context { Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?), } } + + /// Execute a program in mock mode. + #[wasm_bindgen(js_name = executeMock)] + pub async fn execute_mock( + &self, + program_ast_json: &str, + path: Option, + settings: &str, + use_prev_memory: bool, + ) -> Result { + console_error_panic_hook::set_once(); + + let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?; + + let ctx = self.create_executor_ctx(settings, path, true)?; + match ctx.run_mock(program, use_prev_memory).await { + // The serde-wasm-bindgen does not work here because of weird HashMap issues. + // DO NOT USE serde_wasm_bindgen::to_value it will break the frontend. + Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()), + Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?), + } + } } diff --git a/rust/kcl-wasm-lib/src/lib.rs b/rust/kcl-wasm-lib/src/lib.rs index 2353f5efc..75f54d40d 100644 --- a/rust/kcl-wasm-lib/src/lib.rs +++ b/rust/kcl-wasm-lib/src/lib.rs @@ -3,9 +3,13 @@ #[cfg(target_arch = "wasm32")] mod context; #[cfg(target_arch = "wasm32")] +mod lsp; +#[cfg(target_arch = "wasm32")] mod wasm; #[cfg(target_arch = "wasm32")] pub use context::*; #[cfg(target_arch = "wasm32")] +pub use lsp::*; +#[cfg(target_arch = "wasm32")] pub use wasm::*; diff --git a/rust/kcl-wasm-lib/src/lsp.rs b/rust/kcl-wasm-lib/src/lsp.rs new file mode 100644 index 000000000..1a5359c3d --- /dev/null +++ b/rust/kcl-wasm-lib/src/lsp.rs @@ -0,0 +1,147 @@ +//! Wasm interface for our LSP servers. + +use futures::stream::TryStreamExt; +use tower_lsp::{LspService, Server}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct LspServerConfig { + into_server: js_sys::AsyncIterator, + from_server: web_sys::WritableStream, + fs: kcl_lib::wasm_engine::FileSystemManager, +} + +#[wasm_bindgen] +impl LspServerConfig { + #[wasm_bindgen(constructor)] + pub fn new( + into_server: js_sys::AsyncIterator, + from_server: web_sys::WritableStream, + fs: kcl_lib::wasm_engine::FileSystemManager, + ) -> Self { + Self { + into_server, + from_server, + fs, + } + } +} + +/// Run the `kcl` lsp server. +// +// NOTE: we don't use web_sys::ReadableStream for input here because on the +// browser side we need to use a ReadableByteStreamController to construct it +// and so far only Chromium-based browsers support that functionality. + +// NOTE: input needs to be an AsyncIterator specifically +#[wasm_bindgen] +pub async fn lsp_run_kcl(config: LspServerConfig, token: String, baseurl: String) -> Result<(), JsValue> { + console_error_panic_hook::set_once(); + + let LspServerConfig { + into_server, + from_server, + fs, + } = config; + + let executor_ctx = None; + + let mut zoo_client = kittycad::Client::new(token); + zoo_client.set_base_url(baseurl.as_str()); + + // Check if we can send telemetry for this user. + let can_send_telemetry = match zoo_client.users().get_privacy_settings().await { + Ok(privacy_settings) => privacy_settings.can_train_on_data, + Err(err) => { + // In the case of dev we don't always have a sub set, but prod we should. + if err + .to_string() + .contains("The modeling app subscription type is missing.") + { + true + } else { + web_sys::console::warn_1(&format!("Failed to get privacy settings: {err:?}").into()); + false + } + } + }; + + let (service, socket) = LspService::build(|client| { + kcl_lib::KclLspBackend::new_wasm(client, executor_ctx, fs, zoo_client, can_send_telemetry).unwrap() + }) + .custom_method("kcl/updateUnits", kcl_lib::KclLspBackend::update_units) + .custom_method("kcl/updateCanExecute", kcl_lib::KclLspBackend::update_can_execute) + .finish(); + + let input = wasm_bindgen_futures::stream::JsStream::from(into_server); + let input = input + .map_ok(|value| { + value + .dyn_into::() + .expect("could not cast stream item to Uint8Array") + .to_vec() + }) + .map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other)) + .into_async_read(); + + let output = wasm_bindgen::JsCast::unchecked_into::(from_server); + let output = wasm_streams::WritableStream::from_raw(output); + let output = output.try_into_async_write().map_err(|err| err.0)?; + + Server::new(input, output, socket).serve(service).await; + + Ok(()) +} + +/// Run the `copilot` lsp server. +// +// NOTE: we don't use web_sys::ReadableStream for input here because on the +// browser side we need to use a ReadableByteStreamController to construct it +// and so far only Chromium-based browsers support that functionality. + +// NOTE: input needs to be an AsyncIterator specifically +#[wasm_bindgen] +pub async fn lsp_run_copilot(config: LspServerConfig, token: String, baseurl: String) -> Result<(), JsValue> { + console_error_panic_hook::set_once(); + + let LspServerConfig { + into_server, + from_server, + fs, + } = config; + + let mut zoo_client = kittycad::Client::new(token); + zoo_client.set_base_url(baseurl.as_str()); + + let dev_mode = baseurl == "https://api.dev.zoo.dev"; + + let (service, socket) = + LspService::build(|client| kcl_lib::CopilotLspBackend::new_wasm(client, fs, zoo_client, dev_mode)) + .custom_method("copilot/setEditorInfo", kcl_lib::CopilotLspBackend::set_editor_info) + .custom_method( + "copilot/getCompletions", + kcl_lib::CopilotLspBackend::get_completions_cycling, + ) + .custom_method("copilot/notifyAccepted", kcl_lib::CopilotLspBackend::accept_completion) + .custom_method("copilot/notifyRejected", kcl_lib::CopilotLspBackend::reject_completions) + .finish(); + + let input = wasm_bindgen_futures::stream::JsStream::from(into_server); + let input = input + .map_ok(|value| { + value + .dyn_into::() + .expect("could not cast stream item to Uint8Array") + .to_vec() + }) + .map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other)) + .into_async_read(); + + let output = wasm_bindgen::JsCast::unchecked_into::(from_server); + let output = wasm_streams::WritableStream::from_raw(output); + let output = output.try_into_async_write().map_err(|err| err.0)?; + + Server::new(input, output, socket).serve(service).await; + + Ok(()) +} diff --git a/rust/kcl-wasm-lib/src/wasm.rs b/rust/kcl-wasm-lib/src/wasm.rs index 09f9a2189..c39442542 100644 --- a/rust/kcl-wasm-lib/src/wasm.rs +++ b/rust/kcl-wasm-lib/src/wasm.rs @@ -1,38 +1,9 @@ //! Wasm bindings for `kcl`. -use futures::stream::TryStreamExt; use gloo_utils::format::JsValueSerdeExt; use kcl_lib::{pretty::NumericSuffix, CoreDump, Point2d, Program}; -use tower_lsp::{LspService, Server}; use wasm_bindgen::prelude::*; -// wasm_bindgen wrapper for mock execute -#[wasm_bindgen] -pub async fn execute_mock( - program_ast_json: &str, - path: Option, - settings: &str, - use_prev_memory: bool, - fs_manager: kcl_lib::wasm_engine::FileSystemManager, -) -> Result { - console_error_panic_hook::set_once(); - - let program: Program = serde_json::from_str(program_ast_json).map_err(|e| e.to_string())?; - let config: kcl_lib::Configuration = serde_json::from_str(settings).map_err(|e| e.to_string())?; - let mut settings: kcl_lib::ExecutorSettings = config.into(); - if let Some(path) = path { - settings.with_current_file(std::path::PathBuf::from(path)); - } - - let ctx = kcl_lib::ExecutorContext::new_mock(fs_manager, settings.into()).await?; - match ctx.run_mock(program, use_prev_memory).await { - // The serde-wasm-bindgen does not work here because of weird HashMap issues. - // DO NOT USE serde_wasm_bindgen::to_value it will break the frontend. - Ok(outcome) => JsValue::from_serde(&outcome).map_err(|e| e.to_string()), - Err(err) => Err(serde_json::to_string(&err).map_err(|serde_err| serde_err.to_string())?), - } -} - // wasm_bindgen wrapper for execute #[wasm_bindgen] pub async fn kcl_lint(program_ast_json: &str) -> Result { @@ -94,148 +65,6 @@ pub fn format_number(value: f64, suffix_json: &str) -> Result { Ok(kcl_lib::pretty::format_number(value, suffix)) } -#[wasm_bindgen] -pub struct ServerConfig { - into_server: js_sys::AsyncIterator, - from_server: web_sys::WritableStream, - fs: kcl_lib::wasm_engine::FileSystemManager, -} - -#[wasm_bindgen] -impl ServerConfig { - #[wasm_bindgen(constructor)] - pub fn new( - into_server: js_sys::AsyncIterator, - from_server: web_sys::WritableStream, - fs: kcl_lib::wasm_engine::FileSystemManager, - ) -> Self { - Self { - into_server, - from_server, - fs, - } - } -} - -/// Run the `kcl` lsp server. -// -// NOTE: we don't use web_sys::ReadableStream for input here because on the -// browser side we need to use a ReadableByteStreamController to construct it -// and so far only Chromium-based browsers support that functionality. - -// NOTE: input needs to be an AsyncIterator specifically -#[wasm_bindgen] -pub async fn kcl_lsp_run(config: ServerConfig, token: String, baseurl: String) -> Result<(), JsValue> { - console_error_panic_hook::set_once(); - - let ServerConfig { - into_server, - from_server, - fs, - } = config; - - let executor_ctx = None; - - let mut zoo_client = kittycad::Client::new(token); - zoo_client.set_base_url(baseurl.as_str()); - - // Check if we can send telemetry for this user. - let can_send_telemetry = match zoo_client.users().get_privacy_settings().await { - Ok(privacy_settings) => privacy_settings.can_train_on_data, - Err(err) => { - // In the case of dev we don't always have a sub set, but prod we should. - if err - .to_string() - .contains("The modeling app subscription type is missing.") - { - true - } else { - web_sys::console::warn_1(&format!("Failed to get privacy settings: {err:?}").into()); - false - } - } - }; - - let (service, socket) = LspService::build(|client| { - kcl_lib::KclLspBackend::new_wasm(client, executor_ctx, fs, zoo_client, can_send_telemetry).unwrap() - }) - .custom_method("kcl/updateUnits", kcl_lib::KclLspBackend::update_units) - .custom_method("kcl/updateCanExecute", kcl_lib::KclLspBackend::update_can_execute) - .finish(); - - let input = wasm_bindgen_futures::stream::JsStream::from(into_server); - let input = input - .map_ok(|value| { - value - .dyn_into::() - .expect("could not cast stream item to Uint8Array") - .to_vec() - }) - .map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other)) - .into_async_read(); - - let output = wasm_bindgen::JsCast::unchecked_into::(from_server); - let output = wasm_streams::WritableStream::from_raw(output); - let output = output.try_into_async_write().map_err(|err| err.0)?; - - Server::new(input, output, socket).serve(service).await; - - Ok(()) -} - -/// Run the `copilot` lsp server. -// -// NOTE: we don't use web_sys::ReadableStream for input here because on the -// browser side we need to use a ReadableByteStreamController to construct it -// and so far only Chromium-based browsers support that functionality. - -// NOTE: input needs to be an AsyncIterator specifically -#[wasm_bindgen] -pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: String) -> Result<(), JsValue> { - console_error_panic_hook::set_once(); - - let ServerConfig { - into_server, - from_server, - fs, - } = config; - - let mut zoo_client = kittycad::Client::new(token); - zoo_client.set_base_url(baseurl.as_str()); - - let dev_mode = baseurl == "https://api.dev.zoo.dev"; - - let (service, socket) = - LspService::build(|client| kcl_lib::CopilotLspBackend::new_wasm(client, fs, zoo_client, dev_mode)) - .custom_method("copilot/setEditorInfo", kcl_lib::CopilotLspBackend::set_editor_info) - .custom_method( - "copilot/getCompletions", - kcl_lib::CopilotLspBackend::get_completions_cycling, - ) - .custom_method("copilot/notifyAccepted", kcl_lib::CopilotLspBackend::accept_completion) - .custom_method("copilot/notifyRejected", kcl_lib::CopilotLspBackend::reject_completions) - .finish(); - - let input = wasm_bindgen_futures::stream::JsStream::from(into_server); - let input = input - .map_ok(|value| { - value - .dyn_into::() - .expect("could not cast stream item to Uint8Array") - .to_vec() - }) - .map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other)) - .into_async_read(); - - let output = wasm_bindgen::JsCast::unchecked_into::(from_server); - let output = wasm_streams::WritableStream::from_raw(output); - let output = output.try_into_async_write().map_err(|err| err.0)?; - - Server::new(input, output, socket).serve(service).await; - - Ok(()) -} - #[wasm_bindgen] pub fn is_points_ccw(points: &[f64]) -> i32 { console_error_panic_hook::set_once(); diff --git a/src/clientSideScene/ClientSideSceneComp.tsx b/src/clientSideScene/ClientSideSceneComp.tsx index 2b0dea5f0..3992eed6a 100644 --- a/src/clientSideScene/ClientSideSceneComp.tsx +++ b/src/clientSideScene/ClientSideSceneComp.tsx @@ -39,7 +39,7 @@ import { getConstraintInfo, getConstraintInfoKw } from 'lang/std/sketch' import { Dialog, Popover, Transition } from '@headlessui/react' import toast from 'react-hot-toast' import { InstanceProps, create } from 'react-modal-promise' -import { executeAst } from 'lang/langHelpers' +import { executeAstMock } from 'lang/langHelpers' import { deleteSegmentFromPipeExpression, removeSingleConstraintInfo, @@ -437,12 +437,10 @@ export async function deleteSegment({ if (err(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult) modifiedAst = pResult.program - const testExecute = await executeAst({ + const testExecute = await executeAstMock({ ast: modifiedAst, - engineCommandManager: engineCommandManager, - isMock: true, - rustContext, usePrevMemory: false, + rustContext: rustContext, }) if (testExecute.errors.length) { toast.error('Segment tag used outside of current Sketch. Could not delete.') diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index cda0e29f8..98cb53f3a 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -66,7 +66,7 @@ import { } from 'lib/singletons' import { getNodeFromPath } from 'lang/queryAst' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' -import { executeAst, ToolTip } from 'lang/langHelpers' +import { executeAstMock, ToolTip } from 'lang/langHelpers' import { createProfileStartHandle, dashedStraight, @@ -91,7 +91,6 @@ import { createLabeledArg, createLiteral, createNodeFromExprSnippet, - createObjectExpression, createPipeExpression, createPipeSubstitution, createVariableDeclaration, @@ -661,11 +660,9 @@ export class SceneEntities { if (err(prepared)) return Promise.reject(prepared) const { truncatedAst, variableDeclarationName } = prepared - const { execState } = await executeAst({ + const { execState } = await executeAstMock({ ast: truncatedAst, - engineCommandManager: this.engineCommandManager, rustContext, - isMock: true, }) const sketchesInfo = getSketchesInfo({ sketchNodePaths, @@ -1239,11 +1236,9 @@ export class SceneEntities { updateRectangleSketch(sketchInit, x, y, tag) } - const { execState } = await executeAst({ + const { execState } = await executeAstMock({ ast: truncatedAst, - engineCommandManager: this.engineCommandManager, rustContext, - isMock: true, }) const sketch = sketchFromKclValue(execState.variables[varName], varName) if (err(sketch)) return Promise.reject(sketch) @@ -1445,11 +1440,9 @@ export class SceneEntities { ) } - const { execState } = await executeAst({ + const { execState } = await executeAstMock({ ast: truncatedAst, - engineCommandManager: this.engineCommandManager, rustContext, - isMock: true, }) const sketch = sketchFromKclValue(execState.variables[varName], varName) if (err(sketch)) return Promise.reject(sketch) @@ -1624,11 +1617,9 @@ export class SceneEntities { modded = moddedResult.modifiedAst } - const { execState } = await executeAst({ + const { execState } = await executeAstMock({ ast: modded, - engineCommandManager: this.engineCommandManager, rustContext, - isMock: true, }) const sketch = sketchFromKclValue(execState.variables[varName], varName) if (err(sketch)) return @@ -2307,11 +2298,9 @@ export class SceneEntities { modded = moddedResult.modifiedAst } - const { execState } = await executeAst({ + const { execState } = await executeAstMock({ ast: modded, - engineCommandManager: this.engineCommandManager, rustContext, - isMock: true, }) const sketch = sketchFromKclValue(execState.variables[varName], varName) if (err(sketch)) return @@ -2886,11 +2875,9 @@ export class SceneEntities { // 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 codeManager.updateCodeEditor(code) - const { execState } = await executeAst({ + const { execState } = await executeAstMock({ ast: truncatedAst, - engineCommandManager: this.engineCommandManager, rustContext, - isMock: true, }) const variables = execState.variables const sketchesInfo = getSketchesInfo({ diff --git a/src/editor/plugins/lsp/worker.ts b/src/editor/plugins/lsp/worker.ts index 2ff83f51e..05cdfd2cc 100644 --- a/src/editor/plugins/lsp/worker.ts +++ b/src/editor/plugins/lsp/worker.ts @@ -6,9 +6,9 @@ import { } from '@kittycad/codemirror-lsp-client' import { fileSystemManager } from 'lang/std/fileSystemManager' import init, { - ServerConfig, - copilot_lsp_run, - kcl_lsp_run, + LspServerConfig, + lsp_run_copilot, + lsp_run_kcl, } from '@rust/kcl-wasm-lib/pkg/kcl_wasm_lib' import * as jsrpc from 'json-rpc-2.0' import { @@ -30,13 +30,13 @@ const initialise = async (wasmUrl: string) => { } export async function copilotLspRun( - config: ServerConfig, + config: LspServerConfig, token: string, baseUrl: string ) { try { console.log('starting copilot lsp') - await copilot_lsp_run(config, token, baseUrl) + await lsp_run_copilot(config, token, baseUrl) } catch (e: any) { console.log('copilot lsp failed', e) // We can't restart here because a moved value, we should do this another way. @@ -44,13 +44,13 @@ export async function copilotLspRun( } export async function kclLspRun( - config: ServerConfig, + config: LspServerConfig, token: string, baseUrl: string ) { try { console.log('start kcl lsp') - await kcl_lsp_run(config, token, baseUrl) + await lsp_run_kcl(config, token, baseUrl) } catch (e: any) { console.log('kcl lsp failed', e) // We can't restart here because a moved value, we should do this another way. @@ -70,7 +70,7 @@ onmessage = function (event: MessageEvent) { initialise(wasmUrl) .then(async (instantiatedModule) => { console.log('Worker: WASM module loaded', worker, instantiatedModule) - const config = new ServerConfig( + const config = new LspServerConfig( intoServer, fromServer, fileSystemManager diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 9813548ae..ce4379e7f 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -1,4 +1,4 @@ -import { executeAst, lintAst } from 'lang/langHelpers' +import { executeAst, executeAstMock, lintAst } from 'lang/langHelpers' import { handleSelectionBatch, Selections } from 'lib/selections' import { KCLError, @@ -338,7 +338,7 @@ export class KclManager { if (this.isExecuting) { this.executeIsStale = args - // The previous execteAst will be rejected and cleaned up. The execution will be marked as stale. + // The previous executeAst will be rejected and cleaned up. The execution will be marked as stale. // A new executeAst will start. this.engineCommandManager.rejectAllModelingCommands( EXECUTE_AST_INTERRUPT_ERROR_MESSAGE @@ -358,9 +358,7 @@ export class KclManager { const { logs, errors, execState, isInterrupted } = await executeAst({ ast, path: codeManager.currentFilePath || undefined, - engineCommandManager: this.engineCommandManager, rustContext, - isMock: false, }) // Program was not interrupted, setup the scene @@ -476,11 +474,9 @@ export class KclManager { } this._ast = { ...newAst } - const { logs, errors, execState } = await executeAst({ + const { logs, errors, execState } = await executeAstMock({ ast: newAst, - engineCommandManager: this.engineCommandManager, rustContext, - isMock: true, }) this._logs = logs diff --git a/src/lang/langHelpers.ts b/src/lang/langHelpers.ts index 3eeabadc3..604529dc2 100644 --- a/src/lang/langHelpers.ts +++ b/src/lang/langHelpers.ts @@ -1,12 +1,10 @@ import { Program, - executeMock, kclLint, emptyExecState, ExecState, jsAppSettings, } from 'lang/wasm' -import { EngineCommandManager } from 'lang/std/engineConnection' import { KCLError } from 'lang/errors' import { Diagnostic } from '@codemirror/lint' import { Node } from '@rust/kcl-lib/bindings/Node' @@ -50,33 +48,27 @@ export const toolTips: Array = [ 'arcTo', ] -export async function executeAst({ - ast, - path, - engineCommandManager, - isMock, - usePrevMemory, - rustContext, -}: { - ast: Node - path?: string - engineCommandManager: EngineCommandManager - rustContext: RustContext - isMock: boolean - usePrevMemory?: boolean - isInterrupted?: boolean -}): Promise<{ +interface ExecutionResult { logs: string[] errors: KCLError[] execState: ExecState isInterrupted: boolean -}> { - try { - const execState = await (isMock - ? executeMock(ast, usePrevMemory, path) - : rustContext.execute(ast, { settings: await jsAppSettings() }, path)) +} - await engineCommandManager.waitForAllCommands() +export async function executeAst({ + ast, + rustContext, + path, +}: { + ast: Node + rustContext: RustContext + path?: string +}): Promise { + try { + const settings = { settings: await jsAppSettings() } + const execState = await rustContext.execute(ast, settings, path) + + await rustContext.waitForAllEngineCommands() return { logs: [], errors: [], @@ -84,29 +76,65 @@ export async function executeAst({ isInterrupted: false, } } catch (e: any) { - let isInterrupted = false - if (e instanceof KCLError) { - // Detect if it is a force interrupt error which is not a KCL processing error. - if ( - e.msg === - 'Failed to wait for promise from engine: JsValue("Force interrupt, executionIsStale, new AST requested")' - ) { - isInterrupted = true - } - return { - errors: [e], - logs: [], - execState: emptyExecState(), - isInterrupted, - } - } else { - console.log(e) - return { - logs: [e], - errors: [], - execState: emptyExecState(), - isInterrupted, - } + return handleExecuteError(e) + } +} + +export async function executeAstMock({ + ast, + rustContext, + path, + usePrevMemory, +}: { + ast: Node + rustContext: RustContext + path?: string + usePrevMemory?: boolean +}): Promise { + try { + const settings = { settings: await jsAppSettings() } + const execState = await rustContext.executeMock( + ast, + settings, + path, + usePrevMemory + ) + + await rustContext.waitForAllEngineCommands() + return { + logs: [], + errors: [], + execState, + isInterrupted: false, + } + } catch (e: any) { + return handleExecuteError(e) + } +} + +function handleExecuteError(e: any): ExecutionResult { + let isInterrupted = false + if (e instanceof KCLError) { + // Detect if it is a force interrupt error which is not a KCL processing error. + if ( + e.msg === + 'Failed to wait for promise from engine: JsValue("Force interrupt, executionIsStale, new AST requested")' + ) { + isInterrupted = true + } + return { + errors: [e], + logs: [], + execState: emptyExecState(), + isInterrupted, + } + } else { + console.log(e) + return { + logs: [e], + errors: [], + execState: emptyExecState(), + isInterrupted, } } } diff --git a/src/lang/modifyAst/deleteSelection.ts b/src/lang/modifyAst/deleteSelection.ts index a46588d5a..7941ddc8c 100644 --- a/src/lang/modifyAst/deleteSelection.ts +++ b/src/lang/modifyAst/deleteSelection.ts @@ -8,7 +8,7 @@ import { rustContext, } from 'lib/singletons' import { err } from 'lib/trap' -import { executeAst } from 'lang/langHelpers' +import { executeAstMock } from 'lang/langHelpers' export const deletionErrorMessage = 'Unable to delete selection. Please edit manually in code pane.' @@ -29,11 +29,9 @@ export async function deleteSelectionPromise( return new Error(deletionErrorMessage) } - const testExecute = await executeAst({ + const testExecute = await executeAstMock({ ast: modifiedAst, - engineCommandManager, - rustContext, - isMock: true, + rustContext: rustContext, }) if (testExecute.errors.length) { return new Error(deletionErrorMessage) diff --git a/src/lang/wasm.ts b/src/lang/wasm.ts index d5a1c1e74..a0308029b 100644 --- a/src/lang/wasm.ts +++ b/src/lang/wasm.ts @@ -3,7 +3,6 @@ import { parse_wasm, recast_wasm, format_number, - execute_mock, kcl_lint, is_points_ccw, get_tangential_arc_to_info, @@ -340,7 +339,7 @@ export function execStateFromRust( } } -function mockExecStateFromRust(execOutcome: RustExecOutcome): ExecState { +export function mockExecStateFromRust(execOutcome: RustExecOutcome): ExecState { return { variables: execOutcome.variables, operations: execOutcome.operations, @@ -404,34 +403,6 @@ export function sketchFromKclValue( return result } -/** - * Execute a KCL program. - * @param node The AST of the program to execute. - * @param path The full path of the file being executed. Use `null` for - * expressions that don't have a file, like expressions in the command bar. - */ -export const executeMock = async ( - node: Node, - usePrevMemory?: boolean, - path?: string -): Promise => { - try { - if (usePrevMemory === undefined) { - usePrevMemory = true - } - const execOutcome: RustExecOutcome = await execute_mock( - JSON.stringify(node), - path, - JSON.stringify({ settings: await jsAppSettings() }), - usePrevMemory, - fileSystemManager - ) - return mockExecStateFromRust(execOutcome) - } catch (e: any) { - return Promise.reject(errFromErrWithOutputs(e)) - } -} - export const jsAppSettings = async () => { let jsAppSettings = default_app_settings() if (!TEST) { diff --git a/src/lib/kclHelpers.ts b/src/lib/kclHelpers.ts index a0c2cb677..34a65bbf6 100644 --- a/src/lib/kclHelpers.ts +++ b/src/lib/kclHelpers.ts @@ -1,9 +1,8 @@ import { err } from './trap' -import { engineCommandManager, rustContext } from 'lib/singletons' -import { parse, resultIsOk, VariableMap } from 'lang/wasm' -import { PrevVariable } from 'lang/queryAst' -import { executeAst } from 'lang/langHelpers' +import { parse, resultIsOk } from 'lang/wasm' +import { executeAstMock } from 'lang/langHelpers' import { KclExpression } from './commandTypes' +import { rustContext } from './singletons' const DUMMY_VARIABLE_NAME = '__result__' @@ -19,11 +18,9 @@ export async function getCalculatedKclExpressionValue(value: string) { const ast = pResult.program // Execute the program without hitting the engine - const { execState } = await executeAst({ + const { execState } = await executeAstMock({ ast, - engineCommandManager, - rustContext, - isMock: true, + rustContext: rustContext, }) // Find the variable declaration for the result diff --git a/src/lib/rustContext.ts b/src/lib/rustContext.ts index 9ba0d5cd4..387ae18d8 100644 --- a/src/lib/rustContext.ts +++ b/src/lib/rustContext.ts @@ -4,6 +4,7 @@ import { ExecState, execStateFromRust, initPromise, + mockExecStateFromRust, } from 'lang/wasm' import { getModule, ModuleType } from 'lib/wasm_lib_wrapper' import { fileSystemManager } from 'lang/std/fileSystemManager' @@ -48,6 +49,9 @@ export default class RustContext { // Create a new context instance async create() { this.rustInstance = getModule() + // We need this await here, DO NOT REMOVE it even if your editor says it's + // unnecessary. The constructor of the module is async and it will not + // resolve if you don't await it. this.ctxInstance = await new this.rustInstance.Context( this.engineCommandManager, fileSystemManager @@ -87,6 +91,41 @@ export default class RustContext { return Promise.reject(emptyExecState()) } + // Execute a program with in mock mode. + async executeMock( + node: Node, + settings: DeepPartial, + path?: string, + usePrevMemory?: boolean + ): Promise { + await this._checkInstance() + + if (this.ctxInstance) { + try { + if (usePrevMemory === undefined) { + usePrevMemory = true + } + + const result = await this.ctxInstance.executeMock( + JSON.stringify(node), + path, + JSON.stringify(settings), + usePrevMemory + ) + return mockExecStateFromRust(result) + } catch (e: any) { + return Promise.reject(errFromErrWithOutputs(e)) + } + } + + // You will never get here. + return Promise.reject(emptyExecState()) + } + + async waitForAllEngineCommands() { + await this.engineCommandManager.waitForAllCommands() + } + get defaultPlanes() { return this._defaultPlanes } diff --git a/src/lib/testHelpers.ts b/src/lib/testHelpers.ts index 610f7f2ba..b9d70faf9 100644 --- a/src/lib/testHelpers.ts +++ b/src/lib/testHelpers.ts @@ -1,87 +1,12 @@ -import { - Program, - executeMock, - SourceRange, - ExecState, - VariableMap, -} from '../lang/wasm' -import { EngineCommandManager } from 'lang/std/engineConnection' -import { EngineCommand } from 'lang/std/artifactGraph' -import { Models } from '@kittycad/lib' -import { v4 as uuidv4 } from 'uuid' -import { DefaultPlanes } from '@rust/kcl-lib/bindings/DefaultPlanes' -import { err } from 'lib/trap' +import { Program, ExecState, jsAppSettings } from '../lang/wasm' import { Node } from '@rust/kcl-lib/bindings/Node' - -type WebSocketResponse = Models['WebSocketResponse_type'] - -const defaultPlanes: DefaultPlanes = { - xy: uuidv4(), - xz: uuidv4(), - yz: uuidv4(), - negXy: uuidv4(), - negXz: uuidv4(), - negYz: uuidv4(), -} - -class MockEngineCommandManager { - // eslint-disable-next-line @typescript-eslint/no-useless-constructor - constructor(mockParams: { - setIsStreamReady: (isReady: boolean) => void - setMediaStream: (stream: MediaStream) => void - }) {} - startNewSession() {} - waitForAllCommands() {} - waitForReady = new Promise((resolve) => resolve()) - sendModelingCommand({ - id, - range, - command, - }: { - id: string - range: SourceRange - command: EngineCommand - }): Promise { - const response: WebSocketResponse = { - success: true, - resp: { - type: 'modeling', - data: { - modeling_response: { type: 'empty' }, - }, - }, - } - return Promise.resolve(JSON.stringify(response)) - } - async wasmGetDefaultPlanes(): Promise { - return JSON.stringify(defaultPlanes) - } - sendModelingCommandFromWasm( - id: string, - rangeStr: string, - commandStr: string - ): Promise { - if (id === undefined) { - return Promise.reject(new Error('id is undefined')) - } - if (rangeStr === undefined) { - return Promise.reject(new Error('rangeStr is undefined')) - } - if (commandStr === undefined) { - return Promise.reject(new Error('commandStr is undefined')) - } - const command: EngineCommand = JSON.parse(commandStr) - const range: SourceRange = JSON.parse(rangeStr) - - return this.sendModelingCommand({ id, range, command }) - } - sendSceneCommand() {} -} +import { rustContext } from './singletons' export async function enginelessExecutor( ast: Node, usePrevMemory?: boolean, path?: string ): Promise { - return await executeMock(ast, usePrevMemory, path) + const settings = { settings: await jsAppSettings() } + return await rustContext.executeMock(ast, settings, path, usePrevMemory) } diff --git a/src/lib/wasm_lib_wrapper.ts b/src/lib/wasm_lib_wrapper.ts index 33f436cc3..6b972e01b 100644 --- a/src/lib/wasm_lib_wrapper.ts +++ b/src/lib/wasm_lib_wrapper.ts @@ -11,7 +11,6 @@ import { parse_wasm as ParseWasm, recast_wasm as RecastWasm, format_number as FormatNumber, - execute_mock as ExecuteMock, kcl_lint as KclLint, is_points_ccw as IsPointsCcw, get_tangential_arc_to_info as GetTangentialArcToInfo, @@ -55,9 +54,6 @@ export const recast_wasm: typeof RecastWasm = (...args) => { export const format_number: typeof FormatNumber = (...args) => { return getModule().format_number(...args) } -export const execute_mock: typeof ExecuteMock = (...args) => { - return getModule().execute_mock(...args) -} export const kcl_lint: typeof KclLint = (...args) => { return getModule().kcl_lint(...args) }