BREAKING: KCL @settings are the source of truth for units (#5808)

This commit is contained in:
Jonathan Tran
2025-03-31 10:56:03 -04:00
committed by GitHub
parent eac5abba79
commit efc8c82d8b
237 changed files with 820 additions and 2146 deletions

View File

@ -1172,7 +1172,7 @@ fn find_examples(text: &str, filename: &str) -> Vec<(String, String)> {
async fn run_example(text: &str) -> Result<()> {
let program = crate::Program::parse_no_errs(text)?;
let ctx = ExecutorContext::new_with_default_client(crate::UnitLength::Mm).await?;
let ctx = ExecutorContext::new_with_default_client().await?;
let mut exec_state = crate::execution::ExecState::new(&ctx);
ctx.run(&program, &mut exec_state).await?;
Ok(())

View File

@ -1010,20 +1010,17 @@ mod test {
let std = walk_prelude();
for d in std {
for (i, eg) in d.examples().enumerate() {
let result =
match crate::test_server::execute_and_snapshot(eg, crate::settings::types::UnitLength::Mm, None)
.await
{
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e.error,
filename: format!("{}{i}", d.name()),
kcl_source: eg.to_string(),
}));
}
Err(other_err) => panic!("{}", other_err),
Ok(img) => img,
};
let result = match crate::test_server::execute_and_snapshot(eg, None).await {
Err(crate::errors::ExecError::Kcl(e)) => {
return Err(miette::Report::new(crate::errors::Report {
error: e.error,
filename: format!("{}{i}", d.name()),
kcl_source: eg.to_string(),
}));
}
Err(other_err) => panic!("{}", other_err),
Ok(img) => img,
};
twenty_twenty::assert_image(
format!("tests/outputs/serial_test_example_{}{i}.png", d.example_name()),
&result,

View File

@ -176,6 +176,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
)
.await?;
// Reset to the default units. Modules assume the engine starts in the
// default state.
self.set_units(Default::default(), source_range, id_generator).await?;
// Flush the batch queue, so clear is run right away.
// Otherwise the hooks below won't work.
self.flush_batch(false, source_range).await?;
@ -256,9 +260,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
self.set_edge_visibility(settings.highlight_edges, source_range, id_generator)
.await?;
// Change the units.
self.set_units(settings.units, source_range, id_generator).await?;
// Send the command to show the grid.
self.modify_grid(!settings.show_grid, source_range, id_generator)
.await?;

View File

@ -97,15 +97,6 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
// If the settings are different we might need to bust the cache.
// We specifically do this before checking if they are the exact same.
if old.settings != new.settings {
// If the units are different we need to re-execute the whole thing.
if old.settings.units != new.settings.units {
return CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: new.ast.clone(),
};
}
// If anything else is different we may not need to re-execute, but rather just
// run the settings again.
reapply_settings = true;
@ -424,50 +415,6 @@ shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
assert_eq!(result, CacheResult::NoAction(false));
}
// Changing the units with the exact same file should bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_units() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn('XY')
|> startProfileAt([-12, 12], %)
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
let ExecTestResults {
program, mut exec_ctxt, ..
} = parse_execute(new).await.unwrap();
// Change the settings to cm.
exec_ctxt.settings.units = crate::UnitLength::Cm;
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &Default::default(),
},
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(
result,
CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: program.ast
}
);
}
// Changing the grid settings with the exact same file should NOT bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_grid_setting() {
@ -615,4 +562,42 @@ startSketchOn('XY')
}
);
}
// Removing the units settings using an annotation, when it was non-default
// units, with the exact same file should bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
let old_code = r#"@settings(defaultLengthUnit = in)
startSketchOn('XY')
"#;
let new_code = r#"
startSketchOn('XY')
"#;
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
new_program.compute_digest();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &new_program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(
result,
CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: new_program.ast
}
);
}
}

View File

