use std::collections::BTreeMap;
use cstree::syntax::SyntaxNode;
use omena_abstract_value::AbstractCssValueV0;
use omena_parser::{ParsedSassSymbolFactKind, StyleDialect, collect_style_facts, parse};
use omena_syntax::SyntaxKind;
use super::analysis_model::ScssGlobalVariableDeclaration;
use super::call_resolution::canonical_scss_callable_name;
use super::header_values::static_scss_header_abstract_value;
use super::symbol_candidates::cst_variable_declaration_parts;
use super::variables::canonical_scss_variable_name;
pub(super) fn collect_lexical_scss_bindings(
source: &str,
dialect: StyleDialect,
) -> LexicalScssBindings {
let parsed = parse(source, dialect);
let syntax = parsed.syntax();
let Some(scopes) = collect_lexical_scss_scopes_from_cst(source.len(), &syntax) else {
return LexicalScssBindings::new(Vec::new());
};
let facts = collect_style_facts(source, dialect);
let mut bindings = LexicalScssBindings::new(scopes);
for symbol in &facts.sass_symbols {
match symbol.kind {
ParsedSassSymbolFactKind::FunctionDeclaration => bindings.push_callable(
LexicalScssCallableKind::Function,
symbol.name.as_str(),
symbol.range.start().into(),
),
ParsedSassSymbolFactKind::MixinDeclaration => bindings.push_callable(
LexicalScssCallableKind::Mixin,
symbol.name.as_str(),
symbol.range.start().into(),
),
ParsedSassSymbolFactKind::FunctionCall
| ParsedSassSymbolFactKind::MixinInclude
| ParsedSassSymbolFactKind::VariableDeclaration
| ParsedSassSymbolFactKind::VariableReference => {}
}
}
for declaration in cst_scss_variable_declaration_nodes(&syntax) {
let Some((name, declaration_start, _, value_text)) =
cst_variable_declaration_parts(source, declaration)
else {
continue;
};
if value_text.is_empty() {
continue;
}
let Some(scope_id) = lexical_scss_scope_for_position(&bindings.scopes, declaration_start)
else {
continue;
};
bindings.push(
name.as_str(),
declaration_start,
scope_id,
static_scss_header_abstract_value(value_text.as_str()),
);
}
bindings
}
pub(super) fn collect_scss_global_variable_declarations(
source: &str,
dialect: StyleDialect,
) -> Vec<ScssGlobalVariableDeclaration> {
let parsed = parse(source, dialect);
let syntax = parsed.syntax();
let Some(scopes) = collect_lexical_scss_scopes_from_cst(source.len(), &syntax) else {
return Vec::new();
};
cst_scss_variable_declaration_nodes(&syntax)
.filter_map(|declaration| {
let (name, declaration_start, _, _) =
cst_variable_declaration_parts(source, declaration)?;
let scope_id = lexical_scss_scope_for_position(&scopes, declaration_start)?;
(scope_id == 0).then(|| ScssGlobalVariableDeclaration {
name: canonical_scss_variable_name(name.as_str()),
declaration_start,
})
})
.collect()
}
pub(super) fn scss_global_variable_metadata_exists(
name: &str,
position: usize,
declarations: &[ScssGlobalVariableDeclaration],
) -> Option<bool> {
let canonical_name = canonical_scss_variable_name(name);
if declarations.iter().any(|declaration| {
declaration.name == canonical_name && declaration.declaration_start <= position
}) {
return Some(true);
}
if declarations.iter().any(|declaration| {
declaration.name == canonical_name && declaration.declaration_start > position
}) {
return None;
}
Some(false)
}
pub(super) fn static_scss_metadata_exists_call_may_need_resolution(value: &str) -> bool {
const NAMES: [&str; 8] = [
"meta.function-exists(",
"function-exists(",
"meta.mixin-exists(",
"mixin-exists(",
"meta.variable-exists(",
"variable-exists(",
"meta.global-variable-exists(",
"global-variable-exists(",
];
let lower = value.to_ascii_lowercase();
NAMES.iter().any(|name| lower.contains(name))
}
fn collect_lexical_scss_scopes_from_cst(
source_len: usize,
root: &SyntaxNode<SyntaxKind>,
) -> Option<Vec<LexicalScssScope>> {
let mut scopes = vec![LexicalScssScope {
parent_id: None,
body_start: 0,
end: source_len,
}];
let mut stack = vec![0usize];
for token in root
.descendants_with_tokens()
.filter_map(|element| element.into_token())
{
match token.kind() {
SyntaxKind::LeftBrace | SyntaxKind::SassIndent => {
let parent_id = *stack.last()?;
let scope_id = scopes.len();
scopes.push(LexicalScssScope {
parent_id: Some(parent_id),
body_start: u32::from(token.text_range().end()) as usize,
end: source_len,
});
stack.push(scope_id);
}
SyntaxKind::RightBrace | SyntaxKind::SassDedent => {
let scope_id = stack.pop()?;
if scope_id == 0 {
return None;
}
scopes.get_mut(scope_id)?.end = u32::from(token.text_range().start()) as usize;
}
_ => {}
}
}
(stack.len() == 1).then_some(scopes)
}
fn cst_scss_variable_declaration_nodes(
root: &SyntaxNode<SyntaxKind>,
) -> impl Iterator<Item = &SyntaxNode<SyntaxKind>> {
root.descendants()
.filter(|node| node.kind() == SyntaxKind::ScssVariableDeclaration)
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(super) struct LexicalScssBindings {
bindings: Vec<LexicalScssBinding>,
callables: Vec<LexicalScssCallableDeclaration>,
scopes: Vec<LexicalScssScope>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LexicalScssBinding {
name: String,
declaration_start: usize,
scope_id: usize,
value: AbstractCssValueV0,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LexicalScssCallableKind {
Function,
Mixin,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LexicalScssCallableDeclaration {
kind: LexicalScssCallableKind,
name: String,
declaration_start: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LexicalScssScope {
parent_id: Option<usize>,
body_start: usize,
end: usize,
}
impl LexicalScssBindings {
fn new(scopes: Vec<LexicalScssScope>) -> Self {
Self {
bindings: Vec::new(),
callables: Vec::new(),
scopes,
}
}
fn push(
&mut self,
name: &str,
declaration_start: usize,
scope_id: usize,
value: AbstractCssValueV0,
) {
self.bindings.push(LexicalScssBinding {
name: canonical_scss_variable_name(name),
declaration_start,
scope_id,
value,
});
}
pub(super) fn push_root_binding(&mut self, name: &str, value: AbstractCssValueV0) {
self.push(name, 0, 0, value);
}
fn push_callable(
&mut self,
kind: LexicalScssCallableKind,
name: &str,
declaration_start: usize,
) {
self.callables.push(LexicalScssCallableDeclaration {
kind,
name: canonical_scss_callable_name(name),
declaration_start,
});
}
pub(super) fn visible_function_metadata_exists(
&self,
name: &str,
position: usize,
) -> Option<bool> {
self.visible_callable_metadata_exists(LexicalScssCallableKind::Function, name, position)
}
pub(super) fn visible_mixin_metadata_exists(
&self,
name: &str,
position: usize,
) -> Option<bool> {
self.visible_callable_metadata_exists(LexicalScssCallableKind::Mixin, name, position)
}
fn visible_callable_metadata_exists(
&self,
kind: LexicalScssCallableKind,
name: &str,
position: usize,
) -> Option<bool> {
let canonical_name = canonical_scss_callable_name(name);
self.callables
.iter()
.any(|callable| {
callable.kind == kind
&& callable.name == canonical_name
&& callable.declaration_start <= position
})
.then_some(true)
}
pub(super) fn visible_at(&self, position: usize) -> BTreeMap<String, AbstractCssValueV0> {
let Some(scope_id) = lexical_scss_scope_for_position(&self.scopes, position) else {
return BTreeMap::new();
};
let mut visible = BTreeMap::new();
for binding in self.bindings.iter() {
if binding.declaration_start > position {
continue;
}
if lexical_scss_scope_is_ancestor_or_self(&self.scopes, binding.scope_id, scope_id) {
visible.insert(binding.name.clone(), binding.value.clone());
} else {
visible.insert(binding.name.clone(), AbstractCssValueV0::Top);
}
}
visible
}
pub(super) fn visible_variable_metadata_exists(
&self,
name: &str,
position: usize,
) -> Option<bool> {
let canonical_name = canonical_scss_variable_name(name);
let scope_id = lexical_scss_scope_for_position(&self.scopes, position)?;
if self.bindings.iter().any(|binding| {
binding.name == canonical_name
&& binding.declaration_start <= position
&& lexical_scss_scope_is_ancestor_or_self(&self.scopes, binding.scope_id, scope_id)
}) {
return Some(true);
}
if self.bindings.iter().any(|binding| {
binding.name == canonical_name
&& binding.declaration_start > position
&& lexical_scss_scope_is_ancestor_or_self(&self.scopes, binding.scope_id, scope_id)
}) {
return None;
}
Some(false)
}
pub(super) fn global_variable_metadata_exists(
&self,
name: &str,
position: usize,
) -> Option<bool> {
let canonical_name = canonical_scss_variable_name(name);
if self.bindings.iter().any(|binding| {
binding.name == canonical_name
&& binding.scope_id == 0
&& binding.declaration_start <= position
}) {
return Some(true);
}
if self.bindings.iter().any(|binding| {
binding.name == canonical_name
&& binding.scope_id == 0
&& binding.declaration_start > position
}) {
return None;
}
Some(false)
}
}
fn lexical_scss_scope_for_position(scopes: &[LexicalScssScope], position: usize) -> Option<usize> {
scopes
.iter()
.enumerate()
.rev()
.find_map(|(scope_id, scope)| {
(position >= scope.body_start && position < scope.end).then_some(scope_id)
})
}
fn lexical_scss_scope_is_ancestor_or_self(
scopes: &[LexicalScssScope],
ancestor_id: usize,
mut scope_id: usize,
) -> bool {
loop {
if scope_id == ancestor_id {
return true;
}
let Some(parent_id) = scopes.get(scope_id).and_then(|scope| scope.parent_id) else {
return false;
};
scope_id = parent_id;
}
}