use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub const FAMILY_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Family {
pub version: u32,
pub crates: Vec<String>,
}
impl Default for Family {
fn default() -> Self {
Family { version: FAMILY_VERSION, crates: Vec::new() }
}
}
pub fn path_for_project(root: &Path) -> PathBuf {
root.join("family.ron")
}
impl Family {
pub fn from_ron(text: &str) -> Result<Family, String> {
let f: Family = ron::from_str(text).map_err(|e| format!("family: parse: {e}"))?;
if f.version != FAMILY_VERSION {
return Err(format!("family: version {} is not the {FAMILY_VERSION} this build reads", f.version));
}
let mut seen = std::collections::BTreeSet::new();
for c in &f.crates {
if !seen.insert(c.as_str()) {
return Err(format!("family: {c} is listed twice"));
}
}
Ok(f)
}
pub fn load(root: &Path) -> Result<Family, String> {
let path = path_for_project(root);
match std::fs::read_to_string(&path) {
Ok(text) => Family::from_ron(&text).map_err(|e| format!("{}: {e}", path.display())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Family::default()),
Err(e) => Err(format!("family: {}: {e}", path.display())),
}
}
pub fn problems(&self, known: &[&str], built: &[&str]) -> Vec<String> {
self.crates
.iter()
.filter_map(|c| {
if !known.contains(&c.as_str()) {
Some(format!("family.ron lists {c}, which is not a family crate the editor knows"))
} else if !built.contains(&c.as_str()) {
Some(format!(
"family.ron lists {c}, but this editor was built without the `{c}` feature; rebuild with `--features {c}`"
))
} else {
None
}
})
.collect()
}
pub fn active<'a>(&'a self, built: &[&str]) -> Vec<&'a str> {
self.crates.iter().map(String::as_str).filter(|c| built.contains(c)).collect()
}
}
pub fn set_field(text: &str, path: &str, value: &str) -> Result<(String, String), String> {
let value = value.trim();
if value.is_empty() || value_end(value, 0, &[]) != Some(value.len()) {
return Err(format!("family: `{value}` is not one RON value"));
}
let (mut start, mut end) = (0, text.len());
for (depth, name) in path.split('.').enumerate() {
let open = skip_ws(text, start, end);
let open = if depth == 0 { open } else { start };
let body = enter(text, open, end).ok_or_else(|| format!("family: `{name}` is not inside a struct"))?;
let (vs, ve) = find_field(text, body, name)?;
start = vs;
end = ve;
}
let old = text[start..end].to_string();
Ok((format!("{}{}{}", &text[..start], value, &text[end..]), old))
}
fn skip_ws(t: &str, mut i: usize, end: usize) -> usize {
let b = t.as_bytes();
while i < end {
if b[i].is_ascii_whitespace() {
i += 1;
} else if t[i..].starts_with("//") {
while i < end && b[i] != b'\n' {
i += 1;
}
} else if t[i..].starts_with("/*") {
i = t[i..].find("*/").map_or(end, |n| i + n + 2);
} else {
break;
}
}
i
}
fn enter(t: &str, i: usize, end: usize) -> Option<(usize, usize)> {
let b = t.as_bytes();
let mut j = i;
while j < end && (b[j].is_ascii_alphanumeric() || b[j] == b'_') {
j += 1;
}
let j = skip_ws(t, j, end);
if j >= end || !matches!(b[j], b'(' | b'{') {
return None;
}
let close = value_end(t, j, &[])?;
Some((j + 1, close - 1))
}
fn value_end(t: &str, i: usize, _stop: &[u8]) -> Option<usize> {
let b = t.as_bytes();
let mut depth = 0i32;
let mut j = i;
while j < b.len() {
match b[j] {
b'"' => {
j += 1;
while j < b.len() && b[j] != b'"' {
j += if b[j] == b'\\' { 2 } else { 1 };
}
if j >= b.len() {
return None;
}
}
b'/' if t[j..].starts_with("//") => {
while j < b.len() && b[j] != b'\n' {
j += 1;
}
continue;
}
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => {
if depth == 0 {
return Some(j);
}
depth -= 1;
if depth == 0 {
return Some(j + 1);
}
}
b',' if depth == 0 => return Some(j),
_ => {}
}
j += 1;
}
if depth == 0 { Some(b.len()) } else { None }
}
fn find_field(t: &str, body: (usize, usize), name: &str) -> Result<(usize, usize), String> {
let b = t.as_bytes();
let mut i = body.0;
while i < body.1 {
i = skip_ws(t, i, body.1);
if i >= body.1 {
break;
}
let ks = i;
while i < body.1 && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
i += 1;
}
let key = &t[ks..i];
i = skip_ws(t, i, body.1);
if key.is_empty() || i >= body.1 || b[i] != b':' {
return Err(format!("family: `{name}` not found (a field list was expected)"));
}
i = skip_ws(t, i + 1, body.1);
let vs = i;
let ve = value_extent(t, vs, body.1);
if key == name {
let mut e = ve;
while e > vs && b[e - 1].is_ascii_whitespace() {
e -= 1;
}
return Ok((vs, e));
}
i = ve;
if i < body.1 && b[i] == b',' {
i += 1;
}
}
Err(format!("family: no field `{name}`"))
}
fn value_extent(t: &str, i: usize, end: usize) -> usize {
let b = t.as_bytes();
let mut j = i;
while j < end && (b[j].is_ascii_alphanumeric() || b[j] == b'_' || b[j] == b'-' || b[j] == b'.') {
j += 1;
}
if j < end && matches!(b[j], b'(' | b'[' | b'{') {
return value_end(t, j, &[]).unwrap_or(end);
}
if j > i {
return j;
}
value_end(t, i, &[]).map_or(end, |e| e.min(end))
}
pub fn settings_dir(root: &Path) -> PathBuf {
root.join("family")
}
const SETTINGS_POLL_SECS: f64 = 0.1;
const SETTINGS_SETTLE_SECS: f64 = 0.1;
pub struct FamilyWatch {
files: crate::live::FileWatch,
dir: PathBuf,
good: std::collections::BTreeMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SettingsChange {
Reloaded { krate: String, text: String },
Invalid { krate: String, error: String },
Removed { krate: String },
}
impl FamilyWatch {
pub fn new(root: &Path) -> FamilyWatch {
let dir = settings_dir(root);
let mut watch = FamilyWatch { files: crate::live::FileWatch::new(&dir, &["ron"]).with_timing(SETTINGS_POLL_SECS, SETTINGS_SETTLE_SECS), dir, good: Default::default() };
for (krate, text) in watch.read() {
if ron::from_str::<ron::Value>(&text).is_ok() {
watch.good.insert(krate, text);
}
}
watch
}
pub fn current(&self) -> &std::collections::BTreeMap<String, String> {
&self.good
}
fn read(&self) -> Vec<(String, String)> {
let Ok(entries) = std::fs::read_dir(&self.dir) else { return Vec::new() };
let mut out: Vec<(String, String)> = entries
.flatten()
.filter_map(|e| {
let p = e.path();
if p.extension()? != "ron" || p.file_name()?.to_string_lossy().starts_with('.') {
return None;
}
Some((p.file_stem()?.to_string_lossy().into_owned(), std::fs::read_to_string(&p).ok()?))
})
.collect();
out.sort();
out
}
pub fn poll(&mut self, now_secs: f64) -> Vec<SettingsChange> {
if !self.files.poll(now_secs) {
return Vec::new();
}
let now = self.read();
let mut out = Vec::new();
for (krate, text) in &now {
if self.good.get(krate) == Some(text) {
continue;
}
match ron::from_str::<ron::Value>(text) {
Ok(_) => {
self.good.insert(krate.clone(), text.clone());
out.push(SettingsChange::Reloaded { krate: krate.clone(), text: text.clone() });
}
Err(e) => out.push(SettingsChange::Invalid { krate: krate.clone(), error: e.to_string() }),
}
}
let gone: Vec<String> = self.good.keys().filter(|k| !now.iter().any(|(n, _)| n == *k)).cloned().collect();
for krate in gone {
self.good.remove(&krate);
out.push(SettingsChange::Removed { krate });
}
out
}
}