use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::{Mutex, MutexGuard};
pub const MAX_ATOMS_IN_SCOPE: usize = 8;
pub const MAX_ATOMS_IN_REPOSITORY: usize = u16::MAX as usize - 2;
pub const EMPTY_ATOM_INDEX: usize = u16::MAX as usize - 1;
pub const EMPTY_ATOM_NUMBER: u16 = u16::MAX;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default, Hash, Serialize, Deserialize)]
pub struct Scope {
atoms: u128,
}
impl Scope {
pub fn new(scope_str: &str) -> Vec<Scope> {
let mut repo = lock_global_scope_repo();
scope_str
.split_whitespace()
.map(|part| repo.parse(part.trim()))
.collect()
}
#[inline]
pub fn atom_at(self, index: usize) -> u16 {
debug_assert!(index < MAX_ATOMS_IN_SCOPE);
let shift = (MAX_ATOMS_IN_SCOPE - 1 - index) * 16;
((self.atoms >> shift) & 0xFFFF) as u16
}
#[inline]
pub fn len(self) -> u32 {
MAX_ATOMS_IN_SCOPE as u32 - self.missing_atoms()
}
#[inline]
pub fn is_empty(self) -> bool {
self.atoms == 0
}
#[inline]
fn missing_atoms(self) -> u32 {
self.atoms.trailing_zeros() / 16
}
#[inline]
pub fn is_prefix_of(self, other: Scope) -> bool {
let missing = self.missing_atoms();
if missing == MAX_ATOMS_IN_SCOPE as u32 {
return true; }
let mask = u128::MAX << (missing * 16);
(self.atoms ^ other.atoms) & mask == 0
}
pub fn build_string(self) -> String {
let repo = lock_global_scope_repo();
repo.to_string(self)
}
}
impl fmt::Debug for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Scope(\"{}\")", self.build_string())
}
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.build_string())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct ScopeRepository {
atoms: Vec<String>,
atom_index_map: HashMap<String, usize>,
}
impl ScopeRepository {
fn new() -> Self {
Self::default()
}
fn atom_to_index(&mut self, atom: &str) -> usize {
if atom.is_empty() {
return EMPTY_ATOM_INDEX;
}
if let Some(&index) = self.atom_index_map.get(atom) {
return index;
}
if self.atoms.len() >= MAX_ATOMS_IN_REPOSITORY {
panic!(
"Too many atoms in repository: exceeded MAX_ATOMS_IN_REPOSITORY of {MAX_ATOMS_IN_REPOSITORY}"
);
}
let index = self.atoms.len();
self.atoms.push(atom.to_owned());
self.atom_index_map.insert(atom.to_owned(), index);
index
}
pub(crate) fn atom_number_to_str(&self, atom_number: u16) -> &str {
debug_assert!(atom_number > 0);
&self.atoms[(atom_number - 1) as usize]
}
fn parse(&mut self, scope_str: &str) -> Scope {
if scope_str.is_empty() {
return Scope::default();
}
let parts: Vec<&str> = scope_str.split('.').collect();
let atoms_to_process = parts.len().min(MAX_ATOMS_IN_SCOPE); let mut atoms = 0u128;
for (i, &part) in parts.iter().take(atoms_to_process).enumerate() {
let index = self.atom_to_index(part); let atom_number = (index + 1) as u128;
let shift = (MAX_ATOMS_IN_SCOPE - 1 - i) * 16;
atoms |= atom_number << shift;
}
Scope { atoms }
}
fn to_string(&self, scope: Scope) -> String {
let mut parts = Vec::new();
for i in 0..MAX_ATOMS_IN_SCOPE {
match scope.atom_at(i) {
0 => break,
a if a == EMPTY_ATOM_NUMBER => {
parts.push("");
}
a => {
parts.push(self.atom_number_to_str(a));
}
}
}
parts.join(".")
}
}
static SCOPE_REPO: std::sync::LazyLock<Mutex<ScopeRepository>> =
std::sync::LazyLock::new(|| Mutex::new(ScopeRepository::new()));
pub(crate) fn lock_global_scope_repo() -> MutexGuard<'static, ScopeRepository> {
SCOPE_REPO.lock().expect("Failed to lock scope repository")
}
#[cfg(feature = "dump")]
pub(crate) fn replace_global_scope_repo(new_repo: ScopeRepository) {
let mut global_repo = lock_global_scope_repo();
*global_repo = new_repo;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_scope_creation() {
let scope = Scope::new("source.rust.meta.function")[0];
assert_eq!(scope.len(), 4);
assert_eq!(scope.build_string(), "source.rust.meta.function");
}
#[test]
fn test_empty_scope() {
let scope = Scope::new("");
assert_eq!(scope.len(), 0);
assert!(scope.is_empty());
}
#[test]
fn test_prefix_matching() {
let prefix = Scope::new("source.rust")[0];
let full = Scope::new("source.rust.meta.function")[0];
let different = Scope::new("source.javascript")[0];
assert!(prefix.is_prefix_of(full));
assert!(prefix.is_prefix_of(prefix));
assert!(!prefix.is_prefix_of(different));
}
#[test]
fn test_atom_truncation() {
let long_scope = Scope::new("a.b.c.d.e.f.g.h.i.j.k.l")[0];
assert_eq!(long_scope.len(), 8);
assert_eq!(long_scope.build_string(), "a.b.c.d.e.f.g.h");
}
#[test]
fn test_atom_extraction() {
let scope = Scope::new("source.rust.meta")[0];
assert_ne!(scope.atom_at(0), 0); assert_ne!(scope.atom_at(1), 0); assert_ne!(scope.atom_at(2), 0); assert_eq!(scope.atom_at(3), 0); assert_eq!(scope.atom_at(7), 0); }
#[test]
fn test_scope_ordering() {
let scope1 = Scope::new("source.rust")[0];
let scope2 = Scope::new("source.rust.meta")[0];
assert!(scope1 < scope2);
}
#[test]
fn test_scope_equality() {
let scope1 = Scope::new("source.rust.meta")[0];
let scope2 = Scope::new("source.rust.meta")[0];
let scope3 = Scope::new("source.rust")[0];
assert_eq!(scope1, scope2);
assert_ne!(scope1, scope3);
}
#[test]
fn test_empty_atom_preservation() {
let scope = Scope::new("meta.tag.object.svg..end.html")[0];
assert_eq!(scope.build_string(), "meta.tag.object.svg..end.html");
assert_eq!(scope.len(), 7);
}
#[test]
fn test_empty_atoms_various_positions() {
assert_eq!(Scope::new("a...b")[0].build_string(), "a...b");
assert_eq!(Scope::new(".start.end")[0].build_string(), ".start.end");
assert_eq!(Scope::new("start.end.")[0].build_string(), "start.end.");
}
}