diff --git a/.vscode/settings.json b/.vscode/settings.json index 173c424..13f714f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,14 +3,8 @@ "luau-lsp.sourcemap.enabled": false, "luau-lsp.types.roblox": false, "luau-lsp.require.mode": "relativeToFile", - "luau-lsp.require.fileAliases": { - "@lune/fs": "./docs/typedefs/FS.luau", - "@lune/net": "./docs/typedefs/Net.luau", - "@lune/process": "./docs/typedefs/Process.luau", - "@lune/roblox": "./docs/typedefs/Roblox.luau", - "@lune/serde": "./docs/typedefs/Serde.luau", - "@lune/stdio": "./docs/typedefs/Stdio.luau", - "@lune/task": "./docs/typedefs/Task.luau" + "luau-lsp.require.directoryAliases": { + "@lune/": "./docs/typedefs" }, // Luau - ignore type defs file in docs dir and dev scripts we use "luau-lsp.ignoreGlobs": [ diff --git a/Cargo.lock b/Cargo.lock index 0cb3033..6ad1ada 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1135,6 +1135,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "thiserror", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index a3a2932..73a7d4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ console = "0.15" directories = "5.0" futures-util = "0.3" once_cell = "1.17" +thiserror = "1.0" mlua = { version = "0.9.0-beta.3", features = ["luau", "serialize"] } tokio = { version = "1.24", features = ["full"] } diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index d82fa3f..c6601e0 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -31,6 +31,7 @@ serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true tokio.workspace = true +thiserror.workspace = true anyhow = "1.0" env_logger = "0.10" diff --git a/packages/cli/src/cli.rs b/packages/cli/src/cli.rs index 9a63e8f..9d26e06 100644 --- a/packages/cli/src/cli.rs +++ b/packages/cli/src/cli.rs @@ -1,32 +1,22 @@ -use std::{ - borrow::BorrowMut, - collections::HashMap, - fmt::Write as _, - path::{Path, PathBuf}, - process::ExitCode, -}; +use std::{fmt::Write as _, process::ExitCode}; use anyhow::{Context, Result}; use clap::{CommandFactory, Parser}; -use serde_json::Value as JsonValue; -use include_dir::{include_dir, Dir}; use lune::Lune; use tokio::{ - fs::{self, read as read_to_vec}, + fs::read as read_to_vec, io::{stdin, AsyncReadExt}, }; use crate::{ - gen::{generate_gitbook_dir_from_definitions, generate_typedef_files_from_definitions}, + setup::run_setup, utils::{ files::{discover_script_file_path_including_lune_dirs, strip_shebang}, listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list}, }, }; -pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("docs/typedefs"); - /// A Luau script runner #[derive(Parser, Debug, Default, Clone)] #[command(version, long_about = None)] @@ -51,9 +41,6 @@ pub struct Cli { /// Generate a Lune documentation file for Luau LSP #[clap(long, hide = true)] generate_docs_file: bool, - /// Generate the full Lune gitbook directory - #[clap(long, hide = true)] - generate_gitbook_dir: bool, } #[allow(dead_code)] @@ -134,61 +121,21 @@ impl Cli { let generate_file_requested = self.setup || self.generate_luau_types || self.generate_selene_types - || self.generate_docs_file - || self.generate_gitbook_dir; + || self.generate_docs_file; if generate_file_requested { - if self.generate_gitbook_dir { - generate_gitbook_dir_from_definitions(&TYPEDEFS_DIR).await?; - } if (self.generate_luau_types || self.generate_selene_types || self.generate_docs_file) && !self.setup { eprintln!( "\ - Typedef & docs generation files have been superseded by the --setup command.\ - Run lune --setup in your terminal to configure typedef files. + Typedef & docs generation commands have been superseded by the setup command.\ + Run `lune --setup` in your terminal to configure your editor and type definitions. " ); return Ok(ExitCode::FAILURE); } if self.setup { - let generated_paths = - generate_typedef_files_from_definitions(&TYPEDEFS_DIR).await?; - let settings_json_path = PathBuf::from(".vscode/settings.json"); - let message = match fs::metadata(&settings_json_path).await { - Ok(meta) if meta.is_file() => { - if try_add_generated_typedefs_vscode(&settings_json_path, &generated_paths).await.is_err() { - "These files can be added to your LSP settings for autocomplete and documentation." - } else { - "These files have now been added to your workspace LSP settings for Visual Studio Code." - } - } - _ => "These files can be added to your LSP settings for autocomplete and documentation.", - }; - // HACK: We should probably just be serializing this hashmap to print it out, but - // that does not guarantee sorting and the sorted version is much easier to read - let mut sorted_names = generated_paths - .keys() - .map(ToString::to_string) - .collect::>(); - sorted_names.sort_unstable(); - println!( - "Typedefs have been generated in the following locations:\n{{\n{}\n}}\n{message}", - sorted_names - .iter() - .map(|name| { - let path = generated_paths.get(name).unwrap(); - format!( - " \"@lune/{}\": \"{}\",", - name, - path.canonicalize().unwrap().display() - ) - }) - .collect::>() - .join("\n") - .strip_suffix(',') - .unwrap() - ); + run_setup().await; } } if self.script_path.is_none() { @@ -236,29 +183,3 @@ impl Cli { }) } } - -async fn try_add_generated_typedefs_vscode( - settings_json_path: &Path, - generated_paths: &HashMap, -) -> Result<()> { - // FUTURE: Use a jsonc or json5 to read this file instead since it may contain comments and fail - let settings_json_contents = fs::read(settings_json_path).await?; - let mut settings_changed: bool = false; - let mut settings_json: JsonValue = serde_json::from_slice(&settings_json_contents)?; - if let JsonValue::Object(settings) = settings_json.borrow_mut() { - if let Some(JsonValue::Object(aliases)) = settings.get_mut("luau-lsp.require.fileAliases") { - for (name, path) in generated_paths { - settings_changed = true; - aliases.insert( - format!("@lune/{name}"), - JsonValue::String(path.canonicalize().unwrap().to_string_lossy().to_string()), - ); - } - } - } - if settings_changed { - let settings_json_new = serde_json::to_vec_pretty(&settings_json)?; - fs::write(settings_json_path, settings_json_new).await?; - } - Ok(()) -} diff --git a/packages/cli/src/gen/mod.rs b/packages/cli/src/gen/mod.rs index a68e262..1238fd1 100644 --- a/packages/cli/src/gen/mod.rs +++ b/packages/cli/src/gen/mod.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::collections::HashMap; use anyhow::Result; use include_dir::Dir; @@ -15,9 +15,7 @@ pub async fn generate_gitbook_dir_from_definitions(dir: &Dir<'_>) -> Result<()> gitbook_dir::generate_from_type_definitions(definitions).await } -pub async fn generate_typedef_files_from_definitions( - dir: &Dir<'_>, -) -> Result> { +pub async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result { let contents = read_typedefs_dir_contents(dir); typedef_files::generate_from_type_definitions(contents).await } diff --git a/packages/cli/src/gen/typedef_files.rs b/packages/cli/src/gen/typedef_files.rs index 33cc305..b08b651 100644 --- a/packages/cli/src/gen/typedef_files.rs +++ b/packages/cli/src/gen/typedef_files.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, path::PathBuf}; +use std::collections::HashMap; use anyhow::{Context, Result}; use directories::UserDirs; @@ -9,7 +9,8 @@ use tokio::fs::{create_dir_all, write}; #[allow(clippy::too_many_lines)] pub async fn generate_from_type_definitions( typedef_files: HashMap>, -) -> Result> { +) -> Result { + let version_string = env!("CARGO_PKG_VERSION"); let mut dirs_to_write = Vec::new(); let mut files_to_write = Vec::new(); // Create the typedefs dir in the users cache dir @@ -18,7 +19,7 @@ pub async fn generate_from_type_definitions( .home_dir() .join(".lune") .join(".typedefs") - .join(env!("CARGO_PKG_VERSION")); + .join(version_string); dirs_to_write.push(cache_dir.clone()); // Make typedef files for (builtin_name, builtin_typedef) in typedef_files { @@ -38,8 +39,5 @@ pub async fn generate_from_type_definitions( .collect::>(); try_join_all(futs_dirs).await?; try_join_all(futs_files).await?; - Ok(files_to_write - .drain(..) - .map(|(name, path, _)| (name, path)) - .collect::>()) + Ok(version_string.to_string()) } diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index c3f43ba..6defbfa 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -14,11 +14,9 @@ use clap::Parser; pub(crate) mod cli; pub(crate) mod gen; +pub(crate) mod setup; pub(crate) mod utils; -#[cfg(test)] -mod tests; - use cli::Cli; use console::style; diff --git a/packages/cli/src/setup/mod.rs b/packages/cli/src/setup/mod.rs new file mode 100644 index 0000000..098271b --- /dev/null +++ b/packages/cli/src/setup/mod.rs @@ -0,0 +1,128 @@ +use std::{borrow::BorrowMut, env::current_dir, io::ErrorKind, path::PathBuf}; + +use anyhow::Result; +use include_dir::{include_dir, Dir}; +use thiserror::Error; +use tokio::fs; + +// TODO: Use a library that supports json with comments since VSCode settings may contain comments +use serde_json::Value as JsonValue; + +use crate::gen::generate_typedef_files_from_definitions; + +pub(crate) static TYPEDEFS_DIR: Dir<'_> = include_dir!("docs/typedefs"); + +pub(crate) static SETTING_NAME_MODE: &str = "luau-lsp.require.mode"; +pub(crate) static SETTING_NAME_ALIASES: &str = "luau-lsp.require.directoryAliases"; + +#[derive(Debug, Clone, Copy, Error)] +enum SetupError { + #[error("Failed to read settings")] + Read, + #[error("Failed to write settings")] + Write, + #[error("Failed to parse settings")] + Deserialize, + #[error("Failed to create settings")] + Serialize, +} + +fn lune_version() -> &'static str { + env!("CARGO_PKG_VERSION") +} + +fn vscode_path() -> PathBuf { + current_dir() + .expect("No current dir") + .join(".vscode") + .join("settings.json") +} + +async fn read_or_create_vscode_settings_json() -> Result { + let path_file = vscode_path(); + let mut path_dir = path_file.clone(); + path_dir.pop(); + match fs::read(&path_file).await { + Err(e) if e.kind() == ErrorKind::NotFound => { + // TODO: Make sure that VSCode is actually installed, or + // let the user choose their editor for interactive setup + match fs::create_dir_all(path_dir).await { + Err(_) => Err(SetupError::Write), + Ok(_) => match fs::write(path_file, "{}").await { + Err(_) => Err(SetupError::Write), + Ok(_) => Ok(JsonValue::Object(serde_json::Map::new())), + }, + } + } + Err(_) => Err(SetupError::Read), + Ok(contents) => match serde_json::from_slice(&contents) { + Err(_) => Err(SetupError::Deserialize), + Ok(json) => Ok(json), + }, + } +} + +async fn write_vscode_settings_json(value: JsonValue) -> Result<(), SetupError> { + match serde_json::to_vec_pretty(&value) { + Err(_) => Err(SetupError::Serialize), + Ok(json) => match fs::write(vscode_path(), json).await { + Err(_) => Err(SetupError::Write), + Ok(_) => Ok(()), + }, + } +} + +fn add_values_to_vscode_settings_json(value: JsonValue) -> JsonValue { + let mut settings_json = value; + if let JsonValue::Object(settings) = settings_json.borrow_mut() { + // Set require mode + let mode_val = "relativeToFile".to_string(); + settings.insert(SETTING_NAME_MODE.to_string(), JsonValue::String(mode_val)); + // Set require alias to our typedefs + let aliases_key = "@lune/".to_string(); + let aliases_val = format!("~/.lune/.typedefs/{}/", lune_version()); + if let Some(JsonValue::Object(aliases)) = settings.get_mut(SETTING_NAME_ALIASES) { + if aliases.contains_key(&aliases_key) { + if aliases.get(&aliases_key).unwrap() != &JsonValue::String(aliases_val.to_string()) + { + aliases.insert(aliases_key, JsonValue::String(aliases_val)); + } + } else { + aliases.insert(aliases_key, JsonValue::String(aliases_val)); + } + } else { + let mut map = serde_json::Map::new(); + map.insert(aliases_key, JsonValue::String(aliases_val)); + settings.insert(SETTING_NAME_ALIASES.to_string(), JsonValue::Object(map)); + } + } + settings_json +} + +pub async fn run_setup() { + generate_typedef_files_from_definitions(&TYPEDEFS_DIR) + .await + .expect("Failed to generate typedef files"); + // TODO: Let the user interactively choose what editor to set up + let res = async { + let settings = read_or_create_vscode_settings_json().await?; + let modified = add_values_to_vscode_settings_json(settings); + write_vscode_settings_json(modified).await?; + Ok::<_, SetupError>(()) + } + .await; + let message = match res { + Ok(_) => "These settings have been added to your workspace for Visual Studio Code:", + Err(_) => "To finish setting up your editor, add these settings to your workspace:", + }; + let version_string = lune_version(); + println!( + "Lune has now been set up and editor type definitions have been generated.\ + \n{message}\ + \n\ + \n\"{SETTING_NAME_MODE}\": \"relativeToFile\",\ + \n\"{SETTING_NAME_ALIASES}\": {{\ + \n \"@lune/\": \"~/.lune/.typedefs/{version_string}/\"\ + \n}}", + ); +} diff --git a/packages/cli/src/tests/bin_dir.rs b/packages/cli/src/tests/bin_dir.rs deleted file mode 100644 index b23e7dc..0000000 --- a/packages/cli/src/tests/bin_dir.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::{env::set_current_dir, path::PathBuf}; - -use anyhow::{Context, Result}; -use tokio::fs::create_dir_all; - -pub async fn enter_bin_dir() -> Result<()> { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../bin"); - if !path.exists() { - create_dir_all(&path) - .await - .context("Failed to enter bin dir")?; - set_current_dir(&path).context("Failed to set current dir")?; - } - Ok(()) -} - -pub fn leave_bin_dir() -> Result<()> { - set_current_dir(env!("CARGO_MANIFEST_DIR")).context("Failed to leave bin dir")?; - Ok(()) -} diff --git a/packages/cli/src/tests/file_checks.rs b/packages/cli/src/tests/file_checks.rs deleted file mode 100644 index 9d72e92..0000000 --- a/packages/cli/src/tests/file_checks.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::path::PathBuf; - -use anyhow::{bail, Context, Result}; -use tokio::fs::{read_to_string, remove_file}; - -use super::bin_dir::{enter_bin_dir, leave_bin_dir}; -use super::file_type::FileType; - -pub fn fmt_path_relative_to_workspace_root(value: &str) -> String { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../") - .canonicalize() - .unwrap(); - match PathBuf::from(value).strip_prefix(root) { - Err(_) => format!("{:#?}", PathBuf::from(value).display()), - Ok(inner) => format!("{:#?}", inner.display()), - } -} - -async fn inner(file_name: &str, desired_type: FileType) -> Result<()> { - match read_to_string(file_name).await.with_context(|| { - format!( - "Failed to read definitions file at '{}'", - fmt_path_relative_to_workspace_root(file_name) - ) - }) { - Ok(file_contents) => { - remove_file(file_name).await.with_context(|| { - format!( - "Failed to remove definitions file at '{}'", - fmt_path_relative_to_workspace_root(file_name) - ) - })?; - let parsed_type = FileType::sniff(&file_contents); - if parsed_type != Some(desired_type) { - bail!( - "Generating definitions file at '{}' created '{}', expected '{}'", - fmt_path_relative_to_workspace_root(file_name), - parsed_type.map_or("unknown", |t| t.name()), - desired_type.name() - ) - } - Ok(()) - } - Err(e) => bail!( - "Failed to generate definitions file at '{}'\n{e}", - fmt_path_relative_to_workspace_root(file_name) - ), - } -} - -pub async fn ensure_file_exists_and_is(file_name: &str, desired_type: FileType) -> Result<()> { - enter_bin_dir().await?; - let res = inner(file_name, desired_type).await; - leave_bin_dir()?; - res -} diff --git a/packages/cli/src/tests/file_type.rs b/packages/cli/src/tests/file_type.rs deleted file mode 100644 index 94731e0..0000000 --- a/packages/cli/src/tests/file_type.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde_json::Value as JsonValue; -use serde_yaml::Value as YamlValue; - -use crate::gen::definitions::DefinitionsTree; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileType { - Json, - Yaml, - Luau, -} - -impl FileType { - pub fn sniff(contents: &str) -> Option { - if serde_json::from_str::(contents).is_ok() { - Some(Self::Json) - } else if serde_yaml::from_str::(contents).is_ok() { - Some(Self::Yaml) - } else if DefinitionsTree::from_type_definitions(contents).is_ok() { - Some(Self::Luau) - } else { - None - } - } - - pub fn name(self) -> &'static str { - match self { - FileType::Json => "json", - FileType::Yaml => "yaml", - FileType::Luau => "luau", - } - } -} diff --git a/packages/cli/src/tests/mod.rs b/packages/cli/src/tests/mod.rs deleted file mode 100644 index 010ef50..0000000 --- a/packages/cli/src/tests/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::Result; - -use crate::cli::Cli; - -mod bin_dir; -mod file_checks; -mod file_type; -mod run_cli; - -pub(crate) use run_cli::*; - -#[tokio::test] -async fn list() -> Result<()> { - Cli::new().list().run().await?; - Ok(()) -} - -#[tokio::test] -async fn generate_typedef_files() -> Result<()> { - run_cli(Cli::new().setup()).await?; - // TODO: Implement test - Ok(()) -} diff --git a/packages/cli/src/tests/run_cli.rs b/packages/cli/src/tests/run_cli.rs deleted file mode 100644 index 9926c78..0000000 --- a/packages/cli/src/tests/run_cli.rs +++ /dev/null @@ -1,12 +0,0 @@ -use anyhow::Result; - -use crate::cli::Cli; - -use super::bin_dir::{enter_bin_dir, leave_bin_dir}; - -pub async fn run_cli(cli: Cli) -> Result<()> { - enter_bin_dir().await?; - cli.run().await?; - leave_bin_dir()?; - Ok(()) -} diff --git a/packages/lib-roblox/Cargo.toml b/packages/lib-roblox/Cargo.toml index 36b0f3b..9c3cdf5 100644 --- a/packages/lib-roblox/Cargo.toml +++ b/packages/lib-roblox/Cargo.toml @@ -17,10 +17,10 @@ path = "src/lib.rs" [dependencies] mlua.workspace = true once_cell.workspace = true +thiserror.workspace = true glam = "0.24" rand = "0.8" -thiserror = "1.0" rbx_binary = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "2e78feb05e033cbca8db1d9e490f8334c096d13e" } rbx_dom_weak = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "2e78feb05e033cbca8db1d9e490f8334c096d13e" }