diff --git a/rusty_lr_executable/src/lsp/completion.rs b/rusty_lr_executable/src/lsp/completion.rs index 89d7f6b..23e4254 100644 --- a/rusty_lr_executable/src/lsp/completion.rs +++ b/rusty_lr_executable/src/lsp/completion.rs @@ -9,6 +9,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::str::FromStr; use crate::lsp::diagnostics::split_stream; +use crate::lsp::hover; use crate::lsp::position::{offset_to_position, position_to_offset}; pub(crate) const DIRECTIVES: &[&str] = &[ @@ -62,6 +63,7 @@ pub(crate) const KEYWORDS: &[&str] = &[ "data", "lookahead", "shift", + "Err", ]; pub(crate) const SYNTAX_URL: &str = "https://github.com/ehwan/RustyLR/blob/main/SYNTAX.md"; @@ -129,48 +131,98 @@ pub fn completions(content: &str, position: Position) -> CompletionResponse { ); } for variable in &line_variables.value_names { - builder.variable( - &format!("${variable}"), - "current production binding", - Some(format!( - "Semantic value bound by the current production line.\n\nExample:\n\n```rustylr\nExpr : left=Expr plus right=Term {{ left + right }};\n```\n\nHere `$left` and `$right` can be used in RustCode substitution contexts.\n\n[Named variables]({SYNTAX_URL}#named-variables)" - )), - ); + let (detail, documentation) = if let Some(reference) = + line_variables.value_references.get(variable) + { + ( + format!("${variable}: {}", reference.ty), + Some(hover::reduce_action_binding_reference_documentation( + &format!("${variable}"), + reference, + )), + ) + } else { + ( + "current production binding".to_string(), + Some(format!( + "Semantic value bound by the current production line.\n\nExample:\n\n```rustylr\nExpr : left=Expr plus right=Term {{ left + right }};\n```\n\nHere `$left` and `$right` can be used in RustCode substitution contexts.\n\n[Named variables]({SYNTAX_URL}#named-variables)" + )), + ) + }; + builder.variable(&format!("${variable}"), &detail, documentation); } for index in 1..=line_variables.value_count { - builder.variable( - &format!("${index}"), - "positional semantic value", - Some(format!( - "Semantic value of RHS symbol #{index} in the current production line.\n\nExample:\n\n```rustylr\nExpr : Expr plus Term {{ $1 }};\n```\n\n[Bison-style positional variables]({SYNTAX_URL}#3-bison-style-positional-variables)" - )), - ); + let (detail, documentation) = if let Some(reference) = + line_variables.position_references.get(&index) + { + ( + format!("${index}: {}", reference.ty), + Some(hover::reduce_action_binding_reference_documentation( + &format!("${index}"), + reference, + )), + ) + } else { + ( + "positional semantic value".to_string(), + Some(format!( + "Semantic value of RHS symbol #{index} in the current production line.\n\nExample:\n\n```rustylr\nExpr : Expr plus Term {{ $1 }};\n```\n\n[Bison-style positional variables]({SYNTAX_URL}#3-bison-style-positional-variables)" + )), + ) + }; + builder.variable(&format!("${index}"), &detail, documentation); } } CompletionMode::Location => { - builder.variable( - "@$", - "current production location", - location_documentation("@$"), - ); - builder.variable( - "@0", - "current production location", - location_documentation("@0"), - ); + let (detail, documentation) = location_completion_info(parsed.as_ref(), "@$"); + builder.variable("@$", &detail, documentation); + let (detail, documentation) = location_completion_info(parsed.as_ref(), "@0"); + builder.variable("@0", &detail, documentation); for variable in &line_variables.value_names { - builder.variable( - &format!("@{variable}"), - "current production binding location", - location_documentation(&format!("@{variable}")), - ); + let label = format!("@{variable}"); + let (detail, documentation) = + if let Some(reference) = line_variables.value_references.get(variable) { + ( + hover::reduce_action_location_reference_detail( + parsed.as_ref().unwrap(), + &label, + ), + Some(hover::reduce_action_location_reference_documentation( + parsed.as_ref().unwrap(), + &label, + Some(reference), + )), + ) + } else { + ( + "current production binding location".to_string(), + location_documentation(&label), + ) + }; + builder.variable(&label, &detail, documentation); } for index in 1..=line_variables.value_count { - builder.variable( - &format!("@{index}"), - "positional location", - location_documentation(&format!("@{index}")), - ); + let label = format!("@{index}"); + let (detail, documentation) = + if let Some(reference) = line_variables.position_references.get(&index) { + ( + hover::reduce_action_location_reference_detail( + parsed.as_ref().unwrap(), + &label, + ), + Some(hover::reduce_action_location_reference_documentation( + parsed.as_ref().unwrap(), + &label, + Some(reference), + )), + ) + } else { + ( + "positional location".to_string(), + location_documentation(&label), + ) + }; + builder.variable(&label, &detail, documentation); } } CompletionMode::AllowDiagnostic => { @@ -184,9 +236,29 @@ pub fn completions(content: &str, position: Position) -> CompletionResponse { add_symbol_items(&mut builder, &names); } CompletionMode::Symbol => { + if line_variables.in_reduce_action { + for (name, ty) in &line_variables.value_types { + if let Some(reference) = line_variables.value_references.get(name) { + builder.variable( + name, + &hover::reduce_action_binding_detail(name, ty), + Some(hover::reduce_action_binding_reference_documentation( + name, reference, + )), + ); + } else { + builder.variable( + name, + &hover::reduce_action_binding_detail(name, ty), + Some(hover::reduce_action_binding_documentation(name, ty)), + ); + } + } + } add_symbol_items(&mut builder, &names); for keyword in KEYWORDS { - builder.keyword(keyword, "RustyLR keyword", keyword_documentation(keyword)); + let (detail, documentation) = keyword_completion_info(parsed.as_ref(), keyword); + builder.keyword(keyword, &detail, documentation); } for directive in DIRECTIVES { builder.keyword( @@ -201,6 +273,36 @@ pub fn completions(content: &str, position: Position) -> CompletionResponse { CompletionResponse::Array(builder.finish()) } +fn keyword_completion_info(args: Option<&GrammarArgs>, keyword: &str) -> (String, Option) { + if let Some(args) = args { + if let Some(documentation) = hover::reduce_action_variable_documentation(args, keyword) { + let detail = hover::reduce_action_variable_detail(args, keyword) + .unwrap_or_else(|| "RustyLR keyword".to_string()); + return (detail, Some(documentation)); + } + } + + ( + "RustyLR keyword".to_string(), + keyword_documentation(keyword), + ) +} + +fn location_completion_info(args: Option<&GrammarArgs>, label: &str) -> (String, Option) { + if let Some(args) = args { + if let Some(documentation) = hover::reduce_action_variable_documentation(args, label) { + let detail = hover::reduce_action_variable_detail(args, label) + .unwrap_or_else(|| "current production location".to_string()); + return (detail, Some(documentation)); + } + } + + ( + "current production location".to_string(), + location_documentation(label), + ) +} + fn add_symbol_items(builder: &mut CompletionBuilder, names: &CompletionNames) { for (name, documentation) in &names.nonterminals { builder.nonterminal(name, documentation.clone()); @@ -485,10 +587,23 @@ fn rule_line_tokens_text( #[derive(Default)] struct LineVariables { value_names: BTreeSet, + value_types: BTreeMap, + value_references: BTreeMap, + position_references: BTreeMap, value_count: usize, + in_reduce_action: bool, +} + +#[derive(Clone, Debug)] +pub(crate) struct ReduceActionReference { + pub(crate) ty: String, + pub(crate) production: String, + pub(crate) marker: String, + pub(crate) index: usize, } fn variables_for_offset(args: &GrammarArgs, content: &str, offset: usize) -> LineVariables { + let grammar = Grammar::from_grammar_args(args.clone()).ok(); for rule in &args.rules { for (line_idx, line) in rule.rule_lines.iter().enumerate() { let start = args @@ -498,12 +613,50 @@ fn variables_for_offset(args: &GrammarArgs, content: &str, offset: usize) -> Lin let end = rule_line_end(args, content, rule, line_idx); if start <= offset && offset <= end { let mut variables = LineVariables::default(); + variables.in_reduce_action = reduce_action_contains_offset(line, offset); + let token_references = grammar + .as_ref() + .map(|grammar| { + production_token_references(args, content, rule, line_idx, grammar) + }) + .unwrap_or_default(); for (mapped_name, pattern) in &line.tokens { variables.value_count += 1; + if let Some(reference) = token_references.get(variables.value_count - 1) { + variables + .position_references + .insert(variables.value_count, reference.clone()); + } if let Some(name) = mapped_name { variables.value_names.insert(name.value().clone()); + if let Some(grammar) = &grammar { + variables.value_types.insert( + name.value().clone(), + hover::pattern_final_type(args, grammar, pattern), + ); + } + if let Some(reference) = token_references.get(variables.value_count - 1) { + variables + .value_references + .insert(name.value().clone(), reference.clone()); + } } else { collect_default_bindings(pattern, &mut variables.value_names); + if let (Some(grammar), Some(reference)) = + (&grammar, token_references.get(variables.value_count - 1)) + { + collect_default_binding_types( + args, + grammar, + pattern, + &mut variables.value_types, + ); + collect_default_binding_references( + pattern, + reference, + &mut variables.value_references, + ); + } } } return variables; @@ -514,6 +667,93 @@ fn variables_for_offset(args: &GrammarArgs, content: &str, offset: usize) -> Lin LineVariables::default() } +pub(crate) fn reduce_action_binding_reference_for_offset( + args: &GrammarArgs, + content: &str, + offset: usize, + name: &str, +) -> Option { + let variables = variables_for_offset(args, content, offset); + if !variables.in_reduce_action { + return None; + } + variables.value_references.get(name).cloned() +} + +pub(crate) fn reduce_action_positional_reference_for_offset( + args: &GrammarArgs, + content: &str, + offset: usize, + index: usize, +) -> Option { + let variables = variables_for_offset(args, content, offset); + if !variables.in_reduce_action { + return None; + } + variables.position_references.get(&index).cloned() +} + +fn reduce_action_contains_offset(line: &rusty_lr_parser::RuleLineArgs, offset: usize) -> bool { + line.reduce_action + .as_ref() + .and_then(token_stream_range) + .is_some_and(|range| range.contains(&offset)) +} + +fn production_token_references( + args: &GrammarArgs, + content: &str, + rule: &rusty_lr_parser::RuleDefArgs, + line_idx: usize, + grammar: &Grammar, +) -> Vec { + let line = &rule.rule_lines[line_idx]; + let separator = if line_idx == 0 { ':' } else { '|' }; + let mut production = format!("{} {separator}", rule.name.value()); + let mut token_spans = Vec::new(); + for (mapped_name, pattern) in &line.tokens { + let token_text = mapped_pattern_text(args, content, mapped_name.as_ref(), pattern); + if token_text.is_empty() { + continue; + } + production.push(' '); + let start = production.chars().count(); + production.push_str(&token_text); + let width = token_text.chars().count().max(1); + token_spans.push(( + start, + width, + hover::pattern_final_type(args, grammar, pattern), + )); + } + + token_spans + .into_iter() + .enumerate() + .map(|(idx, (start, width, ty))| ReduceActionReference { + ty, + production: production.clone(), + marker: format!("{}{}", " ".repeat(start), "^".repeat(width)), + index: idx + 1, + }) + .collect() +} + +fn mapped_pattern_text( + args: &GrammarArgs, + content: &str, + mapped_name: Option<&rusty_lr_parser::Located>, + pattern: &PatternArgs, +) -> String { + let start = mapped_name + .and_then(|name| args.span_manager.get_byterange(&name.location())) + .map_or_else(|| pattern_start(args, pattern), |range| range.start); + let end = pattern_end(args, pattern); + content[start.min(content.len())..end.min(content.len())] + .trim() + .to_string() +} + fn rule_line_end( args: &GrammarArgs, content: &str, @@ -661,6 +901,35 @@ fn token_stream_end(stream: &TokenStream) -> usize { .unwrap_or(0) } +fn token_stream_range(stream: &TokenStream) -> Option> { + let mut ranges = stream.clone().into_iter().filter_map(token_tree_range); + let first = ranges.next()?; + let range = ranges.fold(first, |acc, range| { + acc.start.min(range.start)..acc.end.max(range.end) + }); + Some(range) +} + +fn token_tree_range(token: TokenTree) -> Option> { + match token { + TokenTree::Group(group) => { + let stream_range = token_stream_range(&group.stream()); + let open = group.span_open().byte_range(); + let close = group.span_close().byte_range(); + let delimiter_range = open.start.min(close.start)..open.end.max(close.end); + Some(if let Some(stream_range) = stream_range { + delimiter_range.start.min(stream_range.start) + ..delimiter_range.end.max(stream_range.end) + } else { + delimiter_range + }) + } + TokenTree::Ident(ident) => Some(ident.span().byte_range()), + TokenTree::Punct(punct) => Some(punct.span().byte_range()), + TokenTree::Literal(lit) => Some(lit.span().byte_range()), + } +} + fn token_tree_end(token: TokenTree) -> usize { match token { TokenTree::Group(group) => token_stream_end(&group.stream()) @@ -672,6 +941,72 @@ fn token_tree_end(token: TokenTree) -> usize { } } +fn collect_default_binding_types( + args: &GrammarArgs, + grammar: &Grammar, + pattern: &PatternArgs, + bindings: &mut BTreeMap, +) { + match pattern { + PatternArgs::Ident(ident) => { + bindings.insert( + ident.value().clone(), + hover::pattern_final_type(args, grammar, pattern), + ); + } + PatternArgs::Plus { base, .. } + | PatternArgs::Star { base, .. } + | PatternArgs::Question { base, .. } => { + collect_default_binding_types(args, grammar, base, bindings); + } + PatternArgs::Minus { base, exclude } => { + collect_default_binding_types(args, grammar, base, bindings); + collect_default_binding_types(args, grammar, exclude, bindings); + } + PatternArgs::Sep { base, .. } => { + collect_default_binding_types(args, grammar, base, bindings); + } + PatternArgs::Exclamation { .. } + | PatternArgs::Group { .. } + | PatternArgs::TerminalSet(_) + | PatternArgs::Byte(_) + | PatternArgs::ByteString(_) + | PatternArgs::Char(_) + | PatternArgs::String(_) => {} + } +} + +fn collect_default_binding_references( + pattern: &PatternArgs, + reference: &ReduceActionReference, + bindings: &mut BTreeMap, +) { + match pattern { + PatternArgs::Ident(ident) => { + bindings.insert(ident.value().clone(), reference.clone()); + } + PatternArgs::Plus { base, .. } + | PatternArgs::Star { base, .. } + | PatternArgs::Question { base, .. } => { + collect_default_binding_references(base, reference, bindings); + } + PatternArgs::Minus { base, exclude } => { + collect_default_binding_references(base, reference, bindings); + collect_default_binding_references(exclude, reference, bindings); + } + PatternArgs::Sep { base, .. } => { + collect_default_binding_references(base, reference, bindings); + } + PatternArgs::Exclamation { .. } + | PatternArgs::Group { .. } + | PatternArgs::TerminalSet(_) + | PatternArgs::Byte(_) + | PatternArgs::ByteString(_) + | PatternArgs::Char(_) + | PatternArgs::String(_) => {} + } +} + fn collect_default_bindings(pattern: &PatternArgs, names: &mut BTreeSet) { match pattern { PatternArgs::Ident(ident) => { @@ -794,6 +1129,9 @@ pub(crate) fn keyword_documentation(label: &str) -> Option { "shift" => format!( "GLR reduce-action control binding used to allow or prune a shift branch.\n\nExample:\n\n```rustylr\n*shift = false;\n```\n\n[Advanced GLR reduce controls]({SYNTAX_URL}#advanced-glr-reduce-controls)" ), + "Err" => format!( + "`Err` constructs the error variant returned from a reduce action's `Result<_, %error>`.\n\nExample:\n\n```rustylr\nExpr : num {{ Err(MyError::InvalidNumber)? }};\n```\n\n[Error type]({SYNTAX_URL}#error-type-optional)" + ), "auto" => "Table layout mode selected automatically by RustyLR.".to_string(), "dense" => "Dense table layout mode.".to_string(), "sparse" => "Sparse table layout mode.".to_string(), @@ -1008,6 +1346,135 @@ Boxed(box $tokentype) : num { num }; assert!(!markup.contains("{ num }")); } + #[test] + fn completion_items_use_hover_reduce_action_documentation() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32) } +%% +%moduleprefix ::my_prefix; +%userdata $moduleprefix::UserData; +%location $moduleprefix::Span; +%error $userdata; +%tokentype $moduleprefix::Token; +%start Expr; +%token num Token::Num(_); +Expr : num { let _ = ; let _ = @0; 0 }; +"#; + let action_offset = grammar.find("let _ = ;").unwrap() + "let _ = ".len(); + let action_items = items(completions( + grammar, + offset_to_position(grammar, action_offset), + )); + + let data = action_items + .iter() + .find(|item| item.label == "data") + .unwrap(); + assert_eq!( + data.detail.as_deref(), + Some("data: &mut ::my_prefix::UserData") + ); + assert!(markdown_value(data).contains("data: &mut ::my_prefix::UserData")); + + let lookahead = action_items + .iter() + .find(|item| item.label == "lookahead") + .unwrap(); + assert_eq!( + lookahead.detail.as_deref(), + Some("lookahead: &::my_prefix::Token") + ); + assert!(markdown_value(lookahead).contains("lookahead: &::my_prefix::Token")); + + let shift = action_items + .iter() + .find(|item| item.label == "shift") + .unwrap(); + assert_eq!(shift.detail.as_deref(), Some("shift: &mut bool")); + assert!(markdown_value(shift).contains("shift: &mut bool")); + + let err = action_items + .iter() + .find(|item| item.label == "Err") + .unwrap(); + assert_eq!( + err.detail.as_deref(), + Some("Result::Err(::my_prefix::UserData)") + ); + assert!(markdown_value(err).contains("Result::Err(::my_prefix::UserData)")); + + let location_offset = grammar.find("@0").unwrap() + 1; + let location_items = items(completions( + grammar, + offset_to_position(grammar, location_offset), + )); + let at0 = location_items + .iter() + .find(|item| item.label == "@0") + .unwrap(); + assert_eq!(at0.detail.as_deref(), Some("@0: &mut ::my_prefix::Span")); + assert!(markdown_value(at0).contains("@0: &mut ::my_prefix::Span")); + } + + #[test] + fn completes_mapped_symbols_inside_reduce_action() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32), Plus } +%% +%tokentype Token; +%start Expr; +%token num Token::Num(_); +%token plus Token::Plus; +Expr(i32) : left=Expr plus right=Expr { }; +"#; + let offset = grammar.find("{ }").unwrap() + 2; + let completion_items = items(completions(grammar, offset_to_position(grammar, offset))); + + let left = completion_items + .iter() + .find(|item| item.label == "left") + .unwrap(); + assert_eq!(left.detail.as_deref(), Some("left: i32")); + let left_markup = markdown_value(left); + assert!(left_markup.contains("left: i32")); + assert!(left_markup.contains("Expr : left=Expr plus right=Expr")); + assert!(left_markup.contains(" ^^^^^^^^^")); + + let right = completion_items + .iter() + .find(|item| item.label == "right") + .unwrap(); + assert_eq!(right.detail.as_deref(), Some("right: i32")); + let right_markup = markdown_value(right); + assert!(right_markup.contains("right: i32")); + assert!(right_markup.contains("Expr : left=Expr plus right=Expr")); + assert!(right_markup.contains(" ^^^^^^^^^")); + + let dollar_grammar = grammar.replacen("{ }", "{ $ }", 1); + let dollar_offset = dollar_grammar.find("{ $").unwrap() + 3; + let dollar_items = items(completions( + &dollar_grammar, + offset_to_position(&dollar_grammar, dollar_offset), + )); + let first = dollar_items.iter().find(|item| item.label == "$1").unwrap(); + assert_eq!(first.detail.as_deref(), Some("$1: i32")); + assert!(markdown_value(first).contains(" ^^^^^^^^^")); + + let location_grammar = grammar.replacen("{ }", "{ @ }", 1); + let location_offset = location_grammar.find("{ @").unwrap() + 3; + let location_items = items(completions( + &location_grammar, + offset_to_position(&location_grammar, location_offset), + )); + let first_location = location_items + .iter() + .find(|item| item.label == "@1") + .unwrap(); + assert!(markdown_value(first_location).contains(" ^^^^^^^^^")); + } + fn markdown_value(item: &CompletionItem) -> &str { let documentation = item.documentation.as_ref().unwrap(); let Documentation::MarkupContent(markup) = documentation else { diff --git a/rusty_lr_executable/src/lsp/hover.rs b/rusty_lr_executable/src/lsp/hover.rs index c1ca6ec..ec783af 100644 --- a/rusty_lr_executable/src/lsp/hover.rs +++ b/rusty_lr_executable/src/lsp/hover.rs @@ -1,10 +1,11 @@ use lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position}; -use proc_macro2::TokenStream; +use proc_macro2::{Group, TokenStream, TokenTree}; use rusty_lr_parser::grammar::Grammar; use rusty_lr_parser::terminal_info::TerminalName; -use rusty_lr_parser::{GrammarArgs, PatternArgs, TerminalSetItem}; +use rusty_lr_parser::{GrammarArgs, Location, PatternArgs, TerminalSetItem}; use std::collections::BTreeSet; use std::ops::Range as ByteRange; +use std::str::FromStr; use crate::lsp::completion::{ self, ALLOW_DIAGNOSTICS, DIRECTIVES, KEYWORDS, SUBSTITUTION_VARIABLES, SYNTAX_URL, @@ -60,31 +61,17 @@ pub fn hover(content: &str, position: Position) -> Option { } if documentation.is_none() { - if word == "data" { - let mut userdata_type = "()".to_string(); - let mut definition_info = "".to_string(); - - if let Some(args) = &parsed { - if let Some((_, ts)) = args.userdata_typename.first() { - userdata_type = ts.to_string(); - definition_info = format!( - "\n\nDefinition:\n```rustylr\n%userdata {};\n```", - userdata_type - ); - } - } - - documentation = Some(format!( - "### `data: &mut {}`{}\n\nMutable user-data binding available inside reduce actions.\n\nExample:\n\n```rustylr\nExpr : num {{ data.count += 1; num }};\n```\n\n[User data]({}#4-user-data-data)", - userdata_type, - definition_info, - SYNTAX_URL - )); - } else { - documentation = hover_word_documentation(&word); + if let Some(args) = &parsed { + documentation = reduce_action_variable_documentation(args, &word).or_else(|| { + reduce_action_reference_documentation_for_word(args, content, offset, &word) + }); } } + if documentation.is_none() { + documentation = hover_word_documentation(&word); + } + let documentation = documentation?; Some(markdown_hover(content, documentation, None)) } @@ -163,6 +150,321 @@ fn reduce_action_documentation() -> String { ) } +pub(crate) fn reduce_action_variable_documentation( + args: &GrammarArgs, + word: &str, +) -> Option { + match word { + "data" => Some(data_documentation(args)), + "lookahead" => Some(lookahead_documentation(args)), + "shift" => Some(shift_documentation()), + "Err" => Some(err_variant_documentation(args)), + "@0" | "@$" => Some(current_location_documentation(args, word)), + _ => None, + } +} + +pub(crate) fn reduce_action_variable_detail(args: &GrammarArgs, word: &str) -> Option { + match word { + "data" => Some(format!("data: &mut {}", resolved_userdata_type_name(args))), + "lookahead" => Some(format!("lookahead: &{}", token_type_name(args))), + "shift" => Some("shift: &mut bool".to_string()), + "Err" => Some(format!("Result::Err({})", resolved_error_type_name(args))), + "@0" | "@$" => Some(format!( + "{word}: &mut {}", + resolved_location_type_name(args) + )), + _ => None, + } +} + +pub(crate) fn reduce_action_binding_detail(name: &str, ty: &str) -> String { + format!("{name}: {ty}") +} + +pub(crate) fn reduce_action_binding_documentation(name: &str, ty: &str) -> String { + reduce_action_binding_documentation_impl(name, ty, None) +} + +pub(crate) fn reduce_action_binding_reference_documentation( + name: &str, + reference: &completion::ReduceActionReference, +) -> String { + reduce_action_binding_documentation_impl(name, &reference.ty, Some(reference)) +} + +fn reduce_action_binding_documentation_impl( + name: &str, + ty: &str, + reference: Option<&completion::ReduceActionReference>, +) -> String { + let reference_section = production_reference_section(reference); + format!( + "### `{}`\n\nSemantic value bound by the current production line.\n\nFinal type: `{}`{reference_section}\n\nExample:\n\n```rustylr\nExpr : left=Expr plus right=Term {{ left + right }};\n```\n\n[Named variables]({SYNTAX_URL}#named-variables)", + reduce_action_binding_detail(name, ty), + ty + ) +} + +pub(crate) fn reduce_action_location_reference_detail(args: &GrammarArgs, label: &str) -> String { + format!("{label}: {}", resolved_location_type_name(args)) +} + +pub(crate) fn reduce_action_location_reference_documentation( + args: &GrammarArgs, + label: &str, + reference: Option<&completion::ReduceActionReference>, +) -> String { + let location_type = resolved_location_type_name(args); + let reference_section = production_reference_section(reference); + format!( + "### `{label}: {location_type}`\n\nSource-location value for a referenced production token.{reference_section}\n\nExamples:\n\n```rustylr\nExpr : left=Expr plus right=Term {{ println!(\"{{:?}}\", @left); }};\nExpr : Expr plus Term {{ println!(\"{{:?}}\", @1); }};\n```\n\n[Location tracking]({SYNTAX_URL}#location-tracking)" + ) +} + +fn production_reference_section(reference: Option<&completion::ReduceActionReference>) -> String { + let Some(reference) = reference else { + return String::new(); + }; + + format!( + "\n\nReferences RHS symbol #{}:\n\n```rustylr\n{}\n{}\n```", + reference.index, reference.production, reference.marker + ) +} + +fn reduce_action_reference_documentation_for_word( + args: &GrammarArgs, + content: &str, + offset: usize, + word: &str, +) -> Option { + if let Some(name) = word.strip_prefix('@') { + if name == "0" || name == "$" { + return None; + } + if let Ok(index) = name.parse::() { + return completion::reduce_action_positional_reference_for_offset( + args, content, offset, index, + ) + .map(|reference| { + reduce_action_location_reference_documentation(args, word, Some(&reference)) + }); + } + return completion::reduce_action_binding_reference_for_offset(args, content, offset, name) + .map(|reference| { + reduce_action_location_reference_documentation(args, word, Some(&reference)) + }); + } + + if let Some(name) = word.strip_prefix('$') { + if let Ok(index) = name.parse::() { + return completion::reduce_action_positional_reference_for_offset( + args, content, offset, index, + ) + .map(|reference| reduce_action_binding_reference_documentation(word, &reference)); + } + return completion::reduce_action_binding_reference_for_offset(args, content, offset, name) + .map(|reference| reduce_action_binding_reference_documentation(word, &reference)); + } + + completion::reduce_action_binding_reference_for_offset(args, content, offset, word) + .map(|reference| reduce_action_binding_reference_documentation(word, &reference)) +} + +fn data_documentation(args: &GrammarArgs) -> String { + let userdata_type = resolved_userdata_type_name(args); + let definition_info = type_definition("userdata", &args.userdata_typename); + + format!( + "### `data: &mut {userdata_type}`{definition_info}\n\nMutable user-data binding available inside reduce actions.\n\nExample:\n\n```rustylr\nExpr : num {{ data.count += 1; num }};\n```\n\n[User data]({SYNTAX_URL}#4-user-data-data)" + ) +} + +fn lookahead_documentation(args: &GrammarArgs) -> String { + let token_type = token_type_name(args); + let definition_info = type_definition("tokentype", &args.token_typename); + + format!( + "### `lookahead: &{token_type}`{definition_info}\n\nGLR reduce-action control binding for inspecting the next terminal.\n\nExample:\n\n```rustylr\nif let Some(term) = lookahead.to_term() {{ /* ... */ }}\n```\n\n[Advanced GLR reduce controls]({SYNTAX_URL}#advanced-glr-reduce-controls)" + ) +} + +fn shift_documentation() -> String { + format!( + "### `shift: &mut bool`\n\nGLR reduce-action control binding used to allow or prune a shift branch.\n\nExample:\n\n```rustylr\n*shift = false;\n```\n\n[Advanced GLR reduce controls]({SYNTAX_URL}#advanced-glr-reduce-controls)" + ) +} + +fn err_variant_documentation(args: &GrammarArgs) -> String { + let error_type = resolved_error_type_name(args); + let definition_info = type_definition("error", &args.error_typename); + + format!( + "### `Result::Err({error_type})`{definition_info}\n\n`Err` constructs the error variant returned from a reduce action's `Result<_, {error_type}>`.\n\nExample:\n\n```rustylr\nExpr : num {{ Err(MyError::InvalidNumber)? }};\n```\n\n[Error type]({SYNTAX_URL}#error-type-optional)" + ) +} + +fn current_location_documentation(args: &GrammarArgs, label: &str) -> String { + let location_type = resolved_location_type_name(args); + let definition_info = type_definition("location", &args.location_typename); + + format!( + "### `{label}: &mut {location_type}`{definition_info}\n\nCurrent production source-location binding available inside reduce actions.\n\nExamples:\n\n```rustylr\nExpr : Term {{ println!(\"{{:?}}\", @$); }};\nExpr : {{ println!(\"{{:?}}\", @0); }};\n```\n\n[Location tracking]({SYNTAX_URL}#location-tracking)" + ) +} + +fn grammar_type_name(items: &[(Location, TokenStream)], default: &str) -> String { + let name = items + .first() + .map(|(_, ty)| ty.to_string()) + .filter(|ty| !ty.is_empty()) + .unwrap_or_else(|| default.to_string()); + normalize_path_spacing(&name) +} + +fn normalize_path_spacing(name: &str) -> String { + name.replace(":: ", "::").replace(" ::", "::") +} + +fn token_type_name(args: &GrammarArgs) -> String { + Grammar::from_grammar_args(args.clone()) + .ok() + .map(|grammar| type_stream_name(grammar.token_type())) + .unwrap_or_else(|| { + resolve_provider_type_name(args, "tokentype") + .unwrap_or_else(|| grammar_type_name(&args.token_typename, "()")) + }) +} + +fn resolved_userdata_type_name(args: &GrammarArgs) -> String { + resolve_provider_type_name(args, "userdata") + .unwrap_or_else(|| grammar_type_name(&args.userdata_typename, "()")) +} + +fn resolved_error_type_name(args: &GrammarArgs) -> String { + resolve_provider_type_name(args, "errortype").unwrap_or_else(|| { + grammar_type_name( + &args.error_typename, + &default_module_type(args, "DefaultReduceActionError"), + ) + }) +} + +fn resolved_location_type_name(args: &GrammarArgs) -> String { + resolve_provider_type_name(args, "location").unwrap_or_else(|| { + grammar_type_name( + &args.location_typename, + &default_module_type(args, "DefaultLocation"), + ) + }) +} + +fn default_module_type(args: &GrammarArgs, type_name: &str) -> String { + let module_prefix = + resolve_provider_type_name(args, "moduleprefix").unwrap_or_else(|| "::rusty_lr".into()); + format!("{module_prefix}::{type_name}") +} + +fn type_stream_name(ty: &TokenStream) -> String { + normalize_path_spacing(&ty.to_string()) +} + +fn resolve_provider_type_name(args: &GrammarArgs, name: &str) -> Option { + resolve_provider_stream(args, name, &mut Vec::new()).map(|stream| type_stream_name(&stream)) +} + +fn resolve_provider_stream( + args: &GrammarArgs, + name: &str, + stack: &mut Vec, +) -> Option { + let stream = provider_stream(args, name)?; + if stack.iter().any(|entry| entry == name) || stack.len() >= 100 { + return None; + } + stack.push(name.to_string()); + let resolved = resolve_type_stream(args, stream, stack); + stack.pop(); + Some(resolved) +} + +fn provider_stream(args: &GrammarArgs, name: &str) -> Option { + match name { + "moduleprefix" => args + .module_prefix + .first() + .map(|(_, stream)| stream.clone()) + .or_else(|| token_stream_from_str("::rusty_lr")), + "userdata" => args + .userdata_typename + .first() + .map(|(_, stream)| stream.clone()) + .or_else(|| token_stream_from_str("()")), + "errortype" | "error" => args + .error_typename + .first() + .map(|(_, stream)| stream.clone()) + .or_else(|| token_stream_from_str("$moduleprefix::DefaultReduceActionError")), + "location" => args + .location_typename + .first() + .map(|(_, stream)| stream.clone()) + .or_else(|| token_stream_from_str("$moduleprefix::DefaultLocation")), + "tokentype" => args + .token_typename + .first() + .map(|(_, stream)| stream.clone()), + _ => None, + } +} + +fn resolve_type_stream( + args: &GrammarArgs, + stream: TokenStream, + stack: &mut Vec, +) -> TokenStream { + let mut resolved = TokenStream::new(); + let mut tokens = stream.into_iter().peekable(); + while let Some(token) = tokens.next() { + match token { + TokenTree::Punct(punct) if punct.as_char() == '$' => { + if let Some(TokenTree::Ident(ident)) = tokens.peek() { + let ident_name = ident.to_string(); + if let Some(substitution) = resolve_provider_stream(args, &ident_name, stack) { + resolved.extend(substitution); + tokens.next(); + continue; + } + } + resolved.extend([TokenTree::Punct(punct)]); + } + TokenTree::Group(group) => { + let delimiter = group.delimiter(); + let stream = resolve_type_stream(args, group.stream(), stack); + resolved.extend([TokenTree::Group(Group::new(delimiter, stream))]); + } + token => resolved.extend([token]), + } + } + resolved +} + +fn token_stream_from_str(value: &str) -> Option { + TokenStream::from_str(value).ok() +} + +fn type_definition(directive: &str, items: &[(Location, TokenStream)]) -> String { + let Some((location, ty)) = items.first() else { + return String::new(); + }; + if matches!(location, Location::CallSite) { + return String::new(); + } + + format!("\n\nDefinition:\n```rustylr\n%{directive} {};\n```", ty) +} + fn hover_word(content: &str, offset: usize) -> Option { let mut offset = offset.min(content.len()); if offset < content.len() { @@ -826,11 +1128,12 @@ Expr : num { *data += 1; 0 }; #[derive(Debug, Clone)] pub enum Token { Num(i32) } %% +%location Span; %userdata MyCoolData; %tokentype Token; %start Expr; %token num Token::Num(_); -Expr : num { println!("{:?}, {:?}", @1, @$); 0 }; +Expr : num { println!("{:?}, {:?}, {:?}", @1, @0, @$); 0 }; "#; // Hover on '@' of '@1' let offset = grammar.find("@1").unwrap(); @@ -842,10 +1145,12 @@ Expr : num { println!("{:?}, {:?}", @1, @$); 0 }; let HoverContents::Markup(markup1) = hover1.contents else { panic!("expected markup hover"); }; - assert!(markup1.value.contains("`@1` refers to a source-location")); + assert!(markup1.value.contains("@1: Span")); + assert!(markup1.value.contains("Expr : num")); + assert!(markup1.value.contains(" ^^^")); - // Hover on '@' of '@$' - let offset = grammar.find("@$").unwrap(); + // Hover on '@' of '@0' + let offset = grammar.find("@0").unwrap(); let hover2 = hover( grammar, crate::lsp::position::offset_to_position(grammar, offset), @@ -854,7 +1159,290 @@ Expr : num { println!("{:?}, {:?}", @1, @$); 0 }; let HoverContents::Markup(markup2) = hover2.contents else { panic!("expected markup hover"); }; - assert!(markup2.value.contains("`@$` refers to a source-location")); + assert!(markup2.value.contains("@0: &mut Span")); + assert!(markup2.value.contains("%location Span;")); + + // Hover on '@' of '@$' + let offset = grammar.find("@$").unwrap(); + let hover3 = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, offset), + ) + .unwrap(); + let HoverContents::Markup(markup3) = hover3.contents else { + panic!("expected markup hover"); + }; + assert!(markup3.value.contains("@$: &mut Span")); + assert!(markup3.value.contains("%location Span;")); + } + + #[test] + fn hovers_shift_with_mutable_bool_type() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32) } +%% +%glr; +%tokentype Token; +%start Expr; +%token num Token::Num(_); +Expr : num { *shift = false; 0 }; +"#; + let offset = grammar.find("shift").unwrap(); + let hover_res = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup.value.contains("shift: &mut bool")); + assert!(markup.value.contains("GLR reduce-action control binding")); + } + + #[test] + fn hovers_lookahead_with_token_type() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32) } +%% +%glr; +%tokentype Token; +%start Expr; +%token num Token::Num(_); +Expr : num { if let Some(_term) = lookahead.to_term() {} 0 }; +"#; + let offset = grammar.find("lookahead").unwrap(); + let hover_res = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup.value.contains("lookahead: &Token")); + assert!(markup.value.contains("%tokentype Token;")); + } + + #[test] + fn hovers_err_with_reduce_error_type() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32) } +struct MyError; +%% +%error MyError; +%tokentype Token; +%start Expr; +%token num Token::Num(_); +Expr : num { Err(MyError)?; 0 }; +"#; + let offset = grammar.find("Err(").unwrap(); + let hover_res = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup.value.contains("Result::Err(MyError)")); + assert!(markup.value.contains("%error MyError;")); + } + + #[test] + fn hovers_err_with_default_reduce_error_type() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32) } +%% +%tokentype Token; +%start Expr; +%token num Token::Num(_); +Expr : num { Err(Default::default())?; 0 }; +"#; + let offset = grammar.find("Err(").unwrap(); + let hover_res = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup + .value + .contains("Result::Err(::rusty_lr::DefaultReduceActionError)")); + assert!(!markup.value.contains("moduleprefix")); + } + + #[test] + fn hovers_err_with_default_reduce_error_type_and_module_prefix() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32) } +%% +%moduleprefix ::my_prefix; +%tokentype Token; +%start Expr; +%token num Token::Num(_); +Expr : num { Err(Default::default())?; 0 }; +"#; + let offset = grammar.find("Err(").unwrap(); + let hover_res = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup + .value + .contains("Result::Err(::my_prefix::DefaultReduceActionError)")); + assert!(!markup.value.contains("moduleprefix")); + } + + #[test] + fn hovers_reduce_action_types_after_substitution() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32) } +%% +%moduleprefix ::my_prefix; +%userdata $moduleprefix::UserData; +%location $moduleprefix::Span; +%error $userdata; +%tokentype $moduleprefix::Token; +%start Expr; +%token num Token::Num(_); +Expr : num { let _ = data; let _ = @$; let _ = lookahead; Err(Default::default())?; 0 }; +"#; + + let data_offset = grammar.find("let _ = data").unwrap() + "let _ = ".len(); + let data_hover = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, data_offset), + ) + .unwrap(); + let HoverContents::Markup(data_markup) = data_hover.contents else { + panic!("expected markup hover"); + }; + assert!(data_markup + .value + .contains("data: &mut ::my_prefix::UserData")); + assert!(!data_markup.value.contains("$moduleprefix")); + + let location_offset = grammar.find("@$").unwrap(); + let location_hover = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, location_offset), + ) + .unwrap(); + let HoverContents::Markup(location_markup) = location_hover.contents else { + panic!("expected markup hover"); + }; + assert!(location_markup.value.contains("@$: &mut ::my_prefix::Span")); + assert!(!location_markup.value.contains("$location")); + + let lookahead_offset = grammar.find("lookahead").unwrap(); + let lookahead_hover = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, lookahead_offset), + ) + .unwrap(); + let HoverContents::Markup(lookahead_markup) = lookahead_hover.contents else { + panic!("expected markup hover"); + }; + assert!(lookahead_markup + .value + .contains("lookahead: &::my_prefix::Token")); + assert!(!lookahead_markup.value.contains("$moduleprefix")); + + let err_offset = grammar.find("Err(").unwrap(); + let err_hover = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, err_offset), + ) + .unwrap(); + let HoverContents::Markup(err_markup) = err_hover.contents else { + panic!("expected markup hover"); + }; + assert!(err_markup + .value + .contains("Result::Err(::my_prefix::UserData)")); + assert!(!err_markup.value.contains("$userdata")); + } + + #[test] + fn hovers_mapped_symbols_inside_reduce_action() { + let grammar = r#" +#[derive(Debug, Clone)] +pub enum Token { Num(i32), Plus } +%% +%tokentype Token; +%start Expr; +%token num Token::Num(_); +%token plus Token::Plus; +Expr(i32) : left=Expr plus right=Expr { left + right }; +"#; + let offset = grammar.find("{ left").unwrap() + 2; + let hover_res = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup.value.contains("left: i32")); + assert!(markup.value.contains("Semantic value bound")); + assert!(markup.value.contains("Expr : left=Expr plus right=Expr")); + assert!(markup.value.contains(" ^^^^^^^^^")); + + let positional_offset = grammar.find("+ right").unwrap() + 2; + let hover_res = hover( + grammar, + crate::lsp::position::offset_to_position(grammar, positional_offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup.value.contains("right: i32")); + assert!(markup.value.contains("Expr : left=Expr plus right=Expr")); + assert!(markup.value.contains("^^^^^^^^^")); + + let dollar_grammar = grammar.replace( + "{ left + right }", + "{ let _ = $1; let _ = @2; left + right }", + ); + let dollar_offset = dollar_grammar.find("$1").unwrap(); + let hover_res = hover( + &dollar_grammar, + crate::lsp::position::offset_to_position(&dollar_grammar, dollar_offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup.value.contains("$1: i32")); + assert!(markup.value.contains(" ^^^^^^^^^")); + + let location_offset = dollar_grammar.find("@2").unwrap(); + let hover_res = hover( + &dollar_grammar, + crate::lsp::position::offset_to_position(&dollar_grammar, location_offset), + ) + .unwrap(); + let HoverContents::Markup(markup) = hover_res.contents else { + panic!("expected markup hover"); + }; + assert!(markup.value.contains("@2:")); + assert!(markup.value.contains("Expr : left=Expr plus right=Expr")); + assert!(markup.value.contains(" ^^^^")); } #[test]