@ -110,12 +110,7 @@ impl ExecutorContext {
let old_units = exec_state.length_unit();
let original_execution = self.engine.replace_execution_kind(exec_kind).await;
let mut local_state = ModuleState::new(
&self.settings,
path.std_path(),
exec_state.stack().memory.clone(),
Some(module_id),
);
let mut local_state = ModuleState::new(path.std_path(), exec_state.stack().memory.clone(), Some(module_id));
if !preserve_mem {
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
}

View File

@ -39,7 +39,6 @@ use crate::{
fs::FileManager,
modules::{ModuleId, ModulePath},
parsing::ast::types::{Expr, ImportPath, NodeRef},
settings::types::UnitLength,
source_range::SourceRange,
std::StdLib,
CompilationError, ExecError, ExecutionKind, KclErrorWithOutputs,
@ -265,8 +264,6 @@ pub struct ExecutorContext {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
pub struct ExecutorSettings {
/// The project-default unit to use in modeling dimensions.
pub units: UnitLength,
/// Highlight edges of 3D objects?
pub highlight_edges: bool,
/// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
@ -287,7 +284,6 @@ pub struct ExecutorSettings {
impl Default for ExecutorSettings {
fn default() -> Self {
Self {
units: Default::default(),
highlight_edges: true,
enable_ssao: false,
show_grid: false,
@ -301,7 +297,6 @@ impl Default for ExecutorSettings {
impl From<crate::settings::types::Configuration> for ExecutorSettings {
fn from(config: crate::settings::types::Configuration) -> Self {
Self {
units: config.settings.modeling.base_unit,
highlight_edges: config.settings.modeling.highlight_edges.into(),
enable_ssao: config.settings.modeling.enable_ssao.into(),
show_grid: config.settings.modeling.show_scale_grid,
@ -315,7 +310,6 @@ impl From<crate::settings::types::Configuration> for ExecutorSettings {
impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
Self {
units: config.settings.modeling.base_unit,
highlight_edges: config.settings.modeling.highlight_edges.into(),
enable_ssao: config.settings.modeling.enable_ssao.into(),
show_grid: Default::default(),
@ -329,7 +323,6 @@ impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSet
impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
Self {
units: modeling.base_unit,
highlight_edges: modeling.highlight_edges.into(),
enable_ssao: modeling.enable_ssao.into(),
show_grid: modeling.show_scale_grid,
@ -343,7 +336,6 @@ impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
Self {
units: modeling.base_unit,
highlight_edges: modeling.highlight_edges.into(),
enable_ssao: modeling.enable_ssao.into(),
show_grid: Default::default(),
@ -476,26 +468,17 @@ impl ExecutorContext {
/// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
/// variables.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_with_default_client(units: UnitLength) -> Result<Self> {
pub async fn new_with_default_client() -> Result<Self> {
// Create the client.
let ctx = Self::new_with_client(
ExecutorSettings {
units,
..Default::default()
},
None,
None,
)
.await?;
let ctx = Self::new_with_client(Default::default(), None, None).await?;
Ok(ctx)
}
/// For executing unit tests.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_for_unit_test(units: UnitLength, engine_addr: Option<String>) -> Result<Self> {
pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
let ctx = ExecutorContext::new_with_client(
ExecutorSettings {
units,
highlight_edges: true,
enable_ssao: false,
show_grid: false,
@ -862,11 +845,6 @@ impl ExecutorContext {
Ok(())
}
/// Update the units for the executor.
pub(crate) fn update_units(&mut self, units: UnitLength) {
self.settings.units = units;
}
/// Get a snapshot of the current scene.
pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
// Zoom to fit.
@ -1008,11 +986,7 @@ mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::{
errors::{KclErrorDetails, Severity},
execution::memory::Stack,
ModuleId,
};
use crate::{errors::KclErrorDetails, execution::memory::Stack, ModuleId};
/// Convenience function to get a JSON value from memory and unwrap.
#[track_caller]
@ -1615,34 +1589,6 @@ const inInches = 2.0 * inch()"#;
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unit_suggest() {
let src = "foo = 42";
let program = crate::Program::parse_no_errs(src).unwrap();
let ctx = ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)),
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: ExecutorSettings {
units: UnitLength::Ft,
..Default::default()
},
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::new(&ctx);
ctx.run(&program, &mut exec_state).await.unwrap();
let errs = exec_state.errors();
assert_eq!(errs.len(), 1, "{errs:?}");
let warn = &errs[0];
assert_eq!(warn.severity, Severity::Warning);
assert_eq!(
warn.apply_suggestion(src).unwrap(),
"@settings(defaultLengthUnit = ft)\nfoo = 42"
)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_zero_param_fn() {
let ast = r#"const sigmaAllow = 35000 // psi
@ -1971,9 +1917,7 @@ let w = f() + f()
)
"#;
let ctx = crate::test_server::new_context(UnitLength::Mm, true, None)
.await
.unwrap();
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let old_program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
@ -2026,9 +1970,7 @@ let w = f() + f()
)
"#;
let mut ctx = crate::test_server::new_context(UnitLength::Mm, true, None)
.await
.unwrap();
let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
let old_program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
@ -2066,7 +2008,7 @@ let w = f() + f()
#[tokio::test(flavor = "multi_thread")]
async fn mock_after_not_mock() {
let ctx = ExecutorContext::new_with_default_client(UnitLength::Mm).await.unwrap();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let program = crate::Program::parse_no_errs("x = 2").unwrap();
let result = ctx.run_with_caching(program).await.unwrap();
assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);

View File

@ -82,7 +82,7 @@ impl ExecState {
pub fn new(exec_context: &super::ExecutorContext) -> Self {
ExecState {
global: GlobalState::new(&exec_context.settings),
mod_local: ModuleState::new(&exec_context.settings, None, ProgramMemory::new(), Default::default()),
mod_local: ModuleState::new(None, ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
}
}
@ -92,7 +92,7 @@ impl ExecState {
*self = ExecState {
global,
mod_local: ModuleState::new(&exec_context.settings, None, ProgramMemory::new(), Default::default()),
mod_local: ModuleState::new(None, ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
};
}
@ -289,12 +289,7 @@ impl GlobalState {
}
impl ModuleState {
pub(super) fn new(
exec_settings: &ExecutorSettings,
std_path: Option<String>,
memory: Arc<ProgramMemory>,
module_id: Option<ModuleId>,
) -> Self {
pub(super) fn new(std_path: Option<String>, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
ModuleState {
id_generator: IdGenerator::new(module_id),
stack: memory.new_stack(),
@ -303,14 +298,14 @@ impl ModuleState {
explicit_length_units: false,
std_path,
settings: MetaSettings {
default_length_units: exec_settings.units.into(),
default_length_units: Default::default(),
default_angle_units: Default::default(),
},
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct MetaSettings {

View File

@ -195,6 +195,10 @@ impl Program {
})
}
pub fn is_empty_or_only_settings(&self) -> bool {
self.ast.is_empty_or_only_settings()
}
pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
self.ast.lint_all()
}

View File

@ -812,56 +812,6 @@ impl Backend {
Ok(())
}
pub async fn update_units(
&self,
params: custom_notifications::UpdateUnitsParams,
) -> RpcResult<Option<custom_notifications::UpdateUnitsResponse>> {
{
let mut ctx = self.executor_ctx.write().await;
// Borrow the executor context mutably.
let Some(ref mut executor_ctx) = *ctx else {
self.client
.log_message(MessageType::ERROR, "no executor context set to update units for")
.await;
return Ok(None);
};
self.client
.log_message(MessageType::INFO, format!("update units: {:?}", params))
.await;
if executor_ctx.settings.units == params.units
&& !self.has_diagnostics(params.text_document.uri.as_ref()).await
{
// Return early the units are the same.
return Ok(None);
}
// Set the engine units.
executor_ctx.update_units(params.units);
}
// Lock is dropped here since nested.
// This is IMPORTANT.
let new_params = TextDocumentItem {
uri: params.text_document.uri.clone(),
text: std::mem::take(&mut params.text.to_string()),
version: Default::default(),
language_id: Default::default(),
};
// Force re-execution.
self.inner_on_change(new_params, true).await;
// Check if we have diagnostics.
// If we do we return early, since we failed in some way.
if self.has_diagnostics(params.text_document.uri.as_ref()).await {
return Ok(None);
}
Ok(Some(custom_notifications::UpdateUnitsResponse {}))
}
pub async fn update_can_execute(
&self,
params: custom_notifications::UpdateCanExecuteParams,

View File

@ -42,7 +42,6 @@ pub async fn kcl_lsp_server(execute: bool) -> Result<crate::lsp::kcl::Backend> {
can_execute: Arc::new(tokio::sync::RwLock::new(can_execute)),
is_initialized: Default::default(),
})
.custom_method("kcl/updateUnits", crate::lsp::kcl::Backend::update_units)
.custom_method("kcl/updateCanExecute", crate::lsp::kcl::Backend::update_can_execute)
.finish();

View File

@ -2324,80 +2324,6 @@ async fn kcl_test_kcl_lsp_on_change_update_memory() {
server.executor_ctx().await.clone().unwrap().close().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 10)]
async fn kcl_test_kcl_lsp_update_units() {
let server = kcl_lsp_server(true).await.unwrap();
let same_text = r#"fn cube = (pos, scale) => {
sg = startSketchOn(XY)
|> startProfileAt(pos, %)
|> line(end = [0, scale])
|> line(end = [scale, 0])
|> line(end = [0, -scale])
return sg
}
part001 = cube([0,0], 20)
|> close()
|> extrude(length = 20)"#
.to_string();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: same_text.clone(),
},
})
.await;
// Get the tokens.
let tokens = server.token_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(tokens.as_slice().len(), 123);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(ast.ast.body.len(), 2);
// Send change file.
server
.did_change(tower_lsp::lsp_types::DidChangeTextDocumentParams {
text_document: tower_lsp::lsp_types::VersionedTextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
version: 1,
},
content_changes: vec![tower_lsp::lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: same_text.clone(),
}],
})
.await;
let units = server.executor_ctx.read().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Update the units.
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::settings::types::UnitLength::M,
text: same_text.clone(),
})
.await
.unwrap();
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::M);
server.executor_ctx().await.clone().unwrap().close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_kcl_lsp_empty_file_execute_ok() {
let server = kcl_lsp_server(true).await.unwrap();
@ -2733,145 +2659,6 @@ async fn kcl_test_kcl_lsp_code_and_ast_unchanged_but_has_diagnostics_reexecute()
server.executor_ctx().await.clone().unwrap().close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_diagnostics_reexecute_on_unit_change() {
let server = kcl_lsp_server(true).await.unwrap();
let code = r#"part001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line(end = [20, 0])
|> line(end = [0, 20])
|> line(end = [-20, 0])
|> close()
|> extrude(length = 3.14)"#;
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: code.to_string(),
},
})
.await;
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert!(ast.ast != Node::<Program>::default());
// Assure we have no diagnostics.
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Add some fake diagnostics.
server.diagnostics_map.insert(
"file:///test.kcl".to_string(),
vec![tower_lsp::lsp_types::Diagnostic {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 0, character: 0 },
end: tower_lsp::lsp_types::Position { line: 0, character: 0 },
},
message: "fake diagnostic".to_string(),
severity: Some(tower_lsp::lsp_types::DiagnosticSeverity::ERROR),
code: None,
source: None,
related_information: None,
tags: None,
data: None,
code_description: None,
}],
);
// Assure we have one diagnostics.
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 1);
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Update the units to the _same_ units.
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert!(ast.ast != Node::<Program>::default());
// Assure we have no diagnostics.
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
server.executor_ctx().await.clone().unwrap().close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_kcl_lsp_code_and_ast_units_unchanged_but_has_memory_reexecute_on_unit_change() {
let server = kcl_lsp_server(true).await.unwrap();
let code = r#"part001 = startSketchOn(XY)
|> startProfileAt([-10, -10], %)
|> line(end = [20, 0])
|> line(end = [0, 20])
|> line(end = [-20, 0])
|> close()
|> extrude(length = 3.14)"#;
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: code.to_string(),
},
})
.await;
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert!(ast.ast != Node::<Program>::default());
// Assure we have no diagnostics.
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Update the units to the _same_ units.
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert!(ast.ast != Node::<Program>::default());
// Assure we have no diagnostics.
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
server.executor_ctx().await.clone().unwrap().close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_kcl_lsp_cant_execute_set() {
let server = kcl_lsp_server(true).await.unwrap();
@ -2903,23 +2690,6 @@ async fn kcl_test_kcl_lsp_cant_execute_set() {
// Assure we have no diagnostics.
assert_diagnostic_count(server.diagnostics_map.get("file:///test.kcl").as_deref(), 0);
// Update the units to the _same_ units.
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert!(ast.ast != Node::<Program>::default());
@ -2936,23 +2706,6 @@ async fn kcl_test_kcl_lsp_cant_execute_set() {
.unwrap();
assert_eq!(server.can_execute().await, false);
// Update the units to the _same_ units.
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
let units = server.executor_ctx().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
let mut default_hashed = Node::<Program>::default();
default_hashed.compute_digest();
@ -2970,23 +2723,6 @@ async fn kcl_test_kcl_lsp_cant_execute_set() {
.unwrap();
assert_eq!(server.can_execute().await, true);
// Update the units to the _same_ units.
let units = server.executor_ctx.read().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
server
.update_units(crate::lsp::kcl::custom_notifications::UpdateUnitsParams {
text_document: crate::lsp::kcl::custom_notifications::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
units: crate::settings::types::UnitLength::Mm,
text: code.to_string(),
})
.await
.unwrap();
let units = server.executor_ctx.read().await.clone().unwrap().settings.units;
assert_eq!(units, crate::settings::types::UnitLength::Mm);
// Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
assert!(ast.ast != Node::<Program>::default());

View File

@ -369,6 +369,26 @@ impl Node<Program> {
Ok(new_program)
}
/// Returns true if the given KCL is empty or only contains settings that
/// would be auto-generated.
pub fn is_empty_or_only_settings(&self) -> bool {
if !self.body.is_empty() {
return false;
}
if self.non_code_meta.start_nodes.iter().any(|node| node.is_comment()) {
return false;
}
for item in &self.inner_attrs {
if item.name() != Some(annotations::SETTINGS) {
return false;
}
}
true
}
}
impl Program {
@ -3564,6 +3584,37 @@ mod tests {
use super::*;
#[track_caller]
fn parse(code: &str) -> Node<Program> {
crate::parsing::top_level_parse(code).unwrap()
}
#[test]
fn test_empty_or_only_settings() {
// Empty is empty.
assert!(parse("").is_empty_or_only_settings());
// Whitespace is empty.
assert!(parse(" ").is_empty_or_only_settings());
// Settings are empty.
assert!(parse(r#"@settings(defaultLengthUnit = mm)"#).is_empty_or_only_settings());
// Only comments is not empty.
assert!(!parse("// comment").is_empty_or_only_settings());
// Any statement is not empty.
assert!(!parse("5").is_empty_or_only_settings());
// Any statement is not empty, even with settings.
let code = r#"@settings(defaultLengthUnit = mm)
5"#;
assert!(!parse(code).is_empty_or_only_settings());
// Non-settings attributes are not empty.
assert!(!parse("@foo").is_empty_or_only_settings());
}
// We have this as a test so we can ensure it never panics with an unwrap in the server.
#[test]
fn test_variable_kind_to_completion() {

View File

@ -155,13 +155,9 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
let ast = crate::Program::parse_no_errs(&input).unwrap();
// Run the program.
let exec_res = crate::test_server::execute_and_snapshot_ast(
ast,
crate::settings::types::UnitLength::Mm,
Some(test.input_dir.join(&test.entry_point)),
export_step,
)
.await;
let exec_res =
crate::test_server::execute_and_snapshot_ast(ast, Some(test.input_dir.join(&test.entry_point)), export_step)
.await;
match exec_res {
Ok((exec_state, env_ref, png, step)) => {
let fail_path = test.output_dir.join("execution_error.snap");

View File

@ -1,7 +1,7 @@
//! Run all the KCL samples in the `kcl_samples` directory.
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::{
fs,
panic::{catch_unwind, AssertUnwindSafe},
path::{Path, PathBuf},
};

View File

@ -165,7 +165,7 @@ async fn inner_fillet(
edge_id,
object_id: solid.id,
radius: LengthUnit(radius),
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
tolerance: LengthUnit(tolerance.unwrap_or_else(|| default_tolerance(&exec_state.length_unit().into()))),
cut_type: CutType::Fillet,
// We make this a none so that we can remove it in the future.
face_id: None,

View File

@ -159,7 +159,7 @@ async fn inner_loft(
section_ids: sketches.iter().map(|group| group.id).collect(),
base_curve_index,
bez_approximate_rational,
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
tolerance: LengthUnit(tolerance.unwrap_or_else(|| default_tolerance(&exec_state.length_unit().into()))),
v_degree,
}),
)

View File

@ -273,7 +273,9 @@ async fn inner_revolve(
target: sketch.id.into(),
axis,
origin,
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
tolerance: LengthUnit(
tolerance.unwrap_or_else(|| default_tolerance(&exec_state.length_unit().into())),
),
axis_is_2d: true,
}),
)
@ -287,7 +289,9 @@ async fn inner_revolve(
angle,
target: sketch.id.into(),
edge_id,
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
tolerance: LengthUnit(
tolerance.unwrap_or_else(|| default_tolerance(&exec_state.length_unit().into())),
),
}),
)
.await?;

View File

@ -191,7 +191,7 @@ async fn inner_sweep(
target: sketch.id.into(),
trajectory,
sectional: sectional.unwrap_or(false),
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
tolerance: LengthUnit(tolerance.unwrap_or_else(|| default_tolerance(&exec_state.length_unit().into()))),
}),
)
.await?;

View File

@ -6,7 +6,6 @@ use crate::{
engine::new_zoo_client,
errors::ExecErrorWithState,
execution::{EnvironmentRef, ExecState, ExecutorContext, ExecutorSettings},
settings::types::UnitLength,
ConnectionError, ExecError, KclError, KclErrorWithOutputs, Program,
};
@ -19,12 +18,8 @@ pub struct RequestBody {
/// Executes a kcl program and takes a snapshot of the result.
/// This returns the bytes of the snapshot.
pub async fn execute_and_snapshot(
code: &str,
units: UnitLength,
current_file: Option<PathBuf>,
) -> Result<image::DynamicImage, ExecError> {
let ctx = new_context(units, true, current_file).await?;
pub async fn execute_and_snapshot(code: &str, current_file: Option<PathBuf>) -> Result<image::DynamicImage, ExecError> {
let ctx = new_context(true, current_file).await?;
let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
let res = do_execute_and_snapshot(&ctx, program)
.await
@ -38,11 +33,10 @@ pub async fn execute_and_snapshot(
/// This returns the bytes of the snapshot.
pub async fn execute_and_snapshot_ast(
ast: Program,
units: UnitLength,
current_file: Option<PathBuf>,
with_export_step: bool,
) -> Result<(ExecState, EnvironmentRef, image::DynamicImage, Option<Vec<u8>>), ExecErrorWithState> {
let ctx = new_context(units, true, current_file).await?;
let ctx = new_context(true, current_file).await?;
let (exec_state, env, img) = do_execute_and_snapshot(&ctx, ast).await?;
let mut step = None;
if with_export_step {
@ -64,10 +58,9 @@ pub async fn execute_and_snapshot_ast(
pub async fn execute_and_snapshot_no_auth(
code: &str,
units: UnitLength,
current_file: Option<PathBuf>,
) -> Result<(image::DynamicImage, EnvironmentRef), ExecError> {
let ctx = new_context(units, false, current_file).await?;
let ctx = new_context(false, current_file).await?;
let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
let res = do_execute_and_snapshot(&ctx, program)
.await
@ -111,11 +104,7 @@ async fn do_execute_and_snapshot(
Ok((exec_state, result.0, img))
}
pub async fn new_context(
units: UnitLength,
with_auth: bool,
current_file: Option<PathBuf>,
) -> Result<ExecutorContext, ConnectionError> {
pub async fn new_context(with_auth: bool, current_file: Option<PathBuf>) -> Result<ExecutorContext, ConnectionError> {
let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None)
.map_err(ConnectionError::CouldNotMakeClient)?;
if !with_auth {
@ -126,7 +115,6 @@ pub async fn new_context(
}
let mut settings = ExecutorSettings {
units,
highlight_edges: true,
enable_ssao: false,
show_grid: false,
@ -145,7 +133,6 @@ pub async fn new_context(
pub async fn execute_and_export_step(
code: &str,
units: UnitLength,
current_file: Option<PathBuf>,
) -> Result<
(
@ -155,7 +142,7 @@ pub async fn execute_and_export_step(
),
ExecErrorWithState,
> {
let ctx = new_context(units, true, current_file).await?;
let ctx = new_context(true, current_file).await?;
let mut exec_state = ExecState::new(&ctx);
let program = Program::parse_no_errs(code)
.map_err(|err| ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone()))?;