use std::collections::HashMap;
use std::path::{Path, PathBuf};
use smol_str::SmolStr;
use crate::ast::{command_name, environment_name};
use crate::incremental::{
IncrementalDb, QueryKind, QueryLogEntry, file_is_document_root, file_labels,
};
use crate::project::graph::{IncludeGraph, Project, project_graph};
use crate::semantic::SemanticModel;
use crate::syntax::{SyntaxKind, SyntaxNode};
pub fn document_label_names(model: &SemanticModel) -> Vec<SmolStr> {
let mut names: Vec<SmolStr> = model
.labels()
.iter()
.map(|label| label.name.clone())
.collect();
names.sort_unstable();
names.dedup();
names
}
pub fn is_document_root(root: &SyntaxNode) -> bool {
root.descendants().any(|node| match node.kind() {
SyntaxKind::COMMAND => command_name(&node).as_deref() == Some("documentclass"),
SyntaxKind::BEGIN => environment_name(&node).as_deref() == Some("document"),
_ => false,
})
}
#[derive(Debug, Default)]
struct Component {
defs: HashMap<SmolStr, Vec<PathBuf>>,
closed: bool,
rooted: bool,
}
#[derive(Debug, Default)]
pub struct ResolvedLabels {
component_of: HashMap<PathBuf, usize>,
components: Vec<Component>,
}
impl ResolvedLabels {
pub fn build(files: &[(PathBuf, Vec<SmolStr>, bool)], graph: &IncludeGraph) -> Self {
let mut paths: Vec<&Path> = files.iter().map(|(p, _, _)| p.as_path()).collect();
paths.sort_unstable();
paths.dedup();
let index: HashMap<&Path, usize> = paths.iter().enumerate().map(|(i, p)| (*p, i)).collect();
let mut uf = UnionFind::new(paths.len());
for (&path, &i) in &index {
for edge in graph.outgoing(path) {
if let Some(&j) = index.get(edge.to.as_path()) {
uf.union(i, j);
}
}
for included in graph.included_by(path) {
if let Some(&j) = index.get(included.as_path()) {
uf.union(i, j);
}
}
}
let mut root_to_id: HashMap<usize, usize> = HashMap::new();
let mut component_of: HashMap<PathBuf, usize> = HashMap::new();
for (i, &path) in paths.iter().enumerate() {
let root = uf.find(i);
let next = root_to_id.len();
let id = *root_to_id.entry(root).or_insert(next);
component_of.insert(path.to_path_buf(), id);
}
let mut components: Vec<Component> = (0..root_to_id.len())
.map(|_| Component {
closed: true,
..Component::default()
})
.collect();
for (path, names, is_root) in files {
let Some(&id) = component_of.get(path) else {
continue;
};
let comp = &mut components[id];
comp.rooted |= *is_root;
for name in names {
comp.defs
.entry(name.clone())
.or_default()
.push(path.clone());
}
}
for edge in graph.unresolved() {
if let Some(&id) = component_of.get(&edge.from) {
components[id].closed = false;
}
}
for comp in &mut components {
for definers in comp.defs.values_mut() {
definers.sort_unstable();
definers.dedup();
}
}
Self {
component_of,
components,
}
}
pub fn definers(&self, file: &Path, name: &str) -> &[PathBuf] {
self.component_of
.get(file)
.and_then(|&id| self.components[id].defs.get(name))
.map_or(&[], Vec::as_slice)
}
pub fn is_defined(&self, file: &Path, name: &str) -> bool {
!self.definers(file, name).is_empty()
}
pub fn namespace_members(&self, file: &Path) -> Vec<&Path> {
let Some(&id) = self.component_of.get(file) else {
return Vec::new();
};
let mut members: Vec<&Path> = self
.component_of
.iter()
.filter(|&(_, &cid)| cid == id)
.map(|(p, _)| p.as_path())
.collect();
members.sort_unstable();
members
}
pub fn is_closed(&self, file: &Path) -> bool {
self.component_of
.get(file)
.is_some_and(|&id| self.components[id].closed)
}
pub fn is_root_component(&self, file: &Path) -> bool {
self.component_of
.get(file)
.is_some_and(|&id| self.components[id].rooted)
}
}
#[salsa::tracked(returns(ref), no_eq, unsafe(non_update_types))]
pub fn resolved_labels<'db>(db: &'db dyn IncrementalDb, project: Project<'db>) -> ResolvedLabels {
db.record_query(QueryLogEntry {
kind: QueryKind::ResolvedLabels,
file: None,
});
let graph = project_graph(db, project);
let files: Vec<(PathBuf, Vec<SmolStr>, bool)> = project
.members(db)
.iter()
.filter(|member| member.kind.is_latex())
.map(|member| {
(
member.path.clone(),
file_labels(db, member.file).clone(),
*file_is_document_root(db, member.file),
)
})
.collect();
ResolvedLabels::build(&files, graph)
}
struct UnionFind {
parent: Vec<usize>,
size: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
Self {
parent: (0..n).collect(),
size: vec![1; n],
}
}
fn find(&mut self, mut x: usize) -> usize {
while self.parent[x] != x {
self.parent[x] = self.parent[self.parent[x]];
x = self.parent[x];
}
x
}
fn union(&mut self, a: usize, b: usize) {
let (mut ra, mut rb) = (self.find(a), self.find(b));
if ra == rb {
return;
}
if self.size[ra] < self.size[rb] {
std::mem::swap(&mut ra, &mut rb);
}
self.parent[rb] = ra;
self.size[ra] += self.size[rb];
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::project::graph::FileFacts;
use crate::project::include::{IncludeEdgeKey, IncludeKind, IncludeTarget};
fn graph(files: &[(&str, &[(IncludeKind, &str)])]) -> IncludeGraph {
let facts: Vec<FileFacts> = files
.iter()
.map(|(path, edges)| FileFacts {
path: PathBuf::from(path),
include_edges: edges
.iter()
.map(|(kind, target)| IncludeEdgeKey {
kind: *kind,
target: IncludeTarget::Path(PathBuf::from(target)),
})
.collect(),
})
.collect();
IncludeGraph::build(&facts, None)
}
fn names(list: &[&str]) -> Vec<SmolStr> {
list.iter().map(SmolStr::new).collect()
}
#[test]
fn lone_file_is_its_own_component() {
let g = graph(&[("/p/a.tex", &[])]);
let r = ResolvedLabels::build(&[(PathBuf::from("/p/a.tex"), names(&["x"]), false)], &g);
assert!(r.is_defined(Path::new("/p/a.tex"), "x"));
assert!(!r.is_defined(Path::new("/p/a.tex"), "y"));
assert_eq!(
r.definers(Path::new("/p/a.tex"), "x"),
&[PathBuf::from("/p/a.tex")]
);
}
#[test]
fn input_chain_shares_one_namespace() {
let g = graph(&[
("/p/main.tex", &[(IncludeKind::Input, "/p/chap.tex")]),
("/p/chap.tex", &[]),
]);
let r = ResolvedLabels::build(
&[
(PathBuf::from("/p/main.tex"), names(&[]), true),
(PathBuf::from("/p/chap.tex"), names(&["a"]), false),
],
&g,
);
assert!(r.is_defined(Path::new("/p/main.tex"), "a"));
assert!(r.is_root_component(Path::new("/p/chap.tex")));
assert!(r.is_closed(Path::new("/p/main.tex")));
}
#[test]
fn diamond_merges_all_four() {
let g = graph(&[
(
"/p/main.tex",
&[
(IncludeKind::Input, "/p/a.tex"),
(IncludeKind::Input, "/p/b.tex"),
],
),
("/p/a.tex", &[(IncludeKind::Input, "/p/shared.tex")]),
("/p/b.tex", &[(IncludeKind::Input, "/p/shared.tex")]),
("/p/shared.tex", &[]),
]);
let r = ResolvedLabels::build(
&[
(PathBuf::from("/p/main.tex"), names(&[]), true),
(PathBuf::from("/p/a.tex"), names(&["k"]), false),
(PathBuf::from("/p/b.tex"), names(&["k"]), false),
(PathBuf::from("/p/shared.tex"), names(&[]), false),
],
&g,
);
assert_eq!(
r.definers(Path::new("/p/a.tex"), "k"),
&[PathBuf::from("/p/a.tex"), PathBuf::from("/p/b.tex")]
);
assert_eq!(
r.namespace_members(Path::new("/p/shared.tex")),
&[
Path::new("/p/a.tex"),
Path::new("/p/b.tex"),
Path::new("/p/main.tex"),
Path::new("/p/shared.tex"),
]
);
}
#[test]
fn namespace_members_isolates_independent_documents() {
let g = graph(&[("/p/one.tex", &[]), ("/p/two.tex", &[])]);
let r = ResolvedLabels::build(
&[
(PathBuf::from("/p/one.tex"), names(&["x"]), true),
(PathBuf::from("/p/two.tex"), names(&["x"]), true),
],
&g,
);
assert_eq!(
r.namespace_members(Path::new("/p/one.tex")),
&[Path::new("/p/one.tex")]
);
assert!(r.namespace_members(Path::new("/p/missing.tex")).is_empty());
}
#[test]
fn independent_documents_do_not_share_labels() {
let g = graph(&[("/p/one.tex", &[]), ("/p/two.tex", &[])]);
let r = ResolvedLabels::build(
&[
(PathBuf::from("/p/one.tex"), names(&["intro"]), true),
(PathBuf::from("/p/two.tex"), names(&["intro"]), true),
],
&g,
);
assert_eq!(
r.definers(Path::new("/p/one.tex"), "intro"),
&[PathBuf::from("/p/one.tex")]
);
assert_eq!(
r.definers(Path::new("/p/two.tex"), "intro"),
&[PathBuf::from("/p/two.tex")]
);
}
#[test]
fn cycle_is_one_component() {
let g = graph(&[
("/p/a.tex", &[(IncludeKind::Input, "/p/b.tex")]),
("/p/b.tex", &[(IncludeKind::Input, "/p/a.tex")]),
]);
let r = ResolvedLabels::build(
&[
(PathBuf::from("/p/a.tex"), names(&["x"]), false),
(PathBuf::from("/p/b.tex"), names(&[]), false),
],
&g,
);
assert!(r.is_defined(Path::new("/p/b.tex"), "x"));
}
#[test]
fn dynamic_include_opens_the_component() {
let g = {
let facts = vec![FileFacts {
path: PathBuf::from("/p/main.tex"),
include_edges: vec![IncludeEdgeKey {
kind: IncludeKind::Input,
target: IncludeTarget::Dynamic,
}],
}];
IncludeGraph::build(&facts, None)
};
let r = ResolvedLabels::build(&[(PathBuf::from("/p/main.tex"), names(&[]), true)], &g);
assert!(!r.is_closed(Path::new("/p/main.tex")));
}
#[test]
fn external_include_opens_the_component() {
let g = graph(&[("/p/main.tex", &[(IncludeKind::Input, "/p/missing.tex")])]);
let r = ResolvedLabels::build(&[(PathBuf::from("/p/main.tex"), names(&[]), true)], &g);
assert!(!r.is_closed(Path::new("/p/main.tex")));
}
#[test]
fn rootless_component_reports_no_root() {
let g = graph(&[("/p/frag.tex", &[])]);
let r = ResolvedLabels::build(&[(PathBuf::from("/p/frag.tex"), names(&["x"]), false)], &g);
assert!(!r.is_root_component(Path::new("/p/frag.tex")));
assert!(r.is_closed(Path::new("/p/frag.tex")));
}
}