Compare commits

...

4 Commits

Author SHA1 Message Date
ac605c2434 Fix yarn tsc in codemirror-lsp-client (#4568) 2024-11-25 15:12:17 -05:00
f6ecdfcb02 Bump kcmc, release kcl-lib (#4567) 2024-11-25 20:10:45 +00:00
28815eb2f1 KCL: Executor returns specific errors, not anyhow (#4566)
Some clients (e.g. the simulation tests) want to know why a KCL program failed. Was it because of a network error? Or because the client wasn't authorized properly? Or was it a KCL runtime type error?

It's difficult for clients to ask these questions, because the interpreter just returns Anyhow error, basically just a string.

Instead, we should return different variants of an Error enum for different kinds of errors.

This lets us render nice Cargo-style error messages that show the exact KCL line where an error occurred, instead of just printing off a source range. We have that in the CLI, but I'd like it for running normal KCL unit tests.

# Testing

This is a pure refactor, it doesn't change any behaviour, it just stops hiding the specific error types via anyhow. No testing needed. If it compiles, it works.
2024-11-25 15:06:23 -05:00
166fa71f7e Add nightly link in about section (#4548)
* Add nightly link in about sectoin

* Add nightly detection

* Lint

* Change APP_NAME to PACKAGE_NAME

* To be improved: working implementation on mac for click to download

* Revert "To be improved: working implementation on mac for click to download"

This reverts commit 7ced32a533.

* Nevermind, will process on the website
2024-11-25 14:20:50 -05:00
13 changed files with 83 additions and 31 deletions

View File

@ -18,7 +18,7 @@
"license": "MIT",
"private": false,
"dependencies": {
"@codemirror/autocomplete": "^6.16.3",
"@codemirror/autocomplete": "6.17.0",
"@codemirror/language": "^6.10.2",
"@codemirror/state": "^6.4.1",
"@lezer/highlight": "^1.2.0",

View File

@ -2,10 +2,10 @@
# yarn lockfile v1
"@codemirror/autocomplete@^6.16.3":
version "6.16.3"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.16.3.tgz#04d5a4e4e44ccae1ba525d47db53a5479bf46338"
integrity sha512-Vl/tIeRVVUCRDuOG48lttBasNQu8usGgXQawBXI7WJAiUDSFOfzflmEsZFZo48mAvAaa4FZ/4/yLLxFtdJaKYA==
"@codemirror/autocomplete@6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz#24ff5fc37fd91f6439df6f4ff9c8e910cde1b053"
integrity sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"

View File

@ -5,7 +5,8 @@ export COMMIT=$(git rev-parse --short HEAD)
# package.json
yarn files:set-version
echo "$(jq --arg name 'Zoo Modeling App (Nightly)' '.productName=$name' package.json --indent 2)" > package.json
PACKAGE=$(jq '.productName="Zoo Modeling App (Nightly)" | .name="zoo-modeling-app-nightly"' package.json --indent 2)
echo "$PACKAGE" > package.json
# electron-builder.yml
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml

View File

@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast'
import { APP_VERSION } from 'routes/Settings'
import { APP_VERSION, PACKAGE_NAME } from 'routes/Settings'
import { PATHS } from 'lib/paths'
import {
createAndOpenNewTutorialProject,
@ -264,6 +264,22 @@ export const AllSettingsFields = forwardRef(
, and start a discussion if you don't see it! Your feedback will
help us prioritize what to build next.
</p>
{PACKAGE_NAME.indexOf('-nightly') === -1 && (
<p className="max-w-2xl mt-6">
Want to experience the latest and (hopefully) greatest from our
main development branch?{' '}
<a
href="https://zoo.dev/modeling-app/download/nightly"
target="_blank"
rel="noopener noreferrer"
>
Click here to grab Zoo Modeling App (Nightly)
</a>
. It can be installed side-by-side with the stable version
you're running now. But careful there, a lot less testing is
involved in their release 🤖.
</p>
)}
</div>
</div>
</div>

View File

@ -26,6 +26,10 @@ export const APP_VERSION =
window.electron.packageJson.version
: 'main'
export const PACKAGE_NAME = isDesktop()
? window.electron.packageJson.name
: 'zoo-modeling-app'
export const Settings = () => {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()

View File

@ -1675,7 +1675,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.25"
version = "0.2.26"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1806,9 +1806,9 @@ dependencies = [
[[package]]
name = "kittycad-modeling-cmds"
version = "0.2.76"
version = "0.2.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2394fe2b28a1c6bd524dec1dbcd7e839c2782a6ecc743085e122cde77ee19cfa"
checksum = "3b77259b37acafa360d98af27431ac394bc8899eeed7037513832ddbee856811"
dependencies = [
"anyhow",
"chrono",

View File

@ -203,7 +203,7 @@ fn bad_gateway(msg: String) -> Response<Body> {
resp
}
fn kcl_err(err: anyhow::Error) -> Response<Body> {
fn kcl_err(err: impl std::fmt::Display) -> Response<Body> {
eprintln!("\tBad KCL");
bad_gateway(format!("{err}"))
}

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.25"
version = "0.2.26"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -4,6 +4,26 @@ use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::{ast::types::ModuleId, executor::SourceRange, lsp::IntoDiagnostic};
/// How did the KCL execution fail
#[derive(thiserror::Error, Debug)]
pub enum ExecError {
#[error("{0}")]
Kcl(#[from] crate::KclError),
#[error("Could not connect to engine: {0}")]
Connection(#[from] ConnectionError),
#[error("PNG snapshot could not be decoded: {0}")]
BadPng(String),
}
/// How did KCL client fail to connect to the engine
#[derive(thiserror::Error, Debug)]
pub enum ConnectionError {
#[error("Could not create a Zoo client: {0}")]
CouldNotMakeClient(anyhow::Error),
#[error("Could not establish connection to engine: {0}")]
Establishing(anyhow::Error),
}
#[derive(Error, Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq)]
#[ts(export)]
#[serde(tag = "kind", rename_all = "snake_case")]

View File

@ -34,7 +34,7 @@ use crate::{
fs::{FileManager, FileSystem},
settings::types::UnitLength,
std::{args::Arg, StdLib},
Program,
ExecError, Program,
};
/// State for executing a program.
@ -2179,12 +2179,16 @@ impl ExecutorContext {
&self,
program: &Program,
exec_state: &mut ExecState,
) -> Result<TakeSnapshot> {
) -> std::result::Result<TakeSnapshot, ExecError> {
self.execute_and_prepare(program, exec_state).await
}
/// Execute the program, return the interpreter and outputs.
pub async fn execute_and_prepare(&self, program: &Program, exec_state: &mut ExecState) -> Result<TakeSnapshot> {
pub async fn execute_and_prepare(
&self,
program: &Program,
exec_state: &mut ExecState,
) -> std::result::Result<TakeSnapshot, ExecError> {
self.run(program, exec_state).await?;
// Zoom to fit.
@ -2216,7 +2220,9 @@ impl ExecutorContext {
modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
} = resp
else {
anyhow::bail!("Unexpected response from engine: {:?}", resp);
return Err(ExecError::BadPng(format!(
"Instead of a TakeSnapshot response, the engine returned {resp:?}"
)));
};
Ok(contents)
}

View File

@ -42,7 +42,7 @@ pub use ast::modify::modify_ast_for_sketch;
pub use ast::types::{FormatOptions, ModuleId};
pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind};
pub use errors::KclError;
pub use errors::{ConnectionError, ExecError, KclError};
pub use executor::{ExecState, ExecutorContext, ExecutorSettings, SourceRange};
pub use lsp::copilot::Backend as CopilotLspBackend;
pub use lsp::kcl::Backend as KclLspBackend;

View File

@ -308,6 +308,10 @@ fn analyze_faces(exec_state: &mut ExecState, args: &Args, face_infos: Vec<Extrus
match face_info.cap {
ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
ExtrusionFaceCapType::Both => {
faces.end_cap_id = face_info.face_id;
faces.start_cap_id = face_info.face_id;
}
ExtrusionFaceCapType::None => {
if let Some(curve_id) = face_info.curve_id {
faces.sides.insert(curve_id, face_info.face_id);

View File

@ -3,7 +3,7 @@
use crate::{
executor::{new_zoo_client, ExecutorContext, ExecutorSettings, ProgramMemory},
settings::types::UnitLength,
ExecState, Program,
ConnectionError, ExecError, ExecState, Program,
};
#[derive(serde::Deserialize, serde::Serialize)]
@ -15,7 +15,7 @@ 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) -> anyhow::Result<image::DynamicImage> {
pub async fn execute_and_snapshot(code: &str, units: UnitLength) -> Result<image::DynamicImage, ExecError> {
let ctx = new_context(units, true).await?;
let program = Program::parse(code)?;
do_execute_and_snapshot(&ctx, program).await.map(|(_state, snap)| snap)
@ -26,14 +26,14 @@ pub async fn execute_and_snapshot(code: &str, units: UnitLength) -> anyhow::Resu
pub async fn execute_and_snapshot_ast(
ast: Program,
units: UnitLength,
) -> anyhow::Result<(ProgramMemory, image::DynamicImage)> {
) -> Result<(ProgramMemory, image::DynamicImage), ExecError> {
let ctx = new_context(units, true).await?;
do_execute_and_snapshot(&ctx, ast)
.await
.map(|(state, snap)| (state.memory, snap))
}
pub async fn execute_and_snapshot_no_auth(code: &str, units: UnitLength) -> anyhow::Result<image::DynamicImage> {
pub async fn execute_and_snapshot_no_auth(code: &str, units: UnitLength) -> Result<image::DynamicImage, ExecError> {
let ctx = new_context(units, false).await?;
let program = Program::parse(code)?;
do_execute_and_snapshot(&ctx, program).await.map(|(_state, snap)| snap)
@ -42,21 +42,21 @@ pub async fn execute_and_snapshot_no_auth(code: &str, units: UnitLength) -> anyh
async fn do_execute_and_snapshot(
ctx: &ExecutorContext,
program: Program,
) -> anyhow::Result<(crate::executor::ExecState, image::DynamicImage)> {
) -> Result<(crate::executor::ExecState, image::DynamicImage), ExecError> {
let mut exec_state = ExecState::default();
let snapshot = ctx.execute_and_prepare(&program, &mut exec_state).await?;
let snapshot_png_bytes = ctx.execute_and_prepare(&program, &mut exec_state).await?.contents.0;
// Create a temporary file to write the output to.
let output_file = std::env::temp_dir().join(format!("kcl_output_{}.png", uuid::Uuid::new_v4()));
// Save the snapshot locally, to that temporary file.
std::fs::write(&output_file, snapshot.contents.0)?;
// Decode the snapshot, return it.
let img = image::ImageReader::open(output_file).unwrap().decode()?;
let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
.with_guessed_format()
.map_err(|e| ExecError::BadPng(e.to_string()))
.and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))?;
Ok((exec_state, img))
}
async fn new_context(units: UnitLength, with_auth: bool) -> anyhow::Result<ExecutorContext> {
let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None)?;
async fn new_context(units: UnitLength, with_auth: bool) -> 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 {
// Use prod, don't override based on env vars.
// We do this so even in the engine repo, tests that need to run with
@ -74,6 +74,7 @@ async fn new_context(units: UnitLength, with_auth: bool) -> anyhow::Result<Execu
replay: None,
},
)
.await?;
.await
.map_err(ConnectionError::Establishing)?;
Ok(ctx)
}