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,
encoding: PositionEncoding,
) -> 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, encoding),
selection_range: text_range_to_lsp_range(line_index, loc.name_range, encoding),
data: None,
}
}
fn class_item(
snapshot: &Analysis,
path: &Path,
name: &str,
encoding: PositionEncoding,
) -> 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 = snapshot.line_index(file);
Some(location_to_item(name, &loc, &uri, line_index, encoding))
}
pub(crate) fn prepare_type_hierarchy_via_db(
snapshot: &Analysis,
path: &Path,
uri: &Uri,
text: &str,
position: Position,
encoding: PositionEncoding,
) -> Option<Vec<TypeHierarchyItem>> {
let line_index = LineIndex::new(text);
let offset = TextSize::new(
line_index
.position_to_byte(position, encoding)
.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, encoding));
}
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, encoding))
.collect::<Vec<_>>()
}))
.unwrap_or_default();
items.extend(cross);
(!items.is_empty()).then_some(items)
}
pub(crate) fn supertypes_via_db(
snapshot: &Analysis,
item: &TypeHierarchyItem,
encoding: PositionEncoding,
) -> Option<Vec<TypeHierarchyItem>> {
class_relatives(snapshot, item, Edge::Super, encoding)
}
pub(crate) fn subtypes_via_db(
snapshot: &Analysis,
item: &TypeHierarchyItem,
encoding: PositionEncoding,
) -> Option<Vec<TypeHierarchyItem>> {
class_relatives(snapshot, item, Edge::Sub, encoding)
}
#[derive(Clone, Copy)]
enum Edge {
Super,
Sub,
}
fn class_relatives(
snapshot: &Analysis,
item: &TypeHierarchyItem,
edge: Edge,
encoding: PositionEncoding,
) -> 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, encoding))
})
.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),
PositionEncoding::Utf16,
)
.unwrap_or_default()
}
fn item_named(snapshot: &Analysis, path: &Path, name: &str) -> TypeHierarchyItem {
class_item(snapshot, path, name, PositionEncoding::Utf16).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"),
PositionEncoding::Utf16,
)
.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"),
PositionEncoding::Utf16,
)
.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"),
PositionEncoding::Utf16,
)
.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"),
PositionEncoding::Utf16,
)
.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"),
PositionEncoding::Utf16,
)
.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"),
PositionEncoding::Utf16,
)
.expect("subtypes");
assert!(subs.is_empty());
}
}