1use bbnf::generate::prettify::hints::{
2 extract_sep_string, extract_split_delim, hint_documentation,
3};
4use ls_types::*;
5
6use crate::analysis::{symbol_at_offset, SymbolAtOffset};
7use crate::state::DocumentState;
8
9pub fn hover(state: &DocumentState, position: Position) -> Option<Hover> {
10 let offset = state.line_index.position_to_offset(position);
11
12 if let Some(hover) = hover_no_collapse(state, offset) {
14 return Some(hover);
15 }
16 if let Some(hover) = hover_recover(state, offset) {
17 return Some(hover);
18 }
19 if let Some(hover) = hover_pretty(state, offset) {
20 return Some(hover);
21 }
22
23 let symbol = symbol_at_offset(&state.info, offset)?;
24
25 match symbol {
26 SymbolAtOffset::RuleDefinition(rule) => {
27 let ref_count: usize = state
28 .info
29 .rules
30 .iter()
31 .flat_map(|r| &r.references)
32 .filter(|r| r.name == rule.name)
33 .count();
34
35 let mut content = format!(
36 "```bbnf\n{} = {}\n```\n\n{} reference{}",
37 rule.name,
38 rule.rhs_text,
39 ref_count,
40 if ref_count == 1 { "" } else { "s" }
41 );
42
43 content.push_str("\n\n---\n");
45 if let Some(first_label) = state.info.first_set_labels.get(&rule.name) {
46 content.push_str(&format!("FIRST: {}\n\n", first_label));
47 }
48 let nullable = state.info.nullable_rules.contains(&rule.name);
49 content.push_str(&format!(
50 "Nullable: {}\n\n",
51 if nullable { "yes" } else { "no" }
52 ));
53 if let Some(cycle_path) = state.info.cyclic_rule_paths.get(&rule.name) {
54 content.push_str(&format!("Cyclic: yes ({})\n", cycle_path));
55 }
56
57 for p in &state.info.pretties {
59 if p.rule_name == rule.name {
60 content.push_str(&format!("\n@pretty: `{}`\n", p.hints.join(" ")));
61 break;
62 }
63 }
64
65 Some(Hover {
66 contents: HoverContents::Markup(MarkupContent {
67 kind: MarkupKind::Markdown,
68 value: content,
69 }),
70 range: Some(
71 state
72 .line_index
73 .span_to_range(rule.name_span.0, rule.name_span.1),
74 ),
75 })
76 }
77 SymbolAtOffset::RuleReference { name, .. } => {
78 let def = state
80 .info
81 .rule_index
82 .get(&name)
83 .map(|&i| &state.info.rules[i]);
84
85 let content = if let Some(def) = def {
86 let mut s = format!("```bbnf\n{} = {}\n```", def.name, def.rhs_text);
87 if let Some(first_label) = state.info.first_set_labels.get(&def.name) {
89 s.push_str(&format!("\n\n---\nFIRST: {}", first_label));
90 }
91 s
92 } else {
93 format!("`{}` — undefined rule", name)
94 };
95
96 Some(Hover {
97 contents: HoverContents::Markup(MarkupContent {
98 kind: MarkupKind::Markdown,
99 value: content,
100 }),
101 range: None,
102 })
103 }
104 }
105}
106
107fn hover_no_collapse(state: &DocumentState, offset: usize) -> Option<Hover> {
109 for nc in &state.info.no_collapses {
110 let kw_end = nc.span.0 + 13;
112 if offset >= nc.span.0 && offset <= kw_end {
113 let rule_def = state
115 .info
116 .rule_index
117 .get(&nc.rule_name)
118 .map(|&i| &state.info.rules[i]);
119
120 let mut content = String::from(
121 "### `@no_collapse` — Span Preservation\n\n\
122 Prevents the parser from merging consecutive spans into a single `Span` \
123 for rule `",
124 );
125 content.push_str(&nc.rule_name);
126 content.push_str("`.\n\n");
127
128 content.push_str(
129 "**Without** `@no_collapse`: repetitions (`*`, `+`) produce a single merged `Span`, \
130 and optionals (`?`) produce `Span` instead of `Option<Span>`.\n\n\
131 **With** `@no_collapse`: repetitions produce `Vec<Span>` and optionals produce \
132 `Option<Span>` — preserving individual element boundaries for formatting.\n\n",
133 );
134
135 content.push_str(
136 "Use when `@pretty` directives need to format each repeated element independently \
137 (e.g. with `sep(\"...\")` or `split(\"...\")`).\n",
138 );
139
140 if let Some(def) = rule_def {
141 content.push_str(&format!("\n---\n```bbnf\n{} = {}\n```", def.name, def.rhs_text));
142 }
143
144 return Some(Hover {
145 contents: HoverContents::Markup(MarkupContent {
146 kind: MarkupKind::Markdown,
147 value: content,
148 }),
149 range: Some(state.line_index.span_to_range(nc.span.0, kw_end)),
150 });
151 }
152 }
153 None
154}
155
156fn hover_recover(state: &DocumentState, offset: usize) -> Option<Hover> {
158 for rec in &state.info.recovers {
159 if offset >= rec.span.0 && offset <= rec.span.1 {
161 if offset >= rec.rule_name_span.0 && offset <= rec.rule_name_span.1 {
163 continue;
165 }
166
167 let rule_def = state
168 .info
169 .rule_index
170 .get(&rec.rule_name)
171 .map(|&i| &state.info.rules[i]);
172
173 let mut content = format!(
174 "### `@recover` directive — Error Recovery\n\n\
175 Wraps rule `{}` with error recovery. When parsing fails mid-rule, the parser:\n\n\
176 1. Records the error with position and expected tokens\n\
177 2. Skips forward to the **sync expression**\n\
178 3. Produces a `Recovered` sentinel node\n\
179 4. Continues parsing subsequent rules\n\n",
180 rec.rule_name
181 );
182
183 if !rec.sync_expr_text.is_empty() {
184 content.push_str(&format!(
185 "**Sync expression:** `{}`\n\n\
186 The parser advances input until this expression matches, then resumes \
187 normal parsing from that point.\n\n",
188 rec.sync_expr_text
189 ));
190 }
191
192 content.push_str(
193 "This enables **multi-error diagnostics** — the parser reports all errors \
194 in a single pass instead of stopping at the first failure.\n",
195 );
196
197 if let Some(def) = rule_def {
198 content.push_str(&format!(
199 "\n---\n```bbnf\n{} = {}\n```",
200 def.name, def.rhs_text
201 ));
202 }
203
204 return Some(Hover {
205 contents: HoverContents::Markup(MarkupContent {
206 kind: MarkupKind::Markdown,
207 value: content,
208 }),
209 range: Some(state.line_index.span_to_range(rec.span.0, rec.span.1)),
210 });
211 }
212 }
213 None
214}
215
216fn hover_pretty(state: &DocumentState, offset: usize) -> Option<Hover> {
218 for pretty in &state.info.pretties {
219 for (i, hint) in pretty.hints.iter().enumerate() {
221 if let Some(&(start, end)) = pretty.hint_spans.get(i) {
222 if offset >= start && offset <= end {
223 let content = build_hint_hover(hint, &pretty.rule_name, &pretty.hints);
224 return Some(Hover {
225 contents: HoverContents::Markup(MarkupContent {
226 kind: MarkupKind::Markdown,
227 value: content,
228 }),
229 range: Some(state.line_index.span_to_range(start, end)),
230 });
231 }
232 }
233 }
234
235 let kw_end = pretty.span.0 + 7;
237 if offset >= pretty.span.0 && offset < kw_end {
238 let content = build_pretty_directive_hover(state, pretty);
239 return Some(Hover {
240 contents: HoverContents::Markup(MarkupContent {
241 kind: MarkupKind::Markdown,
242 value: content,
243 }),
244 range: Some(state.line_index.span_to_range(pretty.span.0, kw_end)),
245 });
246 }
247
248 let (rs, re) = pretty.rule_name_span;
250 if offset >= rs && offset <= re {
251 let content = build_pretty_directive_hover(state, pretty);
252 return Some(Hover {
253 contents: HoverContents::Markup(MarkupContent {
254 kind: MarkupKind::Markdown,
255 value: content,
256 }),
257 range: Some(state.line_index.span_to_range(rs, re)),
258 });
259 }
260 }
261 None
262}
263
264fn build_hint_hover(hint: &str, rule_name: &str, all_hints: &[String]) -> String {
266 if let Some(sep_str) = extract_sep_string(hint) {
267 return format!(
268 "### `sep(\"{}\")` — Custom Separator\n\n\
269 Joins elements of `{}` with the separator `\"{}\"`.\n\n\
270 When combined with `group`: renders `\"{}\"` inline when the group fits, \
271 or `\"{}\"` + newline when the group breaks (trailing whitespace is trimmed \
272 on the break branch).\n\n\
273 Without `group`: uses `\"{}\"` as a flat separator between all elements.\n\n\
274 ```bbnf\n@pretty {} {} ;\n```",
275 sep_str, rule_name, sep_str,
276 sep_str, sep_str.trim_end(),
277 sep_str,
278 rule_name, all_hints.join(" ")
279 );
280 }
281
282 if let Some(delim) = extract_split_delim(hint) {
283 return format!(
284 "### `split(\"{}\")` — Format-Time Splitting\n\n\
285 Splits opaque `Span` text from `{}` on the delimiter `\"{}\"` at format time.\n\n\
286 The split is **depth-aware**: respects `()`, `[]` nesting and `\"\"`, `''` \
287 quoting — only top-level occurrences of the delimiter trigger a split.\n\n\
288 Each resulting segment becomes a separate Doc element, which can then be \
289 joined with `sep(\"...\")` or formatted with `group`/`indent`.\n\n\
290 Uses `memchr` fast-path: skips the full scan when the delimiter isn't present.\n\n\
291 ```bbnf\n@pretty {} {} ;\n```",
292 delim, rule_name, delim,
293 rule_name, all_hints.join(" ")
294 );
295 }
296
297 if let Some(doc) = hint_documentation(hint) {
298 format!(
299 "### `{}` — `@pretty` Hint\n\n{}\n\n\
300 Applied to rule `{}`.\n\n\
301 ```bbnf\n@pretty {} {} ;\n```",
302 hint, doc, rule_name,
303 rule_name, all_hints.join(" ")
304 )
305 } else {
306 format!("`@pretty` hint: **{}**\n\nUnknown hint.", hint)
307 }
308}
309
310fn build_pretty_directive_hover(
312 state: &DocumentState,
313 pretty: &crate::state::pretty::PrettyInfo,
314) -> String {
315 let def = state
316 .info
317 .rule_index
318 .get(&pretty.rule_name)
319 .map(|&i| &state.info.rules[i]);
320
321 let mut content = format!(
322 "### `@pretty` — Formatting Directive\n\n\
323 Controls how rule `{}` is pretty-printed by the formatter.\n\n",
324 pretty.rule_name
325 );
326
327 content.push_str(&format!(
329 "```bbnf\n@pretty {} {} ;\n```\n\n",
330 pretty.rule_name,
331 pretty.hints.join(" ")
332 ));
333
334 if !pretty.hints.is_empty() {
336 content.push_str("**Applied hints:**\n\n");
337 for hint in &pretty.hints {
338 if let Some(sep_str) = extract_sep_string(hint) {
339 content.push_str(&format!(
340 "- `sep(\"{}\")` — joins elements with `\"{}\"`\n",
341 sep_str, sep_str
342 ));
343 } else if let Some(delim) = extract_split_delim(hint) {
344 content.push_str(&format!(
345 "- `split(\"{}\")` — splits Span text on `\"{}\"` (depth-aware)\n",
346 delim, delim
347 ));
348 } else if let Some(desc) = bbnf::generate::prettify::hints::hint_description(hint) {
349 content.push_str(&format!("- `{}` — {}\n", hint, lowercase_first(desc)));
350 }
351 }
352 content.push('\n');
353 }
354
355 if let Some(def) = def {
357 content.push_str(&format!(
358 "---\n```bbnf\n{} = {}\n```",
359 def.name, def.rhs_text
360 ));
361 }
362
363 content
364}
365
366fn lowercase_first(s: &str) -> String {
368 let mut chars = s.chars();
369 match chars.next() {
370 None => String::new(),
371 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
372 }
373}