use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crate::file_discovery::file_kind_or_tex;
use crate::parser::parse_with_flavor;
use crate::project::{PackageTarget, collect_package_edge_keys};
use crate::semantic::{SignatureDb, scan_definitions};
use crate::syntax::SyntaxNode;
pub trait PackageSource {
fn load(&self, path: &Path) -> Option<(SyntaxNode, PathBuf)>;
}
pub fn collect_package_signatures(
root: &SyntaxNode,
base_dir: Option<&Path>,
src: &impl PackageSource,
) -> SignatureDb {
let mut merged = SignatureDb::default();
let mut visited: HashSet<PathBuf> = HashSet::new();
collect_loaded(root, base_dir, src, &mut visited, &mut merged);
merged.merge_from(&scan_definitions(root));
merged
}
fn collect_loaded(
root: &SyntaxNode,
base_dir: Option<&Path>,
src: &impl PackageSource,
visited: &mut HashSet<PathBuf>,
merged: &mut SignatureDb,
) {
for edge in collect_package_edge_keys(root, base_dir) {
let PackageTarget::Path(path) = edge.target else {
continue;
};
if !visited.insert(path.clone()) {
continue;
}
if let Some((pkg_root, pkg_base)) = src.load(&path) {
collect_loaded(&pkg_root, Some(&pkg_base), src, visited, merged);
merged.merge_from(&scan_definitions(&pkg_root));
}
}
}
pub struct DiskPackageSource;
impl PackageSource for DiskPackageSource {
fn load(&self, path: &Path) -> Option<(SyntaxNode, PathBuf)> {
let text = std::fs::read_to_string(path).ok()?;
let parsed = parse_with_flavor(&text, file_kind_or_tex(path).lex_config());
let base = path.parent().map(Path::to_path_buf).unwrap_or_default();
Some((parsed.syntax(), base))
}
}
pub fn disk_scope_signatures(root: &SyntaxNode, path: &Path) -> SignatureDb {
collect_package_signatures(root, path.parent(), &DiskPackageSource)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
use std::collections::HashMap;
struct MapSource {
files: HashMap<PathBuf, String>,
}
impl MapSource {
fn new(files: &[(&str, &str)]) -> Self {
Self {
files: files
.iter()
.map(|(p, s)| (PathBuf::from(p), s.to_string()))
.collect(),
}
}
}
impl PackageSource for MapSource {
fn load(&self, path: &Path) -> Option<(SyntaxNode, PathBuf)> {
let text = self.files.get(path)?;
let root = SyntaxNode::new_root(parse(text).green);
let base = path.parent().map(Path::to_path_buf).unwrap_or_default();
Some((root, base))
}
}
fn scope(doc: &str, base: &str, files: &[(&str, &str)]) -> SignatureDb {
let root = SyntaxNode::new_root(parse(doc).green);
collect_package_signatures(&root, Some(Path::new(base)), &MapSource::new(files))
}
#[test]
fn pulls_in_a_local_package_definition() {
let db = scope(
"\\usepackage{mypkg}\n\\myfoo{a}{b}\n",
"/proj",
&[("/proj/mypkg.sty", "\\newcommand{\\myfoo}[2]{#1#2}\n")],
);
let sig = db.command("myfoo").expect("package command in scope");
assert_eq!(sig.args.len(), 2);
}
#[test]
fn unresolved_package_contributes_nothing() {
let db = scope("\\usepackage{amsmath}\n", "/proj", &[]);
assert!(db.command("amsmath").is_none());
assert_eq!(db.command_names().count(), 0);
}
#[test]
fn transitive_load_is_followed() {
let db = scope(
"\\usepackage{a}\n",
"/proj",
&[
(
"/proj/a.sty",
"\\RequirePackage{b}\n\\newcommand{\\fa}{x}\n",
),
("/proj/b.sty", "\\newcommand{\\fb}[1]{#1}\n"),
],
);
assert!(db.command("fa").is_some());
assert!(db.command("fb").is_some());
}
#[test]
fn document_definition_wins_over_package() {
let db = scope(
"\\usepackage{mypkg}\n\\newcommand{\\dup}[2]{#1#2}\n",
"/proj",
&[("/proj/mypkg.sty", "\\newcommand{\\dup}[1]{#1}\n")],
);
assert_eq!(db.command("dup").unwrap().args.len(), 2);
}
#[test]
fn load_cycle_terminates() {
let db = scope(
"\\usepackage{a}\n",
"/proj",
&[
(
"/proj/a.sty",
"\\RequirePackage{b}\n\\newcommand{\\fa}{x}\n",
),
(
"/proj/b.sty",
"\\RequirePackage{a}\n\\newcommand{\\fb}{y}\n",
),
],
);
assert!(db.command("fa").is_some());
assert!(db.command("fb").is_some());
}
}