Skip to main content

bbnf_analysis/state/
pretty.rs

1//! `@pretty` directive extraction, validation, and semantic token generation.
2//!
3//! Compartmentalised module so all `@pretty` LSP logic lives in one place.
4
5use std::collections::{HashMap, HashSet};
6
7use bbnf::generate::prettify::hints::{self, closest_hint, is_valid_hint};
8use ls_types::*;
9
10use crate::analysis::LineIndex;
11
12use super::types::{SemanticTokenInfo, token_types};
13
14/// Owned `@pretty` directive info (no lifetimes).
15#[derive(Debug, Clone)]
16pub struct PrettyInfo {
17    /// The targeted rule name.
18    pub rule_name: String,
19    /// The formatting hint keywords.
20    pub hints: Vec<String>,
21    /// Byte offset range of the entire directive.
22    pub span: (usize, usize),
23    /// Byte offset range of the rule name within the directive.
24    pub rule_name_span: (usize, usize),
25    /// Byte offset ranges of each hint keyword.
26    pub hint_spans: Vec<(usize, usize)>,
27}
28
29/// Extract `PrettyInfo` from a parsed grammar.
30///
31/// Uses the source text + directive span to locate sub-spans for the rule
32/// name and each hint keyword.
33pub fn extract_pretties(
34    pretties: &[bbnf::types::PrettyDirective<'_>],
35    _src: &str,
36) -> Vec<PrettyInfo> {
37    pretties
38        .iter()
39        .map(|p| {
40            let dir_src = p.span.as_str();
41            let dir_start = p.span.start;
42
43            // Find the rule name span within the directive source.
44            let rule_name = p.rule_name.as_ref();
45            let rule_name_offset = dir_src
46                .find(rule_name)
47                .map(|off| off + dir_start)
48                .unwrap_or_else(|| {
49                    panic!(
50                        "could not resolve @pretty rule-name span for `{}` within directive `{}`",
51                        rule_name, dir_src
52                    )
53                });
54            let rule_name_span = (rule_name_offset, rule_name_offset + rule_name.len());
55
56            // Find each hint span by searching after the rule name.
57            let mut search_start = rule_name_offset + rule_name.len() - dir_start;
58            let hint_spans: Vec<(usize, usize)> = p
59                .hints
60                .iter()
61                .map(|hint| {
62                    let hint_str = hint.as_ref();
63                    let offset = dir_src[search_start..]
64                        .find(hint_str)
65                        .map(|off| off + search_start + dir_start)
66                        .unwrap_or_else(|| {
67                            panic!(
68                                "could not resolve @pretty hint span for `{}` within directive `{}`",
69                                hint_str, dir_src
70                            )
71                        });
72                    search_start = offset - dir_start + hint_str.len();
73                    (offset, offset + hint_str.len())
74                })
75                .collect();
76
77            PrettyInfo {
78                rule_name: rule_name.to_string(),
79                hints: p.hints.iter().map(|h| h.to_string()).collect(),
80                span: (p.span.start, p.span.end),
81                rule_name_span,
82                hint_spans,
83            }
84        })
85        .collect()
86}
87
88/// Validate `@pretty` directives and produce diagnostics + semantic tokens.
89///
90/// Returns `(diagnostics, semantic_tokens, referenced_rule_names)`.
91pub fn validate_pretties(
92    pretties: &[PrettyInfo],
93    defined: &HashMap<&str, usize>,
94    imported_names: &HashSet<&str>,
95    line_index: &LineIndex,
96) -> (Vec<Diagnostic>, Vec<SemanticTokenInfo>) {
97    let mut diagnostics = Vec::new();
98    let mut semantic_tokens = Vec::new();
99
100    for pretty in pretties {
101        // Semantic token: KEYWORD for "@pretty".
102        // "@pretty" is 7 chars, starts at the directive span start.
103        semantic_tokens.push(SemanticTokenInfo {
104            span: (pretty.span.0, pretty.span.0 + 7),
105            token_type: token_types::KEYWORD,
106        });
107
108        // Semantic token: RULE_REFERENCE for the rule name.
109        semantic_tokens.push(SemanticTokenInfo {
110            span: pretty.rule_name_span,
111            token_type: token_types::RULE_REFERENCE,
112        });
113
114        // Special case: `@pretty * <mode>` — meta-directive, skip rule validation.
115        if pretty.rule_name == "*" {
116            // Validate the mode keyword.
117            for (i, hint) in pretty.hints.iter().enumerate() {
118                let valid_modes = ["auto", "minimal", "off"];
119                if !valid_modes.contains(&hint.as_str()) {
120                    let span = pretty
121                        .hint_spans
122                        .get(i)
123                        .copied()
124                        .unwrap_or_else(|| {
125                            panic!(
126                                "missing @pretty mode hint span for `{}` at index {}",
127                                hint, i
128                            )
129                        });
130                    diagnostics.push(Diagnostic {
131                        range: line_index.span_to_range(span.0, span.1),
132                        severity: Some(DiagnosticSeverity::WARNING),
133                        source: Some("bbnf".into()),
134                        message: format!(
135                            "Unknown heuristic mode `{}`. Valid modes: auto, minimal, off",
136                            hint
137                        ),
138                        ..Default::default()
139                    });
140                }
141                // Semantic token for mode keyword.
142                if let Some(&span) = pretty.hint_spans.get(i) {
143                    semantic_tokens.push(SemanticTokenInfo {
144                        span,
145                        token_type: token_types::KEYWORD,
146                    });
147                }
148            }
149            continue;
150        }
151
152        // Validate: warn if the target rule doesn't exist.
153        if !defined.contains_key(pretty.rule_name.as_str())
154            && !imported_names.contains(pretty.rule_name.as_str())
155        {
156            diagnostics.push(Diagnostic {
157                range: line_index.span_to_range(
158                    pretty.rule_name_span.0,
159                    pretty.rule_name_span.1,
160                ),
161                severity: Some(DiagnosticSeverity::WARNING),
162                source: Some("bbnf".into()),
163                message: format!(
164                    "`@pretty` targets undefined rule: `{}`",
165                    pretty.rule_name
166                ),
167                ..Default::default()
168            });
169        }
170
171        // Validate each hint keyword.
172        for (i, hint) in pretty.hints.iter().enumerate() {
173            let span = pretty
174                .hint_spans
175                .get(i)
176                .copied()
177                .unwrap_or_else(|| {
178                    panic!(
179                        "missing @pretty hint span for `{}` at index {}",
180                        hint, i
181                    )
182                });
183
184            if !is_valid_hint(hint) {
185                let mut msg = format!("Unknown `@pretty` hint: `{}`", hint);
186                if let Some(suggestion) = closest_hint(hint) {
187                    msg.push_str(&format!(". Did you mean `{}`?", suggestion));
188                } else {
189                    let valid = hints::valid_hint_names();
190                    msg.push_str(&format!(". Valid hints: {}", valid.join(", ")));
191                }
192                diagnostics.push(Diagnostic {
193                    range: line_index.span_to_range(span.0, span.1),
194                    severity: Some(DiagnosticSeverity::WARNING),
195                    source: Some("bbnf".into()),
196                    message: msg,
197                    ..Default::default()
198                });
199            }
200
201            // Semantic token for each hint keyword.
202            semantic_tokens.push(SemanticTokenInfo {
203                span,
204                token_type: token_types::KEYWORD,
205            });
206        }
207    }
208
209    (diagnostics, semantic_tokens)
210}