use std::collections::HashMap;
use std::fmt;
use std::hash::Hash;
use std::sync::{Mutex, MutexGuard};
use serde::{Deserialize, Serialize};
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)]
pub(crate) struct ScopeRepository {
pub(crate) atoms: Vec<String>,
atom_index_map: HashMap<String, usize>,
}
impl ScopeRepository {
fn new() -> Self {
Self::default()
}
#[cfg(feature = "dump")]
pub fn from_atoms(atoms: Vec<String>) -> Self {
let mut atom_index_map = HashMap::with_capacity(atoms.len());
for (index, atom) in atoms.iter().enumerate() {
atom_index_map.insert(atom.clone(), index);
}
Self {
atoms,
atom_index_map,
}
}
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::OnceLock<Mutex<ScopeRepository>> = std::sync::OnceLock::new();
pub(crate) fn lock_global_scope_repo() -> MutexGuard<'static, ScopeRepository> {
SCOPE_REPO
.get_or_init(|| Mutex::new(ScopeRepository::new()))
.lock()
.expect("Failed to lock scope repository")
}
#[cfg(feature = "dump")]
pub(crate) fn replace_global_scope_repo(new_repo: ScopeRepository) {
let _ = SCOPE_REPO.set(Mutex::new(new_repo));
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default, Hash, Serialize, Deserialize,
)]
pub(crate) struct ScopeListId(u32);
pub(crate) const EMPTY_SCOPE_LIST: ScopeListId = ScopeListId(0);
impl ScopeListId {
#[inline]
pub(crate) fn as_index(self) -> usize {
self.0 as usize
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ScopeListNode {
parent: ScopeListId,
scope: Scope,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub(crate) struct ScopeInterner {
nodes: Vec<ScopeListNode>,
lookup: HashMap<(ScopeListId, Scope), ScopeListId>,
}
impl ScopeInterner {
pub(crate) fn num_nodes(&self) -> usize {
self.nodes.len() + 1
}
pub(crate) fn push(&mut self, parent: ScopeListId, scope: Scope) -> ScopeListId {
*self.lookup.entry((parent, scope)).or_insert_with(|| {
let node = ScopeListNode { parent, scope };
self.nodes.push(node);
ScopeListId(self.nodes.len() as u32)
})
}
pub(crate) fn extend(&mut self, parent: ScopeListId, scopes: &[Scope]) -> ScopeListId {
let mut current = parent;
for scope in scopes {
current = self.push(current, *scope);
}
current
}
pub(crate) fn get_scopes(&self, id: ScopeListId) -> Vec<Scope> {
let mut out = vec![];
let mut current = id;
while current != EMPTY_SCOPE_LIST {
let node = &self.nodes[current.as_index() - 1];
out.push(node.scope);
current = node.parent;
}
out.reverse();
out
}
}
#[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.");
}
#[test]
fn test_interner() {
let mut interner = ScopeInterner::default();
let id1 = interner.push(EMPTY_SCOPE_LIST, Scope::new("source.rust")[0]);
let id2 = interner.push(id1, Scope::new("comment")[0]);
let id3 = interner.push(id1, Scope::new("comment")[0]);
assert_eq!(id2, id3);
assert_eq!(interner.nodes.len(), 2);
let id4 = interner.extend(id2, &[Scope::new("variable")[0], Scope::new("other")[0]]);
let res = interner.get_scopes(id2);
assert_eq!(
res,
vec![Scope::new("source.rust")[0], Scope::new("comment")[0]]
);
let res = interner.get_scopes(id4);
assert_eq!(
res,
vec![
Scope::new("source.rust")[0],
Scope::new("comment")[0],
Scope::new("variable")[0],
Scope::new("other")[0]
]
);
}
}