Compare commits

...

11 Commits

Author SHA1 Message Date
a21c2c50b3 start of url scheme
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 14:38:03 -07:00
aa52407fda Cut release v0.19.3 (#2251) 2024-04-25 13:28:42 -07:00
e45be831d0 Pass the ?pool query param through to the backend. (#2246)
Pass the ?pool query param through to the backend.

This will slice off the ?pool= param and pass it to the WebSocket
request, which requests that the Zoo API use a particular pool of
engines. This isn't something any users of the zoo api require; but it's
needed for the internal engine Zoo development workflow. This may be
used in the future, but for now this'll be always enabled. Passing any
value in the production servers will result in a "no backend" error for
now.
2024-04-25 19:51:33 +00:00
005944f3a3 fix the updater (#2250)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 12:41:22 -07:00
755ef8ce7f download-wasm if there's no rust changes (#2234)
* download-wasm if there's no rust changes

* typo

* typo

* artifact stuff

* add needs

* permissions

* hmm

* more logic

* same for ubuntu
2024-04-26 05:37:32 +10:00
005d1f0ca7 Filter files and folders that start with a . (#2249) 2024-04-25 19:01:50 +00:00
e158f6f513 Better rust parsing of route uris for files (#2248)
* refactors

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

* updates

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

* fiex;

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

* fiex;

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

* fixes

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>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 18:55:11 +00:00
879d7ec4f4 Cut release v0.19.2 (#2247) 2024-04-25 14:38:25 -04:00
f6838b9b14 always ensure the dirs exist (#2245)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 17:07:24 +00:00
cb75c47631 fix env vars for lsp server to match other .env vars (#2243)
fix env vars for lsp

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 16:41:39 +00:00
9b95ec1083 fix relevant extensions (#2241)
* updates

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 08:36:45 -07:00
29 changed files with 583 additions and 98 deletions

View File

@ -16,8 +16,6 @@ jobs:
cache: 'yarn' cache: 'yarn'
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Setup Rust - name: Setup Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache wasm - name: Cache wasm

View File

@ -12,11 +12,31 @@ concurrency:
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
actions: read
jobs: jobs:
check-rust-changes:
runs-on: ubuntu-latest
outputs:
rust-changed: ${{ steps.filter.outputs.rust }}
steps:
- uses: actions/checkout@v4
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v2
with:
filters: |
rust:
- 'src/wasm-lib/**'
playwright-ubuntu: playwright-ubuntu:
timeout-minutes: 60 timeout-minutes: 60
runs-on: ubuntu-latest-8-cores runs-on: ubuntu-latest-8-cores
needs: check-rust-changes
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@ -28,13 +48,39 @@ jobs:
run: yarn run: yarn
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
- name: download wasm
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: actions/download-artifact@v2
continue-on-error: true
with:
name: wasm-bundle
path: src/wasm-lib/pkg
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust - name: Setup Rust
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust
if: steps.download-wasm.outcome == 'failure'
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache wasm - name: Cache wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Cache wasm
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: build wasm - name: build wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: build wasm
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm run: yarn build:wasm
- name: build web - name: build web
run: yarn build:local run: yarn build:local
@ -85,10 +131,16 @@ jobs:
name: playwright-report name: playwright-report
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
- uses: actions/upload-artifact@v4
if: github.ref == 'refs/heads/main'
with:
name: wasm-bundle
path: src/wasm-lib/pkg
playwright-macos: playwright-macos:
timeout-minutes: 60 timeout-minutes: 60
runs-on: macos-14 runs-on: macos-14
needs: check-rust-changes
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
@ -99,13 +151,39 @@ jobs:
run: yarn run: yarn
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
- name: download wasm
id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: actions/download-artifact@v4
with:
name: wasm-bundle
path: src/wasm-lib/pkg
continue-on-error: true
- name: copy wasm blob
if: needs.check-rust-changes.outputs.rust-changed == 'false'
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
continue-on-error: true
- name: Setup Rust - name: Setup Rust
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: dtolnay/rust-toolchain@stable
- name: Setup Rust
if: steps.download-wasm.outcome == 'failure'
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache wasm - name: Cache wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: Cache wasm
if: steps.download-wasm.outcome == 'failure'
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
workspaces: './src/wasm-lib' workspaces: './src/wasm-lib'
- name: build wasm - name: build wasm
if: needs.check-rust-changes.outputs.rust-changed == 'true'
run: yarn build:wasm
- name: build wasm
if: steps.download-wasm.outcome == 'failure'
run: yarn build:wasm run: yarn build:wasm
- name: build web - name: build web
run: yarn build:local run: yarn build:local
@ -122,7 +200,7 @@ jobs:
name: playwright-report name: playwright-report
path: playwright-report/ path: playwright-report/
retention-days: 30 retention-days: 30
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v4
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
with: with:
name: wasm-bundle name: wasm-bundle

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.19.1", "version": "0.19.3",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.16.0", "@codemirror/autocomplete": "^6.16.0",

25
src-tauri/Cargo.lock generated
View File

@ -751,6 +751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive",
] ]
[[package]] [[package]]
@ -763,6 +764,20 @@ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
"strsim 0.11.1", "strsim 0.11.1",
"unicase",
"unicode-width",
]
[[package]]
name = "clap_derive"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.60",
] ]
[[package]] [[package]]
@ -2403,7 +2418,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.52" version = "0.1.53"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx", "approx",
@ -2412,6 +2427,7 @@ dependencies = [
"base64 0.22.0", "base64 0.22.0",
"bson", "bson",
"chrono", "chrono",
"clap",
"dashmap", "dashmap",
"databake", "databake",
"derive-docs", "derive-docs",
@ -2471,6 +2487,7 @@ dependencies = [
"bigdecimal", "bigdecimal",
"bytes", "bytes",
"chrono", "chrono",
"clap",
"data-encoding", "data-encoding",
"format_serde_error", "format_serde_error",
"futures", "futures",
@ -5886,6 +5903,12 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

View File

@ -15,7 +15,7 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
kcl-lib = { version = "0.1.52", path = "../src/wasm-lib/kcl" } kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.0" kittycad = "0.3.0"
oauth2 = "4.4.2" oauth2 = "4.4.2"
serde_json = "1.0" serde_json = "1.0"

View File

@ -11,7 +11,7 @@ use std::{
use anyhow::Result; use anyhow::Result;
use kcl_lib::settings::types::{ use kcl_lib::settings::types::{
file::{FileEntry, Project, ProjectState}, file::{FileEntry, Project, ProjectRoute, ProjectState},
project::ProjectConfiguration, project::ProjectConfiguration,
Configuration, DEFAULT_PROJECT_KCL_FILE, Configuration, DEFAULT_PROJECT_KCL_FILE,
}; };
@ -52,14 +52,22 @@ async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result
Ok(()) Ok(())
} }
fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> { async fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let app_config_dir = app.path().app_config_dir()?; let app_config_dir = app.path().app_config_dir()?;
// Ensure this directory exists.
if !app_config_dir.exists() {
tokio::fs::create_dir_all(&app_config_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(app_config_dir.join(SETTINGS_FILE_NAME)) Ok(app_config_dir.join(SETTINGS_FILE_NAME))
} }
#[tauri::command] #[tauri::command]
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> { async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
let mut settings_path = get_app_settings_file_path(&app)?; let mut settings_path = get_app_settings_file_path(&app).await?;
let mut needs_migration = false; let mut needs_migration = false;
// Check if this file exists. // Check if this file exists.
@ -104,7 +112,7 @@ async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration,
#[tauri::command] #[tauri::command]
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> { async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
let settings_path = get_app_settings_file_path(&app)?; let settings_path = get_app_settings_file_path(&app).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?; let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes()) tokio::fs::write(settings_path, contents.as_bytes())
.await .await
@ -113,13 +121,19 @@ async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configura
Ok(()) Ok(())
} }
fn get_project_settings_file_path(app_settings: Configuration, project_name: &str) -> Result<PathBuf, InvokeError> { async fn get_project_settings_file_path(
Ok(app_settings app_settings: Configuration,
.settings project_name: &str,
.project ) -> Result<PathBuf, InvokeError> {
.directory let project_dir = app_settings.settings.project.directory.join(project_name);
.join(project_name)
.join(PROJECT_SETTINGS_FILE_NAME)) if !project_dir.exists() {
tokio::fs::create_dir_all(&project_dir)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(project_dir.join(PROJECT_SETTINGS_FILE_NAME))
} }
#[tauri::command] #[tauri::command]
@ -127,7 +141,7 @@ async fn read_project_settings_file(
app_settings: Configuration, app_settings: Configuration,
project_name: &str, project_name: &str,
) -> Result<ProjectConfiguration, InvokeError> { ) -> Result<ProjectConfiguration, InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name)?; let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
// Check if this file exists. // Check if this file exists.
if !settings_path.exists() { if !settings_path.exists() {
@ -149,7 +163,7 @@ async fn write_project_settings_file(
project_name: &str, project_name: &str,
configuration: ProjectConfiguration, configuration: ProjectConfiguration,
) -> Result<(), InvokeError> { ) -> Result<(), InvokeError> {
let settings_path = get_project_settings_file_path(app_settings, project_name)?; let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?; let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
tokio::fs::write(settings_path, contents.as_bytes()) tokio::fs::write(settings_path, contents.as_bytes())
.await .await
@ -195,6 +209,12 @@ async fn get_project_info(configuration: Configuration, project_path: &str) -> R
.map_err(InvokeError::from_anyhow) .map_err(InvokeError::from_anyhow)
} }
/// Parse the project route.
#[tauri::command]
async fn parse_project_route(configuration: Configuration, route: &str) -> Result<ProjectRoute, InvokeError> {
ProjectRoute::from_route(&configuration, route).map_err(InvokeError::from_anyhow)
}
#[tauri::command] #[tauri::command]
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> { async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf()) kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf())
@ -332,17 +352,6 @@ fn show_in_folder(path: &str) -> Result<(), InvokeError> {
fn main() -> Result<()> { fn main() -> Result<()> {
tauri::Builder::default() tauri::Builder::default()
.setup(|_app| {
#[cfg(debug_assertions)]
{
_app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
{
_app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
}
Ok(())
})
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
get_state, get_state,
set_state, set_state,
@ -351,6 +360,7 @@ fn main() -> Result<()> {
create_new_project_directory, create_new_project_directory,
list_projects, list_projects,
get_project_info, get_project_info,
parse_project_route,
get_user, get_user,
login, login,
read_dir_recursive, read_dir_recursive,
@ -362,6 +372,16 @@ fn main() -> Result<()> {
]) ])
.plugin(tauri_plugin_cli::init()) .plugin(tauri_plugin_cli::init())
.setup(|app| { .setup(|app| {
// Do update things.
#[cfg(debug_assertions)]
{
app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
{
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
}
let mut verbose = false; let mut verbose = false;
let mut source_path: Option<PathBuf> = None; let mut source_path: Option<PathBuf> = None;
match app.cli().matches() { match app.cli().matches() {
@ -409,6 +429,14 @@ fn main() -> Result<()> {
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> = let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
// Fix for "." path, which is the current directory.
let source_path = if source_path == Path::new(".") {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
} else {
source_path
};
// If the path is a directory, let's assume it is a project directory. // If the path is a directory, let's assume it is a project directory.
if source_path.is_dir() { if source_path.is_dir() {
// Load the details about the project from the path. // Load the details about the project from the path.
@ -467,6 +495,13 @@ fn main() -> Result<()> {
Ok(()) Ok(())
}) })
.register_uri_scheme_protocol("zoo-modeling-app", |_app, request| {
let path = request.uri().path();
dbg!(path);
println!("Requesting path: {}", path);
tauri::http::Response::builder().status(200).body(b"{}").unwrap()
})
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())

View File

@ -71,5 +71,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.19.1" "version": "0.19.3"
} }

View File

@ -59,7 +59,6 @@ const router = createBrowserRouter([
const appState = await getState() const appState = await getState()
if (appState) { if (appState) {
console.log('appState', appState)
// Reset the state. // Reset the state.
// We do this so that we load the initial state from the cli but everything // We do this so that we load the initial state from the cli but everything
// else we can ignore. // else we can ignore.

View File

@ -3,7 +3,7 @@ import type * as LSP from 'vscode-languageserver-protocol'
import React, { createContext, useMemo, useEffect, useContext } from 'react' import React, { createContext, useMemo, useEffect, useContext } from 'react'
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec' import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Client from '../editor/plugins/lsp/client' import Client from '../editor/plugins/lsp/client'
import { DEV, TEST } from 'env' import { TEST, VITE_KC_API_BASE_URL } from 'env'
import kclLanguage from 'editor/plugins/lsp/kcl/language' import kclLanguage from 'editor/plugins/lsp/kcl/language'
import { copilotPlugin } from 'editor/plugins/lsp/copilot' import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { useStore } from 'useStore' import { useStore } from 'useStore'
@ -103,7 +103,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
baseUnit: defaultUnit.current, baseUnit: defaultUnit.current,
devMode: DEV, apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({
worker: LspWorker.Kcl, worker: LspWorker.Kcl,
@ -177,7 +177,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: CopilotWorkerOptions = { const initEvent: CopilotWorkerOptions = {
wasmUrl: wasmUrl(), wasmUrl: wasmUrl(),
token: token, token: token,
devMode: DEV, apiBaseUrl: VITE_KC_API_BASE_URL,
} }
lspWorker.postMessage({ lspWorker.postMessage({
worker: LspWorker.Copilot, worker: LspWorker.Copilot,

View File

@ -56,6 +56,7 @@ import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror' import { EditorSelection } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
@ -84,7 +85,12 @@ export const ModelingMachineProvider = ({
} = useSettingsAuthContext() } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
useSetupEngineManager(streamRef, token, { useSetupEngineManager(streamRef, token, {
pool: pool,
theme: theme.current, theme: theme.current,
highlightEdges: highlightEdges.current, highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current, enableSSAO: enableSSAO.current,

View File

@ -8,13 +8,13 @@ export interface KclWorkerOptions {
wasmUrl: string wasmUrl: string
token: string token: string
baseUnit: UnitLength baseUnit: UnitLength
devMode: boolean apiBaseUrl: string
} }
export interface CopilotWorkerOptions { export interface CopilotWorkerOptions {
wasmUrl: string wasmUrl: string
token: string token: string
devMode: boolean apiBaseUrl: string
} }
export enum LspWorkerEventType { export enum LspWorkerEventType {

View File

@ -28,11 +28,11 @@ const initialise = async (wasmUrl: string) => {
export async function copilotLspRun( export async function copilotLspRun(
config: ServerConfig, config: ServerConfig,
token: string, token: string,
devMode: boolean = false baseUrl: string
) { ) {
try { try {
console.log('starting copilot lsp') console.log('starting copilot lsp')
await copilot_lsp_run(config, token, devMode) await copilot_lsp_run(config, token, baseUrl)
} catch (e: any) { } catch (e: any) {
console.log('copilot lsp failed', e) console.log('copilot lsp failed', e)
// We can't restart here because a moved value, we should do this another way. // We can't restart here because a moved value, we should do this another way.
@ -44,11 +44,11 @@ export async function kclLspRun(
engineCommandManager: EngineCommandManager | null, engineCommandManager: EngineCommandManager | null,
token: string, token: string,
baseUnit: string, baseUnit: string,
devMode: boolean = false baseUrl: string
) { ) {
try { try {
console.log('start kcl lsp') console.log('start kcl lsp')
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, devMode) await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
} catch (e: any) { } catch (e: any) {
console.log('kcl lsp failed', e) console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way. // We can't restart here because a moved value, we should do this another way.
@ -80,12 +80,12 @@ onmessage = function (event) {
null, null,
kclData.token, kclData.token,
kclData.baseUnit, kclData.baseUnit,
kclData.devMode kclData.apiBaseUrl
) )
break break
case LspWorker.Copilot: case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions let copilotData = eventData as CopilotWorkerOptions
copilotLspRun(config, copilotData.token, copilotData.devMode) copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
break break
} }
}) })

View File

@ -9,10 +9,12 @@ export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,
token?: string, token?: string,
settings = { settings = {
pool: null,
theme: Themes.System, theme: Themes.System,
highlightEdges: true, highlightEdges: true,
enableSSAO: true, enableSSAO: true,
} as { } as {
pool: string | null
theme: Themes theme: Themes
highlightEdges: boolean highlightEdges: boolean
enableSSAO: boolean enableSSAO: boolean
@ -35,6 +37,12 @@ export function useSetupEngineManager(
const hasSetNonZeroDimensions = useRef<boolean>(false) const hasSetNonZeroDimensions = useRef<boolean>(false)
if (settings.pool) {
// override the pool param (?pool=) to request a specific engine instance
// from a particular pool.
engineCommandManager.pool = settings.pool
}
useLayoutEffect(() => { useLayoutEffect(() => {
// Load the engine command manager once with the initial width and height, // Load the engine command manager once with the initial width and height,
// then we do not want to reload it. // then we do not want to reload it.

View File

@ -888,6 +888,7 @@ export class EngineCommandManager {
sceneCommandArtifacts: ArtifactMap = {} sceneCommandArtifacts: ArtifactMap = {}
outSequence = 1 outSequence = 1
inSequence = 1 inSequence = 1
pool?: string
engineConnection?: EngineConnection engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes | null = null defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = [] commandLogs: CommandLog[] = []
@ -914,8 +915,9 @@ export class EngineCommandManager {
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] = callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
[] []
constructor() { constructor(pool?: string) {
this.engineConnection = undefined this.engineConnection = undefined
this.pool = pool
} }
private _camControlsCameraChange = () => {} private _camControlsCameraChange = () => {}
@ -972,7 +974,8 @@ export class EngineCommandManager {
} }
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : '' const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}` const pool = this.pool == undefined ? '' : `&pool=${this.pool}`
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}`
this.engineConnection = new EngineConnection({ this.engineConnection = new EngineConnection({
engineCommandManager: this, engineCommandManager: this,
url, url,

View File

@ -14,6 +14,7 @@ import init, {
parse_app_settings, parse_app_settings,
parse_project_settings, parse_project_settings,
default_project_settings, default_project_settings,
parse_project_route,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors' import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError' import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -31,6 +32,7 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } from 'env' import { TEST } from 'env'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration' import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration' import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
export type { Program } from '../wasm-lib/kcl/bindings/Program' export type { Program } from '../wasm-lib/kcl/bindings/Program'
export type { Value } from '../wasm-lib/kcl/bindings/Value' export type { Value } from '../wasm-lib/kcl/bindings/Value'
@ -389,3 +391,18 @@ export function parseProjectSettings(toml: string): ProjectConfiguration {
throw new Error(`Error parsing project settings: ${e}`) throw new Error(`Error parsing project settings: ${e}`)
} }
} }
export function parseProjectRoute(
configuration: Configuration,
route_str: string
): ProjectRoute {
try {
const route: ProjectRoute = parse_project_route(
JSON.stringify(configuration),
route_str
)
return route
} catch (e: any) {
throw new Error(`Error parsing project route: ${e}`)
}
}

View File

@ -49,6 +49,11 @@ export class CoreDumpManager {
return APP_VERSION return APP_VERSION
} }
// Get the backend pool we've requested.
pool(): string {
return this.engineCommandManager.pool || ''
}
// Get the os information. // Get the os information.
getOsInfo(): Promise<string> { getOsInfo(): Promise<string> {
if (this.isTauri()) { if (this.isTauri()) {

View File

@ -1,7 +1,11 @@
import { sep } from '@tauri-apps/api/path'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants' import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
import { parseProjectRoute, readAppSettingsFile } from './tauri'
import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm'
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
const prependRoutes = const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => { (routesObject: Record<string, string>) => (prepend: string) => {
@ -25,28 +29,23 @@ export const paths = {
} as const } as const
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}` export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
export function getProjectMetaByRouteId(id?: string, defaultDir = '') { export async function getProjectMetaByRouteId(
id?: string,
configuration?: Configuration
): Promise<ProjectRoute | undefined> {
if (!id) return undefined if (!id) return undefined
const s = isTauri() ? sep() : '/'
const decodedId = decodeURIComponent(id).replace(/\/$/, '') // remove trailing slash const inTauri = isTauri()
const projectAndFile =
defaultDir === '/'
? decodedId.replace(defaultDir, '')
: decodedId.replace(defaultDir + s, '')
const filePathParts = projectAndFile.split(s)
const projectName = filePathParts[0]
const projectPath =
(defaultDir === '/' ? defaultDir : defaultDir + s) + projectName
const lastPathPart = filePathParts[filePathParts.length - 1]
const currentFileName =
lastPathPart === projectName ? undefined : lastPathPart
const currentFilePath = lastPathPart === projectName ? undefined : decodedId
return { if (!configuration) {
projectName, configuration = inTauri
projectPath, ? await readAppSettingsFile()
currentFileName, : readLocalStorageAppSettingsFile()
currentFilePath,
} }
const route = inTauri
? await parseProjectRoute(configuration, id)
: parseProjectRouteWasm(configuration, id)
return route
} }

View File

@ -28,16 +28,18 @@ export const settingsLoader: LoaderFunction = async ({
}): Promise< }): Promise<
ReturnType<typeof createSettings> | ReturnType<typeof redirect> ReturnType<typeof createSettings> | ReturnType<typeof redirect>
> => { > => {
let { settings } = await loadAndValidateSettings() let { settings, configuration } = await loadAndValidateSettings()
// I don't love that we have to read the settings again here, // I don't love that we have to read the settings again here,
// but we need to get the project path to load the project settings // but we need to get the project path to load the project settings
if (params.id) { if (params.id) {
const defaultDir = settings.app.projectDirectory.current || '' const projectPathData = await getProjectMetaByRouteId(
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir) params.id,
configuration
)
if (projectPathData) { if (projectPathData) {
const { projectName } = projectPathData const { project_name } = projectPathData
const { settings: s } = await loadAndValidateSettings(projectName) const { settings: s } = await loadAndValidateSettings(project_name)
settings = s settings = s
} }
} }
@ -71,17 +73,19 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
export const fileLoader: LoaderFunction = async ({ export const fileLoader: LoaderFunction = async ({
params, params,
}): Promise<FileLoaderData | Response> => { }): Promise<FileLoaderData | Response> => {
let { settings } = await loadAndValidateSettings() let { configuration } = await loadAndValidateSettings()
const defaultDir = settings.app.projectDirectory.current || '/' const projectPathData = await getProjectMetaByRouteId(
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir) params.id,
configuration
)
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH) const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
if (!isBrowserProject && projectPathData) { if (!isBrowserProject && projectPathData) {
const { projectName, projectPath, currentFileName, currentFilePath } = const { project_name, project_path, current_file_name, current_file_path } =
projectPathData projectPathData
if (!currentFileName || !currentFilePath) { if (!current_file_name || !current_file_path || !project_name) {
return redirect( return redirect(
`${paths.FILE}/${encodeURIComponent( `${paths.FILE}/${encodeURIComponent(
`${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}` `${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
@ -91,33 +95,33 @@ export const fileLoader: LoaderFunction = async ({
// TODO: PROJECT_ENTRYPOINT is hardcoded // TODO: PROJECT_ENTRYPOINT is hardcoded
// until we support setting a project's entrypoint file // until we support setting a project's entrypoint file
const code = await readTextFile(currentFilePath) const code = await readTextFile(current_file_path)
// Update both the state and the editor's code. // Update both the state and the editor's code.
// We explicitly do not write to the file here since we are loading from // We explicitly do not write to the file here since we are loading from
// the file system and not the editor. // the file system and not the editor.
codeManager.updateCurrentFilePath(currentFilePath) codeManager.updateCurrentFilePath(current_file_path)
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
kclManager.executeCode(true) kclManager.executeCode(true)
// Set the file system manager to the project path // Set the file system manager to the project path
// So that WASM gets an updated path for operations // So that WASM gets an updated path for operations
fileSystemManager.dir = projectPath fileSystemManager.dir = project_path
const projectData: IndexLoaderData = { const projectData: IndexLoaderData = {
code, code,
project: isTauri() project: isTauri()
? await getProjectInfo(projectPath) ? await getProjectInfo(project_path, configuration)
: { : {
name: projectName, name: project_name,
path: projectPath, path: project_path,
children: [], children: [],
kcl_file_count: 0, kcl_file_count: 0,
directory_count: 0, directory_count: 0,
}, },
file: { file: {
name: currentFileName, name: current_file_name,
path: currentFilePath, path: current_file_path,
children: [], children: [],
}, },
} }

View File

@ -101,7 +101,7 @@ function localStorageProjectSettingsPath() {
return '/' + BROWSER_PROJECT_NAME + '/project.toml' return '/' + BROWSER_PROJECT_NAME + '/project.toml'
} }
function readLocalStorageAppSettingsFile(): Configuration { export function readLocalStorageAppSettingsFile(): Configuration {
// TODO: Remove backwards compatibility after a few releases. // TODO: Remove backwards compatibility after a few releases.
let stored = let stored =
localStorage.getItem(localStorageAppSettingsPath()) ?? localStorage.getItem(localStorageAppSettingsPath()) ??

View File

@ -7,6 +7,7 @@ import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration
import { Project } from 'wasm-lib/kcl/bindings/Project' import { Project } from 'wasm-lib/kcl/bindings/Project'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry' import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState' import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
// Get the app state from tauri. // Get the app state from tauri.
export async function getState(): Promise<ProjectState | undefined> { export async function getState(): Promise<ProjectState | undefined> {
@ -80,6 +81,16 @@ export async function login(host: string): Promise<string> {
return await invoke('login', { host }) return await invoke('login', { host })
} }
export async function parseProjectRoute(
configuration: Configuration,
route: string
): Promise<ProjectRoute> {
return await invoke<ProjectRoute>('parse_project_route', {
configuration,
route,
})
}
export async function getUser( export async function getUser(
token: string | undefined, token: string | undefined,
host: string host: string

View File

@ -1895,7 +1895,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.1.52" version = "0.1.53"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1974,6 +1974,7 @@ dependencies = [
"bigdecimal", "bigdecimal",
"bytes", "bytes",
"chrono", "chrono",
"clap",
"data-encoding", "data-encoding",
"format_serde_error", "format_serde_error",
"futures", "futures",
@ -4726,6 +4727,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",
"clap",
"console_error_panic_hook", "console_error_panic_hook",
"futures", "futures",
"gloo-utils", "gloo-utils",

View File

@ -11,6 +11,7 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] } bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
clap = "4.5.4"
gloo-utils = "0.2.0" gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" } kcl-lib = { path = "kcl" }
kittycad = { workspace = true } kittycad = { workspace = true }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.1.52" version = "0.1.53"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
@ -16,7 +16,7 @@ async-recursion = "1.1.0"
async-trait = "0.1.80" async-trait = "0.1.80"
base64 = "0.22.0" base64 = "0.22.0"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.4", features = ["cargo", "derive", "env", "unicode"], optional = true } clap = { version = "4.5.4", default-features = false, optional = true }
dashmap = "5.5.3" dashmap = "5.5.3"
databake = { version = "0.1.7", features = ["derive"] } databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.17", path = "../derive-docs" } derive-docs = { version = "0.1.17", path = "../derive-docs" }
@ -24,7 +24,7 @@ form_urlencoded = "1.2.1"
futures = { version = "0.3.30" } futures = { version = "0.3.30" }
git_rev = "0.1.0" git_rev = "0.1.0"
gltf-json = "1.4.0" gltf-json = "1.4.0"
kittycad = { workspace = true } kittycad = { workspace = true, features = ["clap"] }
kittycad-execution-plan-macros = { workspace = true } kittycad-execution-plan-macros = { workspace = true }
kittycad-execution-plan-traits = { workspace = true } kittycad-execution-plan-traits = { workspace = true }
lazy_static = "1.4.0" lazy_static = "1.4.0"
@ -61,7 +61,7 @@ tokio-tungstenite = { version = "0.21.0", features = ["rustls-tls-native-roots"]
tower-lsp = { version = "0.20.0", features = ["proposed"] } tower-lsp = { version = "0.20.0", features = ["proposed"] }
[features] [features]
default = ["engine"] default = ["cli", "engine"]
cli = ["dep:clap"] cli = ["dep:clap"]
engine = [] engine = []

View File

@ -33,6 +33,10 @@ impl CoreDump for CoreDumper {
Ok(env!("CARGO_PKG_VERSION").to_string()) Ok(env!("CARGO_PKG_VERSION").to_string())
} }
fn pool(&self) -> Result<String> {
Ok("".to_owned())
}
async fn os(&self) -> Result<crate::coredump::OsInfo> { async fn os(&self) -> Result<crate::coredump::OsInfo> {
Ok(crate::coredump::OsInfo { Ok(crate::coredump::OsInfo {
platform: Some(std::env::consts::OS.to_string()), platform: Some(std::env::consts::OS.to_string()),

View File

@ -19,6 +19,8 @@ pub trait CoreDump: Clone {
fn version(&self) -> Result<String>; fn version(&self) -> Result<String>;
fn pool(&self) -> Result<String>;
async fn os(&self) -> Result<OsInfo>; async fn os(&self) -> Result<OsInfo>;
fn is_tauri(&self) -> Result<bool>; fn is_tauri(&self) -> Result<bool>;
@ -71,6 +73,7 @@ pub trait CoreDump: Clone {
os, os,
webrtc_stats, webrtc_stats,
github_issue_url: None, github_issue_url: None,
pool: self.pool()?,
}; };
app_info.set_github_issue_url(&screenshot_url)?; app_info.set_github_issue_url(&screenshot_url)?;
@ -103,6 +106,9 @@ pub struct AppInfo {
/// This gets prepoulated with all the core dump info. /// This gets prepoulated with all the core dump info.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub github_issue_url: Option<String>, pub github_issue_url: Option<String>,
/// Engine pool the client is connected to.
pub pool: String,
} }
impl AppInfo { impl AppInfo {

View File

@ -16,6 +16,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = baseApiUrl, catch)] #[wasm_bindgen(method, js_name = baseApiUrl, catch)]
fn baseApiUrl(this: &CoreDumpManager) -> Result<String, js_sys::Error>; fn baseApiUrl(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
#[wasm_bindgen(method, js_name = pool, catch)]
fn pool(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
#[wasm_bindgen(method, js_name = version, catch)] #[wasm_bindgen(method, js_name = version, catch)]
fn version(this: &CoreDumpManager) -> Result<String, js_sys::Error>; fn version(this: &CoreDumpManager) -> Result<String, js_sys::Error>;
@ -66,6 +69,12 @@ impl CoreDump for CoreDumper {
.map_err(|e| anyhow::anyhow!("Failed to get response from version: {:?}", e)) .map_err(|e| anyhow::anyhow!("Failed to get response from version: {:?}", e))
} }
fn pool(&self) -> Result<String> {
self.manager
.pool()
.map_err(|e| anyhow::anyhow!("Failed to get response from pool: {:?}", e))
}
async fn os(&self) -> Result<crate::coredump::OsInfo> { async fn os(&self) -> Result<crate::coredump::OsInfo> {
let promise = self let promise = self
.manager .manager

View File

@ -5,6 +5,8 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::Configuration;
/// State management for the application. /// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)] #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)] #[ts(export)]
@ -14,6 +16,100 @@ pub struct ProjectState {
pub current_file: Option<String>, pub current_file: Option<String>,
} }
/// Project route information.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectRoute {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_name: Option<String>,
pub project_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_file_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_file_path: Option<String>,
}
impl ProjectRoute {
/// Get the project state from the url in the route.
pub fn from_route(configuration: &Configuration, route: &str) -> Result<Self> {
let path = std::path::Path::new(route);
// Check if the default project path is in the route.
let (project_path, project_name) = if path.starts_with(&configuration.settings.project.directory)
&& configuration.settings.project.directory != std::path::PathBuf::default()
{
// Get the project name.
if let Some(project_name) = path
.strip_prefix(&configuration.settings.project.directory)
.unwrap()
.iter()
.next()
{
(
configuration
.settings
.project
.directory
.join(project_name)
.display()
.to_string(),
Some(project_name.to_string_lossy().to_string()),
)
} else {
(configuration.settings.project.directory.display().to_string(), None)
}
} else {
// Assume the project path is the parent directory of the file.
let project_dir = if path.display().to_string().ends_with(".kcl") {
path.parent()
.ok_or_else(|| anyhow::anyhow!("Parent directory not found: {}", path.display()))?
} else {
path
};
if project_dir == std::path::Path::new("/") {
(
path.display().to_string(),
Some(
path.file_name()
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
.to_string_lossy()
.to_string(),
),
)
} else if let Some(project_name) = project_dir.file_name() {
(
project_dir.display().to_string(),
Some(project_name.to_string_lossy().to_string()),
)
} else {
(project_dir.display().to_string(), None)
}
};
let (current_file_name, current_file_path) = if path.display().to_string() == project_path {
(None, None)
} else {
(
Some(
path.file_name()
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
.to_string_lossy()
.to_string(),
),
Some(path.display().to_string()),
)
};
Ok(Self {
project_name,
project_path,
current_file_name,
current_file_path,
})
}
}
/// Information about project. /// Information about project.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)] #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)] #[ts(export)]
@ -233,3 +329,141 @@ impl From<std::fs::Metadata> for FileMetadata {
} }
} }
} }
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
#[test]
fn test_project_route_from_route_std_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("assembly".to_string()),
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some(
"/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl".to_string()
),
}
);
}
#[test]
fn test_project_route_from_route_std_path_dir() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("assembly".to_string()),
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
current_file_name: None,
current_file_path: None,
}
);
}
#[test]
fn test_project_route_from_route_std_path_dir_empty() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: None,
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects".to_string(),
current_file_name: None,
current_file_path: None,
}
);
}
#[test]
fn test_project_route_from_route_outside_std_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/kittycad/modeling-app/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("modeling-app".to_string()),
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some("/Users/macinatormax/kittycad/modeling-app/main.kcl".to_string()),
}
);
}
#[test]
fn test_project_route_from_route_outside_std_path_dir() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory =
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
let route = "/Users/macinatormax/kittycad/modeling-app";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("modeling-app".to_string()),
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
current_file_name: None,
current_file_path: None,
}
);
}
#[test]
fn test_project_route_from_route_browser() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory = std::path::PathBuf::default();
let route = "/browser/main.kcl";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("browser".to_string()),
project_path: "/browser".to_string(),
current_file_name: Some("main.kcl".to_string()),
current_file_path: Some("/browser/main.kcl".to_string()),
}
);
}
#[test]
fn test_project_route_from_route_browser_no_path() {
let mut configuration = crate::settings::types::Configuration::default();
configuration.settings.project.directory = std::path::PathBuf::default();
let route = "/browser";
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
assert_eq!(
state,
super::ProjectRoute {
project_name: Some("browser".to_string()),
project_path: "/browser".to_string(),
current_file_name: None,
current_file_path: None,
}
);
}
}

View File

@ -3,9 +3,23 @@
use std::path::Path; use std::path::Path;
use anyhow::Result; use anyhow::Result;
use clap::ValueEnum;
use crate::settings::types::file::FileEntry; use crate::settings::types::file::FileEntry;
lazy_static::lazy_static! {
static ref RELEVANT_EXTENSIONS: Vec<String> = {
let mut relevant_extensions = vec!["kcl".to_string(), "stp".to_string(), "glb".to_string()];
let named_extensions = kittycad::types::FileImportFormat::value_variants()
.iter()
.map(|x| format!("{}", x))
.collect::<Vec<String>>();
// Add all the default import formats.
relevant_extensions.extend_from_slice(&named_extensions);
relevant_extensions
};
}
/// Walk a directory recursively and return a list of all files. /// Walk a directory recursively and return a list of all files.
#[async_recursion::async_recursion] #[async_recursion::async_recursion]
pub async fn walk_dir<P: AsRef<Path> + Send>(dir: P) -> Result<FileEntry> { pub async fn walk_dir<P: AsRef<Path> + Send>(dir: P) -> Result<FileEntry> {
@ -24,9 +38,17 @@ pub async fn walk_dir<P: AsRef<Path> + Send>(dir: P) -> Result<FileEntry> {
let mut entries = tokio::fs::read_dir(&dir.as_ref()).await?; let mut entries = tokio::fs::read_dir(&dir.as_ref()).await?;
while let Some(e) = entries.next_entry().await? { while let Some(e) = entries.next_entry().await? {
// ignore hidden files and directories (starting with a dot)
if e.file_name().to_string_lossy().starts_with('.') {
continue;
}
if e.file_type().await?.is_dir() { if e.file_type().await?.is_dir() {
children.push(walk_dir(&e.path()).await?); children.push(walk_dir(&e.path()).await?);
} else { } else {
if !is_relevant_file(&e.path())? {
continue;
}
children.push(FileEntry { children.push(FileEntry {
name: e.file_name().to_string_lossy().to_string(), name: e.file_name().to_string_lossy().to_string(),
path: e.path().display().to_string(), path: e.path().display().to_string(),
@ -40,3 +62,12 @@ pub async fn walk_dir<P: AsRef<Path> + Send>(dir: P) -> Result<FileEntry> {
Ok(entry) Ok(entry)
} }
/// Check if a file is relevant for the application.
fn is_relevant_file<P: AsRef<Path>>(path: P) -> Result<bool> {
if let Some(ext) = path.as_ref().extension() {
Ok(RELEVANT_EXTENSIONS.contains(&ext.to_string_lossy().to_string()))
} else {
Ok(false)
}
}

View File

@ -198,7 +198,7 @@ pub async fn kcl_lsp_run(
engine_manager: Option<kcl_lib::engine::conn_wasm::EngineCommandManager>, engine_manager: Option<kcl_lib::engine::conn_wasm::EngineCommandManager>,
units: &str, units: &str,
token: String, token: String,
is_dev: bool, baseurl: String,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
@ -216,9 +216,7 @@ pub async fn kcl_lsp_run(
let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap(); let token_types = kcl_lib::token::TokenType::all_semantic_token_types().unwrap();
let mut zoo_client = kittycad::Client::new(token); let mut zoo_client = kittycad::Client::new(token);
if is_dev { zoo_client.set_base_url(baseurl.as_str());
zoo_client.set_base_url("https://api.dev.zoo.dev");
}
let file_manager = Arc::new(kcl_lib::fs::FileManager::new(fs)); let file_manager = Arc::new(kcl_lib::fs::FileManager::new(fs));
@ -313,7 +311,7 @@ pub async fn kcl_lsp_run(
// NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically // NOTE: input needs to be an AsyncIterator<Uint8Array, never, void> specifically
#[wasm_bindgen] #[wasm_bindgen]
pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool) -> Result<(), JsValue> { pub async fn copilot_lsp_run(config: ServerConfig, token: String, baseurl: String) -> Result<(), JsValue> {
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
let ServerConfig { let ServerConfig {
@ -323,9 +321,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool)
} = config; } = config;
let mut zoo_client = kittycad::Client::new(token); let mut zoo_client = kittycad::Client::new(token);
if is_dev { zoo_client.set_base_url(baseurl.as_str());
zoo_client.set_base_url("https://api.dev.zoo.dev");
}
let file_manager = Arc::new(kcl_lib::fs::FileManager::new(fs)); let file_manager = Arc::new(kcl_lib::fs::FileManager::new(fs));
@ -511,3 +507,19 @@ pub fn parse_project_settings(toml_str: &str) -> Result<JsValue, String> {
// gloo-serialize crate instead. // gloo-serialize crate instead.
JsValue::from_serde(&settings).map_err(|e| e.to_string()) JsValue::from_serde(&settings).map_err(|e| e.to_string())
} }
/// Parse the project route.
#[wasm_bindgen]
pub fn parse_project_route(configuration: &str, route: &str) -> Result<JsValue, String> {
console_error_panic_hook::set_once();
let configuration: kcl_lib::settings::types::Configuration =
serde_json::from_str(configuration).map_err(|e| e.to_string())?;
let route =
kcl_lib::settings::types::file::ProjectRoute::from_route(&configuration, route).map_err(|e| e.to_string())?;
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
// gloo-serialize crate instead.
JsValue::from_serde(&route).map_err(|e| e.to_string())
}