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_with_chain};
#[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(ctx) = find_type_ref_at_position(index, file, line, col) {
if let Some(target_symbol) = resolve_type_ref_with_chain(index, &ctx) {
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: ctx.type_ref.start_line,
start_col: ctx.type_ref.start_col,
end_line: ctx.type_ref.end_line,
end_col: ctx.type_ref.end_col,
});
} else {
let contents = format!(
"```sysml\n{}\n```\n\n**Symbol not resolved**\n\nThe symbol `{}` is not visible in this scope. \
You may need to add an import statement.",
ctx.target_name, ctx.target_name
);
return Some(HoverResult {
contents,
qualified_name: None,
is_definition: false,
relationships: Vec::new(),
start_line: ctx.type_ref.start_line,
start_col: ctx.type_ref.start_col,
end_line: ctx.type_ref.end_line,
end_col: ctx.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::PartDefinition
| SymbolKind::ItemDefinition
| SymbolKind::ActionDefinition
| SymbolKind::PortDefinition
| SymbolKind::AttributeDefinition
| SymbolKind::ConnectionDefinition
| SymbolKind::InterfaceDefinition
| SymbolKind::AllocationDefinition
| SymbolKind::RequirementDefinition
| SymbolKind::ConstraintDefinition
| SymbolKind::StateDefinition
| SymbolKind::CalculationDefinition
| SymbolKind::UseCaseDefinition
| SymbolKind::AnalysisCaseDefinition
| SymbolKind::ConcernDefinition
| SymbolKind::ViewDefinition
| SymbolKind::ViewpointDefinition
| SymbolKind::RenderingDefinition
| SymbolKind::EnumerationDefinition
| SymbolKind::MetadataDefinition
| SymbolKind::Interaction
| SymbolKind::DataType
| SymbolKind::Class
| SymbolKind::Structure
| SymbolKind::Behavior
| SymbolKind::Function
| SymbolKind::Association => {
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::TransitionUsage
| SymbolKind::CalculationUsage
| SymbolKind::ReferenceUsage
| SymbolKind::OccurrenceUsage
| SymbolKind::FlowConnectionUsage
| SymbolKind::ViewUsage
| SymbolKind::ViewpointUsage
| SymbolKind::RenderingUsage => {
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
| SymbolKind::ExposeRelationship => 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::*;
use crate::hir::new_element_id;
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),
element_id: new_element_id(),
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,
view_data: None,
metadata_annotations: Vec::new(),
is_abstract: false,
is_variation: false,
is_readonly: false,
is_derived: false,
is_parallel: false,
is_individual: false,
is_end: false,
is_default: false,
is_ordered: false,
is_nonunique: false,
is_portion: false,
direction: None,
multiplicity: None,
}
}
#[test]
fn test_hover_part_def() {
let mut index = SymbolIndex::new();
let mut def = make_symbol("Car", "Vehicle::Car", SymbolKind::PartDefinition, 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");
}
}