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>
This commit is contained in:
Jess Frazelle
2024-04-25 11:55:11 -07:00
committed by GitHub
parent 879d7ec4f4
commit e158f6f513
13 changed files with 341 additions and 46 deletions

2
src-tauri/Cargo.lock generated
View File

@ -2418,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",

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,
}; };
@ -209,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())
@ -365,6 +371,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,
@ -423,6 +430,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.

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

@ -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

@ -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",

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"

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

@ -507,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())
}