use std::sync::Arc;
use crate::base::FileId;
use crate::hir::{HirRelationship, HirSymbol, RelationshipKind, SymbolIndex, SymbolKind};
use crate::ide::type_info::{find_type_ref_at_position, resolve_type_ref};
#[derive(Clone, Debug)]
pub struct ResolvedRelationship {
pub kind: RelationshipKind,
pub target_name: Arc<str>,
pub target_file: Option<FileId>,
pub target_line: Option<u32>,
}
#[derive(Clone, Debug)]
pub struct HoverResult {
pub contents: String,
pub qualified_name: Option<Arc<str>>,
pub is_definition: bool,
pub relationships: Vec<ResolvedRelationship>,
pub start_line: u32,
pub start_col: u32,
pub end_line: u32,
pub end_col: u32,
}
impl HoverResult {
pub fn new(contents: String, symbol: &HirSymbol, index: &SymbolIndex) -> Self {
Self {
contents,
qualified_name: Some(symbol.qualified_name.clone()),
is_definition: symbol.kind.is_definition(),
relationships: resolve_relationships(&symbol.relationships, index),
start_line: symbol.start_line,
start_col: symbol.start_col,
end_line: symbol.end_line,
end_col: symbol.end_col,
}
}
}
fn resolve_relationships(
relationships: &[HirRelationship],
index: &SymbolIndex,
) -> Vec<ResolvedRelationship> {
relationships
.iter()
.map(|rel| {
let target_name = rel.target.clone();
let target_symbol = index
.lookup_qualified(&target_name)
.or_else(|| index.lookup_definition(&target_name))
.or_else(|| index.lookup_simple(&target_name).into_iter().next())
.or_else(|| {
if let Some(simple_name) = target_name.rsplit("::").next() {
index
.lookup_definition(simple_name)
.or_else(|| index.lookup_simple(simple_name).into_iter().next())
} else {
None
}
});
ResolvedRelationship {
kind: rel.kind,
target_name,
target_file: target_symbol.map(|s| s.file),
target_line: target_symbol.map(|s| s.start_line),
}
})
.collect()
}
pub fn hover(index: &SymbolIndex, file: FileId, line: u32, col: u32) -> Option<HoverResult> {
if let Some((target_name, type_ref, containing_symbol)) =
find_type_ref_at_position(index, file, line, col)
{
if let Some(target_symbol) =
resolve_type_ref(index, type_ref, &target_name, containing_symbol)
{
let contents = build_hover_content(&target_symbol, index);
return Some(HoverResult {
contents,
qualified_name: Some(target_symbol.qualified_name.clone()),
is_definition: target_symbol.kind.is_definition(),
relationships: resolve_relationships(&target_symbol.relationships, index),
start_line: type_ref.start_line,
start_col: type_ref.start_col,
end_line: type_ref.end_line,
end_col: type_ref.end_col,
});
}
}
let symbol = find_symbol_at_position(index, file, line, col)?;
let contents = build_hover_content(symbol, index);
Some(HoverResult::new(contents, symbol, index))
}
fn build_hover_content(symbol: &HirSymbol, _index: &SymbolIndex) -> String {
let mut content = String::new();
content.push_str("```sysml\n");
content.push_str(&build_signature(symbol));
content.push_str("\n```\n");
if let Some(ref doc) = symbol.doc {
content.push_str("\n---\n\n");
content.push_str(doc);
content.push('\n');
}
content.push_str("\n**Qualified Name:** `");
content.push_str(&symbol.qualified_name);
content.push_str("`\n");
content
}
fn build_signature(symbol: &HirSymbol) -> String {
let kind_str = symbol.kind.display();
let name_with_alias = if let Some(ref short) = symbol.short_name {
if short.as_ref() != symbol.name.as_ref() {
format!("<{}> {}", short, symbol.name)
} else {
symbol.name.to_string()
}
} else {
symbol.name.to_string()
};
match symbol.kind {
SymbolKind::PartDef
| SymbolKind::ItemDef
| SymbolKind::ActionDef
| SymbolKind::PortDef
| SymbolKind::AttributeDef
| SymbolKind::ConnectionDef
| SymbolKind::InterfaceDef
| SymbolKind::AllocationDef
| SymbolKind::RequirementDef
| SymbolKind::ConstraintDef
| SymbolKind::StateDef
| SymbolKind::CalculationDef
| SymbolKind::UseCaseDef
| SymbolKind::AnalysisCaseDef
| SymbolKind::ConcernDef
| SymbolKind::ViewDef
| SymbolKind::ViewpointDef
| SymbolKind::RenderingDef
| SymbolKind::EnumerationDef => {
let mut sig = format!("{} {}", kind_str, name_with_alias);
if !symbol.supertypes.is_empty() {
sig.push_str(" :> ");
sig.push_str(&symbol.supertypes.join(", "));
}
sig
}
SymbolKind::PartUsage
| SymbolKind::ItemUsage
| SymbolKind::ActionUsage
| SymbolKind::PortUsage
| SymbolKind::AttributeUsage
| SymbolKind::ConnectionUsage
| SymbolKind::InterfaceUsage
| SymbolKind::AllocationUsage
| SymbolKind::RequirementUsage
| SymbolKind::ConstraintUsage
| SymbolKind::StateUsage
| SymbolKind::CalculationUsage
| SymbolKind::ReferenceUsage
| SymbolKind::OccurrenceUsage
| SymbolKind::FlowUsage => {
let mut sig = format!("{} {}", kind_str, name_with_alias);
if !symbol.supertypes.is_empty() {
sig.push_str(" : ");
sig.push_str(symbol.supertypes[0].as_ref());
}
sig
}
SymbolKind::Package => format!("package {}", name_with_alias),
SymbolKind::Import => format!("import {}", symbol.name),
SymbolKind::Alias => {
if !symbol.supertypes.is_empty() {
format!("alias {} for {}", name_with_alias, symbol.supertypes[0])
} else {
format!("alias {}", name_with_alias)
}
}
SymbolKind::Comment | SymbolKind::Other | SymbolKind::Dependency => name_with_alias,
}
}
fn find_symbol_at_position(
index: &SymbolIndex,
file: FileId,
line: u32,
col: u32,
) -> Option<&HirSymbol> {
let symbols = index.symbols_in_file(file);
let mut best: Option<&HirSymbol> = None;
for symbol in symbols {
if contains_position(symbol, line, col) || contains_short_name_position(symbol, line, col) {
match best {
None => best = Some(symbol),
Some(current) => {
if symbol_size(symbol) < symbol_size(current) {
best = Some(symbol);
}
}
}
}
}
best
}
fn contains_position(symbol: &HirSymbol, line: u32, col: u32) -> bool {
let after_start =
line > symbol.start_line || (line == symbol.start_line && col >= symbol.start_col);
let before_end = line < symbol.end_line || (line == symbol.end_line && col <= symbol.end_col);
after_start && before_end
}
fn contains_short_name_position(symbol: &HirSymbol, line: u32, col: u32) -> bool {
let (Some(start_line), Some(start_col), Some(end_line), Some(end_col)) = (
symbol.short_name_start_line,
symbol.short_name_start_col,
symbol.short_name_end_line,
symbol.short_name_end_col,
) else {
return false;
};
let after_start = line > start_line || (line == start_line && col >= start_col);
let before_end = line < end_line || (line == end_line && col <= end_col);
after_start && before_end
}
fn symbol_size(symbol: &HirSymbol) -> u32 {
let line_diff = symbol.end_line.saturating_sub(symbol.start_line);
let col_diff = symbol.end_col.saturating_sub(symbol.start_col);
line_diff * 1000 + col_diff
}
#[cfg(test)]
mod tests {
use super::*;
fn make_symbol(name: &str, qualified: &str, kind: SymbolKind, line: u32) -> HirSymbol {
HirSymbol {
name: Arc::from(name),
short_name: None,
qualified_name: Arc::from(qualified),
kind,
file: FileId::new(0),
start_line: line,
start_col: 0,
end_line: line,
end_col: 20,
short_name_start_line: None,
short_name_start_col: None,
short_name_end_line: None,
short_name_end_col: None,
doc: None,
supertypes: Vec::new(),
relationships: Vec::new(),
type_refs: Vec::new(),
is_public: false,
}
}
#[test]
fn test_hover_part_def() {
let mut index = SymbolIndex::new();
let mut def = make_symbol("Car", "Vehicle::Car", SymbolKind::PartDef, 5);
def.doc = Some(Arc::from("A car is a vehicle."));
def.supertypes = vec![Arc::from("Vehicle")];
index.add_file(FileId::new(0), vec![def]);
let result = hover(&index, FileId::new(0), 5, 5);
assert!(result.is_some());
let hover = result.unwrap();
assert!(hover.contents.contains("Part def Car"));
assert!(hover.contents.contains(":> Vehicle"));
assert!(hover.contents.contains("A car is a vehicle"));
}
#[test]
fn test_hover_usage() {
let mut index = SymbolIndex::new();
let mut usage = make_symbol("engine", "Car::engine", SymbolKind::PartUsage, 10);
usage.supertypes = vec![Arc::from("Engine")];
index.add_file(FileId::new(0), vec![usage]);
let result = hover(&index, FileId::new(0), 10, 5);
assert!(result.is_some());
let hover = result.unwrap();
assert!(hover.contents.contains("Part engine"));
assert!(hover.contents.contains(": Engine"));
}
#[test]
fn test_hover_not_found() {
let index = SymbolIndex::new();
let result = hover(&index, FileId::new(0), 0, 0);
assert!(result.is_none());
}
#[test]
fn test_build_signature_package() {
let symbol = make_symbol("Vehicle", "Vehicle", SymbolKind::Package, 0);
let sig = build_signature(&symbol);
assert_eq!(sig, "package Vehicle");
}
}