use super::*;
use crate::project::{ClassLocation, class_name_at_offset, locate_class_def};
fn location_to_item(
name: &str,
loc: &ClassLocation,
uri: &Uri,
line_index: &LineIndex,
) -> TypeHierarchyItem {
TypeHierarchyItem {
name: name.to_string(),
kind: LspSymbolKind::CLASS,
tags: None,
detail: Some(loc.system.label().to_string()),
uri: uri.clone(),
range: text_range_to_lsp_range(line_index, loc.full_range),
selection_range: text_range_to_lsp_range(line_index, loc.name_range),
data: None,
}
}
fn class_item(snapshot: &Analysis, path: &Path, name: &str) -> Option<TypeHierarchyItem> {
let file = snapshot.lookup_file(path)?;
let uri = uri::from_path(path)?;
let root = snapshot.parsed_tree(file);
let loc = locate_class_def(&root, name)?;
let line_index = LineIndex::new(snapshot.file_text(file));
Some(location_to_item(name, &loc, &uri, &line_index))
}
pub(crate) fn prepare_type_hierarchy_via_db(
snapshot: &Analysis,
path: &Path,
uri: &Uri,
text: &str,
position: Position,
) -> Option<Vec<TypeHierarchyItem>> {
let line_index = LineIndex::new(text);
let offset = TextSize::new(line_index.position_to_byte(position).min(text.len()) as u32);
let root = parse(text).cst;
let name = class_name_at_offset(&root, offset)?;
let mut items: Vec<TypeHierarchyItem> = Vec::new();
if let Some(loc) = locate_class_def(&root, &name) {
items.push(location_to_item(&name, &loc, uri, &line_index));
}
let cross = salsa::Cancelled::catch(AssertUnwindSafe(|| {
snapshot
.class_def_sites(&name)
.into_iter()
.filter(|(def_path, _)| def_path != path)
.filter_map(|(def_path, _)| class_item(snapshot, &def_path, &name))
.collect::<Vec<_>>()
}))
.unwrap_or_default();
items.extend(cross);
(!items.is_empty()).then_some(items)
}
pub(crate) fn supertypes_via_db(
snapshot: &Analysis,
item: &TypeHierarchyItem,
) -> Option<Vec<TypeHierarchyItem>> {
class_relatives(snapshot, item, Edge::Super)
}
pub(crate) fn subtypes_via_db(
snapshot: &Analysis,
item: &TypeHierarchyItem,
) -> Option<Vec<TypeHierarchyItem>> {
class_relatives(snapshot, item, Edge::Sub)
}
#[derive(Clone, Copy)]
enum Edge {
Super,
Sub,
}
fn class_relatives(
snapshot: &Analysis,
item: &TypeHierarchyItem,
edge: Edge,
) -> Option<Vec<TypeHierarchyItem>> {
let name = item.name.clone();
salsa::Cancelled::catch(AssertUnwindSafe(|| {
let related = match edge {
Edge::Super => snapshot.class_supertypes(&name),
Edge::Sub => snapshot.class_subtypes(&name),
};
related
.into_iter()
.filter_map(|rel| {
snapshot
.class_def_sites(&rel)
.into_iter()
.find_map(|(path, _)| class_item(snapshot, &path, &rel))
})
.collect::<Vec<_>>()
}))
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn prepare_at(
snapshot: &Analysis,
path: &Path,
text: &str,
offset: usize,
) -> Vec<TypeHierarchyItem> {
let uri = uri::from_path(path).unwrap();
prepare_type_hierarchy_via_db(snapshot, path, &uri, text, pos_at(text, offset))
.unwrap_or_default()
}
fn item_named(snapshot: &Analysis, path: &Path, name: &str) -> TypeHierarchyItem {
class_item(snapshot, path, name).expect("class item")
}
#[test]
fn prepare_on_a_class_name_yields_its_item() {
let src = "setClass(\"Animal\")\n";
let snapshot = rename_workspace(src, "");
let items = prepare_at(&snapshot, &ws_path("a.R"), src, src.find("Animal").unwrap());
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "Animal");
assert_eq!(items[0].kind, LspSymbolKind::CLASS);
assert_eq!(items[0].detail.as_deref(), Some("S4 class"));
}
#[test]
fn prepare_on_a_contains_reference_yields_the_parent_item() {
let src = "setClass(\"Animal\")\nsetClass(\"Dog\", contains = \"Animal\")\n";
let snapshot = rename_workspace(src, "");
let offset = src.find("\"Animal\")").unwrap() + 1; let items = prepare_at(&snapshot, &ws_path("a.R"), src, offset);
assert_eq!(items.len(), 1);
assert_eq!(items[0].name, "Animal");
}
#[test]
fn prepare_declines_a_non_class_string() {
let src = "x <- \"Animal\"\n";
let snapshot = rename_workspace(src, "");
let items = prepare_at(&snapshot, &ws_path("a.R"), src, src.find("Animal").unwrap());
assert!(items.is_empty());
}
#[test]
fn supertypes_reports_s4_contains_parents() {
let src = "setClass(\"Animal\")\nsetClass(\"Dog\", contains = c(\"Animal\", \"Pet\"))\nsetClass(\"Pet\")\n";
let snapshot = rename_workspace(src, "");
let supers = supertypes_via_db(&snapshot, &item_named(&snapshot, &ws_path("a.R"), "Dog"))
.expect("supertypes");
let mut names: Vec<&str> = supers.iter().map(|i| i.name.as_str()).collect();
names.sort();
assert_eq!(names, ["Animal", "Pet"]);
}
#[test]
fn supertypes_reports_r6_inherit_parent() {
let src = "Animal <- R6Class(\"Animal\")\nDog <- R6Class(\"Dog\", inherit = Animal)\n";
let snapshot = rename_workspace(src, "");
let supers = supertypes_via_db(&snapshot, &item_named(&snapshot, &ws_path("a.R"), "Dog"))
.expect("supertypes");
assert_eq!(supers.len(), 1);
assert_eq!(supers[0].name, "Animal");
assert_eq!(supers[0].detail.as_deref(), Some("R6 class"));
}
#[test]
fn supertypes_omits_a_parent_defined_nowhere() {
let src = "setClass(\"Dog\", contains = \"Base\")\n";
let snapshot = rename_workspace(src, "");
let supers = supertypes_via_db(&snapshot, &item_named(&snapshot, &ws_path("a.R"), "Dog"))
.expect("supertypes");
assert!(supers.is_empty(), "undefined parent has no item");
}
#[test]
fn subtypes_reports_direct_children() {
let src = "setClass(\"Animal\")\nsetClass(\"Dog\", contains = \"Animal\")\n";
let snapshot = rename_workspace(src, "");
let subs = subtypes_via_db(&snapshot, &item_named(&snapshot, &ws_path("a.R"), "Animal"))
.expect("subtypes");
assert_eq!(subs.len(), 1);
assert_eq!(subs[0].name, "Dog");
}
#[test]
fn subtypes_resolves_a_cross_file_child() {
let a_src = "setClass(\"Animal\")\n";
let b_src = "setClass(\"Dog\", contains = \"Animal\")\n";
let snapshot = rename_workspace(a_src, b_src);
let subs = subtypes_via_db(&snapshot, &item_named(&snapshot, &ws_path("a.R"), "Animal"))
.expect("subtypes");
assert_eq!(subs.len(), 1);
assert_eq!(subs[0].name, "Dog");
assert_eq!(subs[0].uri, uri::from_path(&ws_path("b.R")).unwrap());
}
#[test]
fn leaf_class_has_no_subtypes() {
let src = "setClass(\"Animal\")\nsetClass(\"Dog\", contains = \"Animal\")\n";
let snapshot = rename_workspace(src, "");
let subs = subtypes_via_db(&snapshot, &item_named(&snapshot, &ws_path("a.R"), "Dog"))
.expect("subtypes");
assert!(subs.is_empty());
}
}