use perl_semantic_analyzer::symbol::{ScopeId, ScopeKind, SymbolTable};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ScopeDistance {
Immediate,
Parent,
PackageLevel,
Workspace,
}
impl ScopeDistance {
pub fn sort_key(self) -> char {
match self {
Self::Immediate => 'a',
Self::Parent => 'b',
Self::PackageLevel => 'c',
Self::Workspace => 'd',
}
}
}
fn last_unmatched_open_brace(source: &str) -> Option<usize> {
let mut stack = Vec::new();
for (idx, ch) in source.char_indices() {
match ch {
'{' => stack.push(idx),
'}' => {
stack.pop();
}
_ => {}
}
}
stack.pop()
}
fn scope_depth(symbol_table: &SymbolTable, scope_id: ScopeId) -> usize {
let mut depth = 0usize;
let mut current = scope_id;
while let Some(scope) = symbol_table.scopes.get(¤t) {
let Some(parent) = scope.parent else {
break;
};
depth += 1;
current = parent;
}
depth
}
pub fn scope_at_position(symbol_table: &SymbolTable, source: &str, position: usize) -> ScopeId {
let mut containing: Option<(ScopeId, usize, usize)> = None;
for scope in symbol_table.scopes.values() {
if scope.location.start <= position && position <= scope.location.end {
let depth = scope_depth(symbol_table, scope.id);
let is_better_containing = containing.is_none_or(|(_, best_start, best_depth)| {
scope.location.start > best_start
|| (scope.location.start == best_start && depth > best_depth)
});
if is_better_containing {
containing = Some((scope.id, scope.location.start, depth));
}
}
}
if position == source.len()
&& let Some(brace_pos) = last_unmatched_open_brace(source)
{
let mut eof_hint = containing;
for scope in symbol_table.scopes.values() {
if scope.location.start <= brace_pos {
let depth = scope_depth(symbol_table, scope.id);
let is_better_hint = eof_hint.is_none_or(|(_, best_start, best_depth)| {
scope.location.start > best_start
|| (scope.location.start == best_start && depth > best_depth)
});
if is_better_hint {
eof_hint = Some((scope.id, scope.location.start, depth));
}
}
}
return eof_hint.map_or(0, |(id, _, _)| id);
}
containing.map_or(0, |(id, _, _)| id)
}
pub fn compute_scope_distance(
symbol_table: &SymbolTable,
cursor_scope: ScopeId,
symbol_scope: ScopeId,
) -> ScopeDistance {
if cursor_scope == symbol_scope {
return ScopeDistance::Immediate;
}
let mut current = cursor_scope;
let mut hops = 0u32;
while let Some(scope) = symbol_table.scopes.get(¤t) {
if let Some(parent_id) = scope.parent {
hops += 1;
if parent_id == symbol_scope {
if let Some(parent_scope) = symbol_table.scopes.get(&parent_id)
&& matches!(parent_scope.kind, ScopeKind::Global | ScopeKind::Package)
{
return ScopeDistance::PackageLevel;
}
return ScopeDistance::Parent;
}
current = parent_id;
} else {
break;
}
if hops > 100 {
break;
}
}
if let Some(sym_scope) = symbol_table.scopes.get(&symbol_scope)
&& matches!(sym_scope.kind, ScopeKind::Global | ScopeKind::Package)
{
return ScopeDistance::PackageLevel;
}
ScopeDistance::Workspace
}
#[cfg(test)]
mod tests {
use super::*;
use perl_parser_core::SourceLocation;
use perl_semantic_analyzer::symbol::{Scope, ScopeKind, SymbolTable};
use std::collections::HashSet;
fn build_test_table() -> SymbolTable {
let mut table = SymbolTable::new();
table.scopes.insert(
0,
Scope {
id: 0,
parent: None,
kind: ScopeKind::Global,
location: SourceLocation { start: 0, end: 100 },
symbols: HashSet::new(),
},
);
table.scopes.insert(
1,
Scope {
id: 1,
parent: Some(0),
kind: ScopeKind::Package,
location: SourceLocation { start: 5, end: 95 },
symbols: HashSet::new(),
},
);
table.scopes.insert(
2,
Scope {
id: 2,
parent: Some(1),
kind: ScopeKind::Subroutine,
location: SourceLocation { start: 10, end: 80 },
symbols: HashSet::new(),
},
);
table.scopes.insert(
3,
Scope {
id: 3,
parent: Some(2),
kind: ScopeKind::Block,
location: SourceLocation { start: 20, end: 50 },
symbols: HashSet::new(),
},
);
table
}
#[test]
fn test_scope_at_position_innermost_block() {
let table = build_test_table();
assert_eq!(scope_at_position(&table, "", 30), 3);
}
#[test]
fn test_scope_at_position_subroutine() {
let table = build_test_table();
assert_eq!(scope_at_position(&table, "", 60), 2);
}
#[test]
fn test_scope_at_position_global() {
let table = build_test_table();
assert_eq!(scope_at_position(&table, "", 98), 0);
}
#[test]
fn test_scope_at_position_uses_eof_unmatched_brace_hint() {
let mut table = build_test_table();
if let Some(scope) = table.scopes.get_mut(&3) {
scope.location.end = 25;
}
let source = "sub process {\n if (1) {\n $";
assert_eq!(scope_at_position(&table, source, source.len()), 3);
}
#[test]
fn test_scope_at_position_does_not_revive_closed_child_block() {
let mut table = build_test_table();
if let Some(scope) = table.scopes.get_mut(&3) {
scope.location.end = 25;
}
let source = "sub process {\n if (1) {\n }\n $";
assert_eq!(scope_at_position(&table, source, source.len()), 2);
}
#[test]
fn test_scope_at_position_prefers_deeper_scope_when_ranges_match() {
let mut table = SymbolTable::new();
table.scopes.insert(
0,
Scope {
id: 0,
parent: None,
kind: ScopeKind::Global,
location: SourceLocation { start: 0, end: 100 },
symbols: HashSet::new(),
},
);
table.scopes.insert(
1,
Scope {
id: 1,
parent: Some(0),
kind: ScopeKind::Block,
location: SourceLocation { start: 10, end: 90 },
symbols: HashSet::new(),
},
);
table.scopes.insert(
2,
Scope {
id: 2,
parent: Some(1),
kind: ScopeKind::Block,
location: SourceLocation { start: 10, end: 90 },
symbols: HashSet::new(),
},
);
assert_eq!(scope_at_position(&table, "", 50), 2);
}
#[test]
fn test_scope_distance_immediate() {
let table = build_test_table();
assert_eq!(compute_scope_distance(&table, 3, 3), ScopeDistance::Immediate);
}
#[test]
fn test_scope_distance_parent_subroutine() {
let table = build_test_table();
assert_eq!(compute_scope_distance(&table, 3, 2), ScopeDistance::Parent);
}
#[test]
fn test_scope_distance_package_level() {
let table = build_test_table();
assert_eq!(compute_scope_distance(&table, 3, 1), ScopeDistance::PackageLevel);
}
#[test]
fn test_scope_distance_global_level() {
let table = build_test_table();
assert_eq!(compute_scope_distance(&table, 3, 0), ScopeDistance::PackageLevel);
}
#[test]
fn test_scope_distance_different_branch() {
let mut table = build_test_table();
table.scopes.insert(
4,
Scope {
id: 4,
parent: Some(1),
kind: ScopeKind::Subroutine,
location: SourceLocation { start: 82, end: 94 },
symbols: HashSet::new(),
},
);
assert_eq!(compute_scope_distance(&table, 3, 4), ScopeDistance::Workspace);
}
#[test]
fn test_sort_key_ordering() {
assert!(ScopeDistance::Immediate.sort_key() < ScopeDistance::Parent.sort_key());
assert!(ScopeDistance::Parent.sort_key() < ScopeDistance::PackageLevel.sort_key());
assert!(ScopeDistance::PackageLevel.sort_key() < ScopeDistance::Workspace.sort_key());
}
}