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'
- name: Install dependencies
run: yarn
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm

View File

@ -12,11 +12,31 @@ concurrency:
permissions:
contents: write
pull-requests: write
actions: read
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:
timeout-minutes: 60
runs-on: ubuntu-latest-8-cores
needs: check-rust-changes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -28,13 +48,39 @@ jobs:
run: yarn
- name: Install Playwright Browsers
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
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
- 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
with:
workspaces: './src/wasm-lib'
- 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
- name: build web
run: yarn build:local
@ -85,10 +131,16 @@ jobs:
name: playwright-report
path: playwright-report/
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:
timeout-minutes: 60
runs-on: macos-14
needs: check-rust-changes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -99,13 +151,39 @@ jobs:
run: yarn
- name: Install Playwright Browsers
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
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
- 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
with:
workspaces: './src/wasm-lib'
- 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
- name: build web
run: yarn build:local
@ -122,7 +200,7 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v4
if: github.ref == 'refs/heads/main'
with:
name: wasm-bundle

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.19.1",
"version": "0.19.3",
"private": true,
"dependencies": {
"@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"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
@ -763,6 +764,20 @@ dependencies = [
"anstyle",
"clap_lex",
"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]]
@ -2403,7 +2418,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.52"
version = "0.1.53"
dependencies = [
"anyhow",
"approx",
@ -2412,6 +2427,7 @@ dependencies = [
"base64 0.22.0",
"bson",
"chrono",
"clap",
"dashmap",
"databake",
"derive-docs",
@ -2471,6 +2487,7 @@ dependencies = [
"bigdecimal",
"bytes",
"chrono",
"clap",
"data-encoding",
"format_serde_error",
"futures",
@ -5886,6 +5903,12 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "untrusted"
version = "0.9.0"

View File

@ -15,7 +15,7 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies]
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"
oauth2 = "4.4.2"
serde_json = "1.0"

View File

@ -11,7 +11,7 @@ use std::{
use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project, ProjectState},
file::{FileEntry, Project, ProjectRoute, ProjectState},
project::ProjectConfiguration,
Configuration, DEFAULT_PROJECT_KCL_FILE,
};
@ -52,14 +52,22 @@ async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result
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()?;
// 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))
}
#[tauri::command]
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;
// Check if this file exists.
@ -104,7 +112,7 @@ async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration,
#[tauri::command]
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()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
@ -113,13 +121,19 @@ async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configura
Ok(())
}
fn get_project_settings_file_path(app_settings: Configuration, project_name: &str) -> Result<PathBuf, InvokeError> {
Ok(app_settings
.settings
.project
.directory
.join(project_name)
.join(PROJECT_SETTINGS_FILE_NAME))
async fn get_project_settings_file_path(
app_settings: Configuration,
project_name: &str,
) -> Result<PathBuf, InvokeError> {
let project_dir = app_settings.settings.project.directory.join(project_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]
@ -127,7 +141,7 @@ async fn read_project_settings_file(
app_settings: Configuration,
project_name: &str,
) -> 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.
if !settings_path.exists() {
@ -149,7 +163,7 @@ async fn write_project_settings_file(
project_name: &str,
configuration: ProjectConfiguration,
) -> 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()))?;
tokio::fs::write(settings_path, contents.as_bytes())
.await
@ -195,6 +209,12 @@ async fn get_project_info(configuration: Configuration, project_path: &str) -> R
.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]
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
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<()> {
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![
get_state,
set_state,
@ -351,6 +360,7 @@ fn main() -> Result<()> {
create_new_project_directory,
list_projects,
get_project_info,
parse_project_route,
get_user,
login,
read_dir_recursive,
@ -362,6 +372,16 @@ fn main() -> Result<()> {
])
.plugin(tauri_plugin_cli::init())
.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 source_path: Option<PathBuf> = None;
match app.cli().matches() {
@ -409,6 +429,14 @@ fn main() -> Result<()> {
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
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 source_path.is_dir() {
// Load the details about the project from the path.
@ -467,6 +495,13 @@ fn main() -> Result<()> {
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_fs::init())
.plugin(tauri_plugin_http::init())

View File

@ -71,5 +71,5 @@
}
},
"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()
if (appState) {
console.log('appState', appState)
// Reset the state.
// We do this so that we load the initial state from the cli but everything
// 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 { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
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 { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { useStore } from 'useStore'
@ -103,7 +103,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
wasmUrl: wasmUrl(),
token: token,
baseUnit: defaultUnit.current,
devMode: DEV,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({
worker: LspWorker.Kcl,
@ -177,7 +177,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: CopilotWorkerOptions = {
wasmUrl: wasmUrl(),
token: token,
devMode: DEV,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({
worker: LspWorker.Copilot,

View File

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

View File

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

View File

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

View File

@ -9,10 +9,12 @@ export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
token?: string,
settings = {
pool: null,
theme: Themes.System,
highlightEdges: true,
enableSSAO: true,
} as {
pool: string | null
theme: Themes
highlightEdges: boolean
enableSSAO: boolean
@ -35,6 +37,12 @@ export function useSetupEngineManager(
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(() => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.

View File

@ -888,6 +888,7 @@ export class EngineCommandManager {
sceneCommandArtifacts: ArtifactMap = {}
outSequence = 1
inSequence = 1
pool?: string
engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = []
@ -914,8 +915,9 @@ export class EngineCommandManager {
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
[]
constructor() {
constructor(pool?: string) {
this.engineConnection = undefined
this.pool = pool
}
private _camControlsCameraChange = () => {}
@ -972,7 +974,8 @@ export class EngineCommandManager {
}
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({
engineCommandManager: this,
url,

View File

@ -14,6 +14,7 @@ import init, {
parse_app_settings,
parse_project_settings,
default_project_settings,
parse_project_route,
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
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 { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
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 { 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}`)
}
}
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
}
// Get the backend pool we've requested.
pool(): string {
return this.engineCommandManager.pool || ''
}
// Get the os information.
getOsInfo(): Promise<string> {
if (this.isTauri()) {

View File

@ -1,7 +1,11 @@
import { sep } from '@tauri-apps/api/path'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
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 =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -25,28 +29,23 @@ export const paths = {
} as const
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
const s = isTauri() ? sep() : '/'
const decodedId = decodeURIComponent(id).replace(/\/$/, '') // remove trailing slash
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
const inTauri = isTauri()
return {
projectName,
projectPath,
currentFileName,
currentFilePath,
if (!configuration) {
configuration = inTauri
? await readAppSettingsFile()
: readLocalStorageAppSettingsFile()
}
const route = inTauri
? await parseProjectRoute(configuration, id)
: parseProjectRouteWasm(configuration, id)
return route
}

View File

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

View File

@ -101,7 +101,7 @@ function localStorageProjectSettingsPath() {
return '/' + BROWSER_PROJECT_NAME + '/project.toml'
}
function readLocalStorageAppSettingsFile(): Configuration {
export function readLocalStorageAppSettingsFile(): Configuration {
// TODO: Remove backwards compatibility after a few releases.
let stored =
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 { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
// Get the app state from tauri.
export async function getState(): Promise<ProjectState | undefined> {
@ -80,6 +81,16 @@ export async function login(host: string): Promise<string> {
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(
token: string | undefined,
host: string

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.1.52"
version = "0.1.53"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -16,7 +16,7 @@ async-recursion = "1.1.0"
async-trait = "0.1.80"
base64 = "0.22.0"
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"
databake = { version = "0.1.7", features = ["derive"] }
derive-docs = { version = "0.1.17", path = "../derive-docs" }
@ -24,7 +24,7 @@ form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
git_rev = "0.1.0"
gltf-json = "1.4.0"
kittycad = { workspace = true }
kittycad = { workspace = true, features = ["clap"] }
kittycad-execution-plan-macros = { workspace = true }
kittycad-execution-plan-traits = { workspace = true }
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"] }
[features]
default = ["engine"]
default = ["cli", "engine"]
cli = ["dep:clap"]
engine = []

View File

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

View File

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

View File

@ -16,6 +16,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = baseApiUrl, catch)]
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)]
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))
}
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> {
let promise = self
.manager

View File

@ -5,6 +5,8 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::Configuration;
/// State management for the application.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
@ -14,6 +16,100 @@ pub struct ProjectState {
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.
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[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 anyhow::Result;
use clap::ValueEnum;
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.
#[async_recursion::async_recursion]
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?;
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() {
children.push(walk_dir(&e.path()).await?);
} else {
if !is_relevant_file(&e.path())? {
continue;
}
children.push(FileEntry {
name: e.file_name().to_string_lossy().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)
}
/// 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>,
units: &str,
token: String,
is_dev: bool,
baseurl: String,
) -> Result<(), JsValue> {
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 mut zoo_client = kittycad::Client::new(token);
if is_dev {
zoo_client.set_base_url("https://api.dev.zoo.dev");
}
zoo_client.set_base_url(baseurl.as_str());
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
#[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();
let ServerConfig {
@ -323,9 +321,7 @@ pub async fn copilot_lsp_run(config: ServerConfig, token: String, is_dev: bool)
} = config;
let mut zoo_client = kittycad::Client::new(token);
if is_dev {
zoo_client.set_base_url("https://api.dev.zoo.dev");
}
zoo_client.set_base_url(baseurl.as_str());
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.
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())
}