use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Deserialize, Default)]
struct ConfigFile {
#[serde(default)]
preludes: Vec<String>,
#[serde(default)]
modules: Vec<String>,
builtin_prelude: Option<bool>,
#[serde(default, rename = "module")]
module_defs: Vec<ModuleDef>,
#[serde(default, rename = "shader")]
shaders: Vec<ShaderBinding>,
}
#[derive(Debug, Clone, Deserialize)]
struct ModuleDef {
name: String,
source: Option<String>,
#[serde(default)]
builtin: bool,
types: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
struct ShaderBinding {
#[serde(rename = "match")]
pattern: String,
#[serde(default)]
modules: Vec<String>,
builtin_prelude: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct Config {
pub preludes: Vec<PathBuf>,
pub modules: Vec<PathBuf>,
pub use_builtin_prelude: bool,
}
impl Config {
pub fn resolve_for(file: &Path) -> Config {
let dir = file.parent().unwrap_or(Path::new("."));
if let Some(toml_path) = find_up(dir, "glsl-lsp.toml")
&& let Ok(text) = std::fs::read_to_string(&toml_path)
&& let Ok(cf) = toml::from_str::<ConfigFile>(&text)
{
let base = toml_path.parent().unwrap_or(Path::new("."));
if !cf.shaders.is_empty() {
return resolve_bindings(file, &cf, base);
}
return Config {
preludes: join_all(base, &cf.preludes),
modules: join_all(base, &cf.modules),
use_builtin_prelude: cf.builtin_prelude.unwrap_or(true),
};
}
if let Some(d) = crate::derive::derive(file) {
return Config {
preludes: Vec::new(),
modules: d.modules,
use_builtin_prelude: d.use_builtin_prelude,
};
}
Config {
preludes: Vec::new(),
modules: discover_sibling_modules(dir),
use_builtin_prelude: true,
}
}
}
fn resolve_bindings(file: &Path, cf: &ConfigFile, base: &Path) -> Config {
let name = file.file_name().and_then(|n| n.to_str()).unwrap_or("");
let rel = file
.strip_prefix(base)
.ok()
.and_then(|r| r.to_str())
.map(|s| s.replace('\\', "/"));
let matches =
|pat: &str| glob_match(pat, name) || rel.as_deref().is_some_and(|r| glob_match(pat, r));
let Some(binding) = cf.shaders.iter().find(|s| matches(&s.pattern)) else {
return Config {
preludes: join_all(base, &cf.preludes),
modules: Vec::new(),
use_builtin_prelude: cf.builtin_prelude.unwrap_or(true),
};
};
let mut modules = Vec::new();
let mut wants_builtin = false;
for module_name in &binding.modules {
if let Some(def) = cf.module_defs.iter().find(|m| &m.name == module_name) {
if let Some(src) = &def.source {
modules.push(base.join(src));
}
wants_builtin |= def.builtin;
}
}
Config {
preludes: join_all(base, &cf.preludes),
modules,
use_builtin_prelude: binding.builtin_prelude.unwrap_or(wants_builtin),
}
}
fn join_all(base: &Path, rels: &[String]) -> Vec<PathBuf> {
rels.iter().map(|p| base.join(p)).collect()
}
pub fn drift_for(file: &Path) -> Option<(String, PathBuf)> {
let dir = file.parent().unwrap_or(Path::new("."));
let toml_path = find_up(dir, "glsl-lsp.toml")?;
let text = std::fs::read_to_string(&toml_path).ok()?;
let cf: ConfigFile = toml::from_str(&text).ok()?;
let base = toml_path.parent().unwrap_or(Path::new("."));
for m in &cf.module_defs {
if let (Some(src), Some(types)) = (&m.source, &m.types)
&& same_path(&base.join(src), file)
{
return Some((m.name.clone(), base.join(types)));
}
}
None
}
fn same_path(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(x), Ok(y)) => x == y,
_ => a == b,
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let (p, t) = (pattern.as_bytes(), text.as_bytes());
let (mut pi, mut ti) = (0, 0);
let (mut star, mut resume) = (None, 0);
while ti < t.len() {
if pi < p.len() && (p[pi] == b'?' || p[pi] == t[ti]) {
pi += 1;
ti += 1;
} else if pi < p.len() && p[pi] == b'*' {
star = Some(pi);
resume = ti;
pi += 1;
} else if let Some(s) = star {
pi = s + 1;
resume += 1;
ti = resume;
} else {
return false;
}
}
while pi < p.len() && p[pi] == b'*' {
pi += 1;
}
pi == p.len()
}
fn discover_sibling_modules(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for e in entries.flatten() {
let p = e.path();
if p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with("Uniforms.glsl"))
{
out.push(p);
}
}
}
out.sort();
out
}
fn find_up(start: &Path, name: &str) -> Option<PathBuf> {
let mut dir = Some(start);
while let Some(d) = dir {
let candidate = d.join(name);
if candidate.is_file() {
return Some(candidate);
}
if d.join(".git").exists() {
break;
}
dir = d.parent();
}
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn glob_matches_filename_patterns() {
assert!(glob_match("draw.*.glsl", "draw.vert.glsl"));
assert!(glob_match("draw.*.glsl", "draw.frag.glsl"));
assert!(glob_match("*.frag.glsl", "blit.frag.glsl"));
assert!(glob_match("blit.vert.glsl", "blit.vert.glsl"));
assert!(!glob_match("draw.*.glsl", "blit.vert.glsl"));
assert!(!glob_match("draw.*.glsl", "draw.vert.glsl.bak"));
assert!(glob_match(
"src/shaders/draw.*",
"src/shaders/draw.vert.glsl"
));
}
fn cf() -> ConfigFile {
ConfigFile {
module_defs: vec![
ModuleDef {
name: "windUniforms".into(),
source: Some("src/shaders/windUniforms.glsl".into()),
builtin: false,
types: None,
},
ModuleDef {
name: "blitUniforms".into(),
source: Some("src/shaders/blitUniforms.glsl".into()),
builtin: false,
types: None,
},
ModuleDef {
name: "project32".into(),
source: None,
builtin: true,
types: None,
},
],
shaders: vec![
ShaderBinding {
pattern: "draw.*.glsl".into(),
modules: vec!["project32".into(), "windUniforms".into()],
builtin_prelude: None,
},
ShaderBinding {
pattern: "blit.*.glsl".into(),
modules: vec!["blitUniforms".into()],
builtin_prelude: None,
},
],
..Default::default()
}
}
#[test]
fn binding_resolves_each_shader_to_its_own_modules() {
let base = Path::new("/proj");
let draw = resolve_bindings(Path::new("/proj/src/shaders/draw.vert.glsl"), &cf(), base);
assert!(draw.use_builtin_prelude);
assert_eq!(
draw.modules,
vec![PathBuf::from("/proj/src/shaders/windUniforms.glsl")]
);
let blit = resolve_bindings(Path::new("/proj/src/shaders/blit.frag.glsl"), &cf(), base);
assert!(!blit.use_builtin_prelude);
assert_eq!(
blit.modules,
vec![PathBuf::from("/proj/src/shaders/blitUniforms.glsl")]
);
}
#[test]
fn unmatched_shader_gets_only_the_prelude() {
let cfg = resolve_bindings(
Path::new("/proj/src/shaders/windUniforms.glsl"),
&cf(),
Path::new("/proj"),
);
assert!(cfg.modules.is_empty());
}
}