use crate::actions;
use rite_model::{ArtifactId, Ceremony, MaterialId, MaterialKind};
use rite_resolver::{ReferenceTarget, SpanMap};
use tower_lsp_server::ls_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position};
pub fn hover_at(
text: &str,
span_map: &SpanMap,
resolved: Option<&Ceremony>,
pos: Position,
) -> Option<Hover> {
let line = pos.line as usize + 1; let col = pos.character as usize + 1;
span_map
.find_target_at(line, col)
.and_then(|t| hover_for_target(t, span_map, resolved))
.map(markdown_hover)
.or_else(|| {
let word = crate::convert::word_at_position(text, pos)?;
Some(markdown_hover(
actions::hover_description(&word)?.to_string(),
))
})
}
fn hover_for_target(
target: &ReferenceTarget,
span_map: &SpanMap,
resolved: Option<&Ceremony>,
) -> Option<String> {
if let ReferenceTarget::Artifact(id) = target {
return Some(artifact_hover(id, span_map, resolved));
}
let resolved = resolved?;
match target {
ReferenceTarget::Section(id) => {
let section = resolved.sections.get(id)?;
let mut md = format!("**Section** `{}`", id);
if let Some(name) = §ion.name {
md.push_str(&format!("\n\n{name}"));
}
if let Some(desc) = §ion.description {
md.push_str(&format!("\n\n{desc}"));
}
if let Some(act_id) = §ion.act {
md.push_str(&format!("\n\n*Act: `{act_id}`*"));
}
Some(md)
}
ReferenceTarget::Role(id) => {
let role = resolved.roles.get(id)?;
let mut md = format!("**Role** `{}`", id);
if role.name != id.as_str() {
md.push_str(&format!(", {}", role.name));
}
if role.role_type != id.as_str() {
md.push_str(&format!("\n\nType: `{}`", role.role_type));
}
if let Some(person) = &role.person {
md.push_str(&format!("\n\nPerson: {person}"));
}
Some(md)
}
ReferenceTarget::Act(id) => {
let act = resolved.acts.get(id)?;
let mut md = format!("**Act** `{}`", id);
if let Some(name) = &act.name {
md.push_str(&format!("\n\n{name}"));
}
if let Some(desc) = &act.description {
md.push_str(&format!("\n\n{desc}"));
}
Some(md)
}
ReferenceTarget::Param(id) => {
let param = resolved.parameters.get(id)?;
let mut md = format!("**param** · `{}`", param.declared_type);
if let Some(desc) = ¶m.description {
md.push_str(&format!("\n\n{desc}"));
}
Some(md)
}
ReferenceTarget::Material(id) => {
let material = resolved.materials.get(id)?;
let kind_str = match &material.kind {
MaterialKind::Digital { .. } => "digital",
MaterialKind::Physical { .. } => "physical",
};
let mut md = format!("**material** · `{kind_str}`");
if let Some(title) = &material.title {
md.push_str(&format!("\n\n{title}"));
}
if let Some(desc) = &material.description {
md.push_str(&format!("\n\n{desc}"));
}
Some(md)
}
ReferenceTarget::Backend(name) => {
let backend = resolved.backends.get(name.as_str())?;
Some(format!(
"**backend** `{name}` · provider: `{}`",
backend.provider
))
}
_ => None,
}
}
fn artifact_hover(id: &ArtifactId, span_map: &SpanMap, resolved: Option<&Ceremony>) -> String {
if span_map.artifacts.contains_key(id) {
return format!("**artifact** `{id}` · produced upstream");
}
if let Some(ceremony) = resolved
&& let Some(material) = ceremony.materials.get(&MaterialId::new(id.as_str()))
{
let kind_str = match &material.kind {
MaterialKind::Digital { .. } => "digital",
MaterialKind::Physical { .. } => "physical",
};
let mut md = format!("**artifact** `{id}` · material · `{kind_str}`");
if let Some(title) = &material.title {
md.push_str(&format!("\n\n{title}"));
}
if let Some(desc) = &material.description {
md.push_str(&format!("\n\n{desc}"));
}
return md;
}
format!("**artifact** `{id}`")
}
fn markdown_hover(value: String) -> Hover {
Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value,
}),
range: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rite_model::{ArtifactId, StepId};
use rite_resolver::{ReferenceContext, ReferenceEntry, Span};
#[test]
fn extracts_word_at_middle() {
let text = " action: confirm\n";
let pos = Position {
line: 0,
character: 14,
};
assert_eq!(
crate::convert::word_at_position(text, pos).as_deref(),
Some("confirm")
);
}
#[test]
fn action_hover_via_fallback() {
let span_map = SpanMap::default();
let text = " action: confirm\n";
let pos = Position {
line: 0,
character: 14,
};
assert!(hover_at(text, &span_map, None, pos).is_some());
}
#[test]
fn returns_none_for_unknown_word() {
let span_map = SpanMap::default();
let text = " action: unknown_action\n";
let pos = Position {
line: 0,
character: 14,
};
assert!(hover_at(text, &span_map, None, pos).is_none());
}
#[test]
fn artifact_hover_says_produced_when_creates_site_present() {
let mut span_map = SpanMap::default();
span_map.artifacts.insert(
ArtifactId::new("keypair"),
Span {
line: 5,
column: 7,
length: Some(20),
},
);
span_map.references.push(ReferenceEntry {
span: Span {
line: 12,
column: 14,
length: Some(20),
},
target: ReferenceTarget::Artifact(ArtifactId::new("keypair")),
context: ReferenceContext::Step(StepId::new("use_step")),
value: "${artifact.keypair}".to_string(),
});
let pos = Position {
line: 11,
character: 14,
};
let hover = hover_at("", &span_map, None, pos).expect("hover");
if let HoverContents::Markup(MarkupContent { value, .. }) = hover.contents {
assert!(value.contains("produced"), "got: {value}");
assert!(value.contains("keypair"), "got: {value}");
} else {
panic!("expected markup hover");
}
}
#[test]
fn artifact_hover_falls_through_to_basic_when_no_metadata() {
let mut span_map = SpanMap::default();
span_map.references.push(ReferenceEntry {
span: Span {
line: 12,
column: 14,
length: Some(15),
},
target: ReferenceTarget::Artifact(ArtifactId::new("ghost")),
context: ReferenceContext::Step(StepId::new("use_step")),
value: "${artifact.ghost}".to_string(),
});
let pos = Position {
line: 11,
character: 14,
};
let hover = hover_at("", &span_map, None, pos).expect("hover");
if let HoverContents::Markup(MarkupContent { value, .. }) = hover.contents {
assert!(value.contains("ghost"), "got: {value}");
assert!(!value.contains("produced"), "got: {value}");
} else {
panic!("expected markup hover");
}
}
}