use tower_lsp::lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind};
use crate::document::Document;
use crate::utils::position::position_to_offset;
pub fn get_hover(doc: &Document, params: &HoverParams) -> Option<Hover> {
let offset = position_to_offset(
params.text_document_position_params.position,
&doc.line_index,
);
let content = &doc.content;
let parse_result = doc.parse_result.as_ref()?;
let (element_type, element_text) = find_element_at_offset(content, offset)?;
let hover_text = match element_type {
ElementType::Ingredient => {
let name = extract_name(&element_text);
for ingredient in &parse_result.recipe.ingredients {
if ingredient.name.eq_ignore_ascii_case(&name) {
return Some(create_hover(format_ingredient_hover(ingredient)));
}
}
format!("**Ingredient:** {}", name)
}
ElementType::Cookware => {
let name = extract_name(&element_text);
for cookware in &parse_result.recipe.cookware {
if cookware.name.eq_ignore_ascii_case(&name) {
return Some(create_hover(format_cookware_hover(cookware)));
}
}
format!("**Cookware:** {}", name)
}
ElementType::Timer => {
let name = extract_name(&element_text);
for timer in &parse_result.recipe.timers {
let timer_name = timer.name.as_deref().unwrap_or("");
if timer_name.eq_ignore_ascii_case(&name) || name.is_empty() {
return Some(create_hover(format_timer_hover(timer)));
}
}
format!(
"**Timer:** {}",
if name.is_empty() { "unnamed" } else { &name }
)
}
ElementType::Section => {
format!("**Section:** {}", element_text.trim_matches('=').trim())
}
ElementType::Metadata => {
format!(
"**Metadata:** {}",
element_text.trim_start_matches('>').trim()
)
}
ElementType::Comment => "**Comment**".to_string(),
};
Some(create_hover(hover_text))
}
#[derive(Debug)]
enum ElementType {
Ingredient,
Cookware,
Timer,
Section,
Metadata,
Comment,
}
fn find_element_at_offset(content: &str, offset: usize) -> Option<(ElementType, String)> {
let len = content.len();
if offset >= len {
return None;
}
let line_start = content[..offset].rfind('\n').map(|i| i + 1).unwrap_or(0);
let line_end = content[offset..]
.find('\n')
.map(|i| offset + i)
.unwrap_or(len);
let line = &content[line_start..line_end];
if line.starts_with("--") {
return Some((ElementType::Comment, line.to_string()));
}
if line.starts_with(">>") {
return Some((ElementType::Metadata, line.to_string()));
}
if line.starts_with('=') && line.ends_with('=') && line.len() > 1 {
return Some((ElementType::Section, line.to_string()));
}
let before_cursor = &content[line_start..offset];
let markers = [
('@', ElementType::Ingredient),
('#', ElementType::Cookware),
('~', ElementType::Timer),
];
let mut best_match: Option<(usize, ElementType)> = None;
for (marker, elem_type) in markers {
if let Some(pos) = before_cursor.rfind(marker) {
let after_marker = &before_cursor[pos..];
if !after_marker.contains('}') {
match best_match {
None => best_match = Some((line_start + pos, elem_type)),
Some((best_pos, _)) if line_start + pos > best_pos => {
best_match = Some((line_start + pos, elem_type));
}
_ => {}
}
}
}
}
if let Some((marker_pos, elem_type)) = best_match {
let end = find_element_end(content, marker_pos + 1);
return Some((elem_type, content[marker_pos..end].to_string()));
}
None
}
fn find_element_end(content: &str, start: usize) -> usize {
let mut in_braces = false;
for (i, ch) in content[start..].char_indices() {
let pos = start + i;
match ch {
'{' => in_braces = true,
'}' => return pos + 1,
' ' | '\n' | '\r' if !in_braces => return pos,
_ => {}
}
}
content.len()
}
fn extract_name(element: &str) -> String {
let s = element.trim_start_matches(['@', '#', '~']);
s.split('{').next().unwrap_or(s).trim().to_string()
}
fn create_hover(text: String) -> Hover {
Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: text,
}),
range: None,
}
}
fn format_ingredient_hover(ingredient: &cooklang::model::Ingredient) -> String {
let mut parts = Vec::new();
parts.push(format!("**Ingredient:** {}", ingredient.name));
if let Some(ref quantity) = ingredient.quantity {
parts.push(format!("**Quantity:** {}", quantity));
}
if let Some(ref note) = ingredient.note {
parts.push(format!("**Note:** {}", note));
}
parts.join("\n\n")
}
fn format_cookware_hover(cookware: &cooklang::model::Cookware) -> String {
let mut parts = Vec::new();
parts.push(format!("**Cookware:** {}", cookware.name));
if let Some(ref quantity) = cookware.quantity {
parts.push(format!("**Quantity:** {}", quantity));
}
if let Some(ref note) = cookware.note {
parts.push(format!("**Note:** {}", note));
}
parts.join("\n\n")
}
fn format_timer_hover(timer: &cooklang::model::Timer) -> String {
let mut parts = Vec::new();
if let Some(ref name) = timer.name {
parts.push(format!("**Timer:** {}", name));
} else {
parts.push("**Timer**".to_string());
}
if let Some(ref quantity) = timer.quantity {
parts.push(format!("**Duration:** {}", quantity));
}
parts.join("\n\n")
}