Skip to main content

perl_lsp_completion/completion/
context.rs

1//! Completion context analysis
2
3use perl_semantic_analyzer::symbol::{ScopeId, ScopeKind, SymbolKind, SymbolTable};
4
5/// Context for completion
6#[derive(Debug, Clone)]
7pub struct CompletionContext {
8    /// The position where completion was triggered
9    pub position: usize,
10    /// The character that triggered completion (if any)
11    pub trigger_character: Option<char>,
12    /// Whether we're in a string literal
13    pub in_string: bool,
14    /// Whether we're in a regex
15    pub in_regex: bool,
16    /// Whether we're in a comment
17    pub in_comment: bool,
18    /// Whether we're completing a module name after `use` or `require`
19    pub in_use_statement: bool,
20    /// Current package context
21    pub current_package: String,
22    /// Prefix text before cursor
23    pub prefix: String,
24    /// Start position of the prefix (for text edit range calculation)
25    pub prefix_start: usize,
26    /// The innermost scope containing the cursor position
27    pub cursor_scope_id: ScopeId,
28}
29
30impl CompletionContext {
31    pub(crate) fn detect_current_package(symbol_table: &SymbolTable, position: usize) -> String {
32        // First, check for innermost package scope containing the position
33        let mut scope_start: Option<usize> = None;
34        for scope in symbol_table.scopes.values() {
35            if scope.kind == ScopeKind::Package
36                && scope.location.start <= position
37                && position <= scope.location.end
38                && scope_start.is_none_or(|s| scope.location.start >= s)
39            {
40                scope_start = Some(scope.location.start);
41            }
42        }
43
44        if let Some(start) = scope_start
45            && let Some(sym) = symbol_table
46                .symbols
47                .values()
48                .flat_map(|v| v.iter())
49                .find(|sym| sym.kind == SymbolKind::Package && sym.location.start == start)
50        {
51            return sym.name.clone();
52        }
53
54        // Fallback: find last package declaration without block before position
55        let mut current = "main".to_string();
56        let mut packages: Vec<&perl_semantic_analyzer::symbol::Symbol> = symbol_table
57            .symbols
58            .values()
59            .flat_map(|v| v.iter())
60            .filter(|sym| sym.kind == SymbolKind::Package)
61            .collect();
62        packages.sort_by_key(|sym| sym.location.start);
63        for sym in packages {
64            if sym.location.start <= position {
65                let has_scope = symbol_table.scopes.values().any(|sc| {
66                    sc.kind == ScopeKind::Package && sc.location.start == sym.location.start
67                });
68                if !has_scope {
69                    current = sym.name.clone();
70                }
71            } else {
72                break;
73            }
74        }
75        current
76    }
77
78    pub(crate) fn new(
79        symbol_table: &SymbolTable,
80        position: usize,
81        trigger_character: Option<char>,
82        in_string: bool,
83        in_regex: bool,
84        in_comment: bool,
85        prefix: String,
86        prefix_start: usize,
87    ) -> Self {
88        let current_package = Self::detect_current_package(symbol_table, position);
89        CompletionContext {
90            position,
91            trigger_character,
92            in_string,
93            in_regex,
94            in_comment,
95            in_use_statement: false,
96            current_package,
97            prefix,
98            prefix_start,
99            cursor_scope_id: 0,
100        }
101    }
102}