use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use super::DynError;
pub fn vekl_include_path() -> Option<PathBuf> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let vendored = manifest_dir.join("vekl");
if vendored.is_dir() {
return Some(vendored);
}
manifest_dir
.parent()
.map(|p| p.join("vekl"))
.filter(|p| p.is_dir())
}
pub fn write_slang_lsp_config(_shader_dir: &str) -> Result<(), DynError> {
let user_manifest = match std::env::var("CARGO_MANIFEST_DIR") {
Ok(v) => PathBuf::from(v),
Err(_) => {
return Ok(());
}
};
let vekl_src = match vekl_include_path() {
Some(p) => p,
None => {
println!("cargo:warning=[prgpu] write_slang_lsp_config: no vekl include dir found, skipping LSP setup");
return Ok(());
}
};
let deps_dir = user_manifest.join(".slang-deps");
let vekl_dst = deps_dir.join("vekl");
sync_vekl_mirror(&vekl_src, &vekl_dst)?;
merge_vscode_settings(&user_manifest)?;
ensure_gitignore(&user_manifest, ".slang-deps/")?;
println!("cargo:rerun-if-changed={}", vekl_src.display());
Ok(())
}
fn sync_vekl_mirror(src: &Path, dst: &Path) -> io::Result<()> {
if dst.exists() {
fs::remove_dir_all(dst)?;
}
fs::create_dir_all(dst)?;
copy_filtered(src, dst)
}
fn copy_filtered(src: &Path, dst: &Path) -> io::Result<()> {
for entry in fs::read_dir(src)? {
let entry = entry?;
let ft = entry.file_type()?;
let from = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
let to = dst.join(&name);
if ft.is_dir() {
if matches!(name_str.as_ref(), ".git" | "target" | "node_modules") {
continue;
}
fs::create_dir_all(&to)?;
copy_filtered(&from, &to)?;
} else if ft.is_file() {
let keep = name_str.ends_with(".slang")
|| name_str == "LICENSE"
|| name_str == "README.md";
if keep {
fs::copy(&from, &to)?;
}
}
}
Ok(())
}
const SEARCH_PATH_VALUE: &str = "${workspaceFolder}/.slang-deps/vekl";
fn merge_vscode_settings(user_manifest: &Path) -> io::Result<()> {
let vscode_dir = user_manifest.join(".vscode");
let settings_path = vscode_dir.join("settings.json");
if !settings_path.exists() {
fs::create_dir_all(&vscode_dir)?;
let initial = format!(
"{{\n \"slang.additionalSearchPaths\": [\"{SEARCH_PATH_VALUE}\"]\n}}\n"
);
fs::write(&settings_path, initial)?;
return Ok(());
}
let content = fs::read_to_string(&settings_path)?;
let mut parsed: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(_) => {
println!(
"cargo:warning=[prgpu] .vscode/settings.json is not strict JSON (likely has comments). Add {:?} to `slang.additionalSearchPaths` manually.",
SEARCH_PATH_VALUE
);
return Ok(());
}
};
let obj = match parsed.as_object_mut() {
Some(o) => o,
None => {
println!(
"cargo:warning=[prgpu] .vscode/settings.json root is not a JSON object, leaving it alone"
);
return Ok(());
}
};
let entry = obj
.entry("slang.additionalSearchPaths")
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
let arr = match entry.as_array_mut() {
Some(a) => a,
None => {
println!(
"cargo:warning=[prgpu] slang.additionalSearchPaths in .vscode/settings.json is not an array, leaving it alone"
);
return Ok(());
}
};
let already_present = arr
.iter()
.any(|v| v.as_str() == Some(SEARCH_PATH_VALUE));
if already_present {
return Ok(());
}
arr.push(serde_json::Value::String(SEARCH_PATH_VALUE.to_string()));
let pretty = serde_json::to_string_pretty(&parsed)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let mut pretty = pretty;
if !pretty.ends_with('\n') {
pretty.push('\n');
}
fs::write(&settings_path, pretty)
}
fn ensure_gitignore(user_manifest: &Path, entry: &str) -> io::Result<()> {
let path = user_manifest.join(".gitignore");
if path.exists() {
let current = fs::read_to_string(&path)?;
for line in current.lines() {
let trimmed = line.trim_end_matches('/');
if trimmed == entry.trim_end_matches('/') {
return Ok(());
}
}
let mut next = current;
if !next.ends_with('\n') {
next.push('\n');
}
next.push_str(entry);
next.push('\n');
fs::write(&path, next)
} else {
fs::write(&path, format!("{entry}\n"))
}
}