Rewrite setup subcommand to be more permissive and user friendly

This commit is contained in:
Filip Tibell 2023-06-28 12:10:17 +02:00
parent 65f2319a64
commit 66e122ea63
No known key found for this signature in database
15 changed files with 149 additions and 254 deletions

10
.vscode/settings.json vendored
View file

@ -3,14 +3,8 @@
"luau-lsp.sourcemap.enabled": false, "luau-lsp.sourcemap.enabled": false,
"luau-lsp.types.roblox": false, "luau-lsp.types.roblox": false,
"luau-lsp.require.mode": "relativeToFile", "luau-lsp.require.mode": "relativeToFile",
"luau-lsp.require.fileAliases": { "luau-lsp.require.directoryAliases": {
"@lune/fs": "./docs/typedefs/FS.luau", "@lune/": "./docs/typedefs"
"@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 - ignore type defs file in docs dir and dev scripts we use // Luau - ignore type defs file in docs dir and dev scripts we use
"luau-lsp.ignoreGlobs": [ "luau-lsp.ignoreGlobs": [

1
Cargo.lock generated
View file

@ -1135,6 +1135,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"thiserror",
"tokio", "tokio",
] ]

View file

@ -21,6 +21,7 @@ console = "0.15"
directories = "5.0" directories = "5.0"
futures-util = "0.3" futures-util = "0.3"
once_cell = "1.17" once_cell = "1.17"
thiserror = "1.0"
mlua = { version = "0.9.0-beta.3", features = ["luau", "serialize"] } mlua = { version = "0.9.0-beta.3", features = ["luau", "serialize"] }
tokio = { version = "1.24", features = ["full"] } tokio = { version = "1.24", features = ["full"] }

View file

@ -31,6 +31,7 @@ serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
serde_yaml.workspace = true serde_yaml.workspace = true
tokio.workspace = true tokio.workspace = true
thiserror.workspace = true
anyhow = "1.0" anyhow = "1.0"
env_logger = "0.10" env_logger = "0.10"

View file

@ -1,32 +1,22 @@
use std::{ use std::{fmt::Write as _, process::ExitCode};
borrow::BorrowMut,
collections::HashMap,
fmt::Write as _,
path::{Path, PathBuf},
process::ExitCode,
};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::{CommandFactory, Parser}; use clap::{CommandFactory, Parser};
use serde_json::Value as JsonValue;
use include_dir::{include_dir, Dir};
use lune::Lune; use lune::Lune;
use tokio::{ use tokio::{
fs::{self, read as read_to_vec}, fs::read as read_to_vec,
io::{stdin, AsyncReadExt}, io::{stdin, AsyncReadExt},
}; };
use crate::{ use crate::{
gen::{generate_gitbook_dir_from_definitions, generate_typedef_files_from_definitions}, setup::run_setup,
utils::{ utils::{
files::{discover_script_file_path_including_lune_dirs, strip_shebang}, files::{discover_script_file_path_including_lune_dirs, strip_shebang},
listing::{find_lune_scripts, sort_lune_scripts, write_lune_scripts_list}, 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 /// A Luau script runner
#[derive(Parser, Debug, Default, Clone)] #[derive(Parser, Debug, Default, Clone)]
#[command(version, long_about = None)] #[command(version, long_about = None)]
@ -51,9 +41,6 @@ pub struct Cli {
/// Generate a Lune documentation file for Luau LSP /// Generate a Lune documentation file for Luau LSP
#[clap(long, hide = true)] #[clap(long, hide = true)]
generate_docs_file: bool, generate_docs_file: bool,
/// Generate the full Lune gitbook directory
#[clap(long, hide = true)]
generate_gitbook_dir: bool,
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -134,61 +121,21 @@ impl Cli {
let generate_file_requested = self.setup let generate_file_requested = self.setup
|| self.generate_luau_types || self.generate_luau_types
|| self.generate_selene_types || self.generate_selene_types
|| self.generate_docs_file || self.generate_docs_file;
|| self.generate_gitbook_dir;
if generate_file_requested { 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) if (self.generate_luau_types || self.generate_selene_types || self.generate_docs_file)
&& !self.setup && !self.setup
{ {
eprintln!( eprintln!(
"\ "\
Typedef & docs generation files have been superseded by the --setup command.\ Typedef & docs generation commands have been superseded by the setup command.\
Run lune --setup in your terminal to configure typedef files. Run `lune --setup` in your terminal to configure your editor and type definitions.
" "
); );
return Ok(ExitCode::FAILURE); return Ok(ExitCode::FAILURE);
} }
if self.setup { if self.setup {
let generated_paths = run_setup().await;
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::<Vec<_>>();
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::<Vec<_>>()
.join("\n")
.strip_suffix(',')
.unwrap()
);
} }
} }
if self.script_path.is_none() { 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<String, PathBuf>,
) -> 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(())
}

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, path::PathBuf}; use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
use include_dir::Dir; 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 gitbook_dir::generate_from_type_definitions(definitions).await
} }
pub async fn generate_typedef_files_from_definitions( pub async fn generate_typedef_files_from_definitions(dir: &Dir<'_>) -> Result<String> {
dir: &Dir<'_>,
) -> Result<HashMap<String, PathBuf>> {
let contents = read_typedefs_dir_contents(dir); let contents = read_typedefs_dir_contents(dir);
typedef_files::generate_from_type_definitions(contents).await typedef_files::generate_from_type_definitions(contents).await
} }

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, path::PathBuf}; use std::collections::HashMap;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use directories::UserDirs; use directories::UserDirs;
@ -9,7 +9,8 @@ use tokio::fs::{create_dir_all, write};
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub async fn generate_from_type_definitions( pub async fn generate_from_type_definitions(
typedef_files: HashMap<String, Vec<u8>>, typedef_files: HashMap<String, Vec<u8>>,
) -> Result<HashMap<String, PathBuf>> { ) -> Result<String> {
let version_string = env!("CARGO_PKG_VERSION");
let mut dirs_to_write = Vec::new(); let mut dirs_to_write = Vec::new();
let mut files_to_write = Vec::new(); let mut files_to_write = Vec::new();
// Create the typedefs dir in the users cache dir // Create the typedefs dir in the users cache dir
@ -18,7 +19,7 @@ pub async fn generate_from_type_definitions(
.home_dir() .home_dir()
.join(".lune") .join(".lune")
.join(".typedefs") .join(".typedefs")
.join(env!("CARGO_PKG_VERSION")); .join(version_string);
dirs_to_write.push(cache_dir.clone()); dirs_to_write.push(cache_dir.clone());
// Make typedef files // Make typedef files
for (builtin_name, builtin_typedef) in typedef_files { for (builtin_name, builtin_typedef) in typedef_files {
@ -38,8 +39,5 @@ pub async fn generate_from_type_definitions(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
try_join_all(futs_dirs).await?; try_join_all(futs_dirs).await?;
try_join_all(futs_files).await?; try_join_all(futs_files).await?;
Ok(files_to_write Ok(version_string.to_string())
.drain(..)
.map(|(name, path, _)| (name, path))
.collect::<HashMap<_, _>>())
} }

View file

@ -14,11 +14,9 @@ use clap::Parser;
pub(crate) mod cli; pub(crate) mod cli;
pub(crate) mod gen; pub(crate) mod gen;
pub(crate) mod setup;
pub(crate) mod utils; pub(crate) mod utils;
#[cfg(test)]
mod tests;
use cli::Cli; use cli::Cli;
use console::style; use console::style;

View file

@ -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<JsonValue, SetupError> {
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}}",
);
}

View file

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

View file

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

View file

@ -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<Self> {
if serde_json::from_str::<JsonValue>(contents).is_ok() {
Some(Self::Json)
} else if serde_yaml::from_str::<YamlValue>(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",
}
}
}

View file

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

View file

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

View file

@ -17,10 +17,10 @@ path = "src/lib.rs"
[dependencies] [dependencies]
mlua.workspace = true mlua.workspace = true
once_cell.workspace = true once_cell.workspace = true
thiserror.workspace = true
glam = "0.24" glam = "0.24"
rand = "0.8" rand = "0.8"
thiserror = "1.0"
rbx_binary = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "2e78feb05e033cbca8db1d9e490f8334c096d13e" } 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" } rbx_dom_weak = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "2e78feb05e033cbca8db1d9e490f8334c096d13e" }