use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader, IsTerminal};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::rc::Rc;
use glacier_dialect::build::{compile_file, source_path};
use glacier_dialect::{SourceMap, is_generated};
use crate::diagnostics::Source;
pub fn sources() -> Vec<PathBuf> {
glacier_dialect::build::sources(Path::new("."))
}
pub fn compile_all() -> Result<usize, String> {
glacier_dialect::build::compile_all(Path::new("."))
}
pub fn format(paths: &[PathBuf], check: bool) -> Result<(), String> {
let defaults = paths.is_empty();
let named = if defaults {
vec![
PathBuf::from("src"),
PathBuf::from("tests"),
PathBuf::from("db"),
]
} else {
paths.to_vec()
};
let mut paths = Vec::new();
for path in named {
if path.is_dir() {
glacier_files(&path, &mut paths);
} else if !defaults {
paths.push(path);
}
}
let mut unformatted = Vec::new();
for path in &paths {
let source =
fs::read_to_string(path).map_err(|e| format!("reading {}: {e}", path.display()))?;
let formatted =
glacier_dialect::format::format(&source, glacier_dialect::Mode::for_path(path))
.map_err(|e| format!("{}:{e}", path.display()))?;
if formatted == source {
continue;
}
if check {
println!("{}", path.display());
} else {
fs::write(path, formatted).map_err(|e| format!("writing {}: {e}", path.display()))?;
println!("formatted {}", path.display());
}
unformatted.push(path);
}
if check && !unformatted.is_empty() {
return Err(format!("{} file(s) need formatting", unformatted.len()));
}
let total = paths.len();
match (check, unformatted.len()) {
(_, 0) => println!("{total} file(s) checked; all formatted"),
(_, n) => println!("{total} file(s) checked; formatted {n}"),
}
Ok(())
}
pub fn cargo(subcommand: &str, cargo_args: &[String]) -> bool {
if let Err(message) = compile_all() {
eprintln!("\x1b[31merror\x1b[0m: {message}");
return false;
}
let color = std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none();
let format = if color {
"json-diagnostic-rendered-ansi"
} else {
"json"
};
let mut child = match Command::new("cargo")
.arg(subcommand)
.args(["--message-format", format])
.args(cargo_args)
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(e) => {
eprintln!("error: running cargo: {e}");
return false;
}
};
let mut maps: HashMap<String, Option<SourceMap>> = HashMap::new();
let mut sources: HashMap<String, Option<Rc<Source>>> = HashMap::new();
let mut lookup = |file: &str| {
sources
.entry(file.to_owned())
.or_insert_with(|| glacier_source(file))
.clone()
};
let stdout = child.stdout.take().expect("piped stdout");
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
let Ok(message) = serde_json::from_str::<serde_json::Value>(&line) else {
println!("{}", map_locations(&line, &mut maps));
continue;
};
if message["reason"] != "compiler-message" {
continue;
}
let diagnostic = &message["message"];
if let Some(text) = crate::diagnostics::render(diagnostic, &mut lookup, color) {
eprint!("{text}");
continue;
}
let Some(rendered) = diagnostic["rendered"].as_str() else {
continue;
};
eprint!(
"{}",
map_rendered(rendered, &diagnostic["spans"], &mut maps)
);
}
child.wait().is_ok_and(|status| status.success())
}
fn map_rendered(
rendered: &str,
spans: &serde_json::Value,
maps: &mut HashMap<String, Option<SourceMap>>,
) -> String {
let mut out = rendered.to_owned();
for span in spans.as_array().into_iter().flatten() {
let Some(file) = span["file_name"].as_str() else {
continue;
};
let (Some(line), Some(col)) = (span["line_start"].as_u64(), span["column_start"].as_u64())
else {
continue;
};
let map = maps
.entry(file.to_owned())
.or_insert_with(|| generated_map(file));
let Some(map) = map else { continue };
let line0 = line as usize - 1;
let src_col = map.to_glacier(line0, col as usize - 1) + 1;
let Some(glacier) = source_path(Path::new(file)) else {
continue;
};
out = out.replace(
&format!("{file}:{line}:{col}"),
&format!("{}:{line}:{src_col}", glacier.display()),
);
}
out
}
fn map_locations(text: &str, maps: &mut HashMap<String, Option<SourceMap>>) -> String {
let cwd = std::env::current_dir().unwrap_or_default();
let mut out = text.to_owned();
for word in text.split_whitespace() {
let mut parts = word.trim_end_matches(':').rsplitn(3, ':');
let (Some(col), Some(line), Some(file)) = (parts.next(), parts.next(), parts.next()) else {
continue;
};
let (Ok(line), Ok(col)) = (line.parse::<usize>(), col.parse::<usize>()) else {
continue;
};
if line == 0 || col == 0 || !file.ends_with(".rs") {
continue;
}
let map = maps
.entry(file.to_owned())
.or_insert_with(|| generated_map(file));
let (Some(map), Some(glacier)) = (map, source_path(Path::new(file))) else {
continue;
};
let src_col = map.to_glacier(line - 1, col - 1) + 1;
let shown = glacier
.strip_prefix(&cwd)
.unwrap_or(&glacier)
.display()
.to_string();
out = out.replace(
&format!("{file}:{line}:{col}"),
&format!("{shown}:{line}:{src_col}"),
);
}
out
}
pub(crate) fn glacier_source(file: &str) -> Option<Rc<Source>> {
let map = generated_map(file)?;
let glacier = source_path(Path::new(file))?;
let text = fs::read_to_string(&glacier).ok()?;
let cwd = std::env::current_dir().unwrap_or_default();
Some(Rc::new(Source {
path: glacier
.strip_prefix(&cwd)
.unwrap_or(&glacier)
.display()
.to_string(),
lines: text.lines().map(str::to_owned).collect(),
map,
}))
}
fn generated_map(file: &str) -> Option<SourceMap> {
let rust = fs::read_to_string(file).ok()?;
if !is_generated(&rust) {
return None;
}
compile_file(&source_path(Path::new(file))?)
.ok()
.map(|(_, map)| map)
}
fn glacier_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
let mut entries: Vec<PathBuf> = entries.filter_map(|e| Some(e.ok()?.path())).collect();
entries.sort();
for path in entries {
if path.is_dir() {
glacier_files(&path, out);
} else if path.extension().is_some_and(|ext| ext == "glacier") {
out.push(path);
}
}
}