use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use capnp::serialize;
use crate::schema_capnp;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeKind {
File,
Struct,
Enum,
Interface,
Const,
Annotation,
Other,
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct NodeInfo {
pub id: u64,
pub display_name: String,
pub short_name: String,
pub kind: NodeKind,
pub file: PathBuf,
pub start_byte: u32,
pub end_byte: u32,
pub scope_id: u64,
pub doc_comment: Option<String>,
pub parameters: Vec<String>,
pub fields: Vec<FieldInfo>,
pub annotation_value_type: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct FieldInfo {
pub name: String,
pub type_str: String,
}
#[derive(Debug, Clone, Copy)]
pub struct IdentRef {
pub start_byte: u32,
pub end_byte: u32,
pub target_node_id: u64,
}
#[derive(Debug, Default, Clone)]
pub struct FileIndex {
pub identifiers: Vec<IdentRef>,
pub imports: HashMap<u64, String>,
}
#[derive(Debug, Default, Clone)]
pub struct Index {
pub nodes: HashMap<u64, NodeInfo>,
pub files: HashMap<PathBuf, FileIndex>,
}
impl Index {
pub fn from_cgr_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.is_empty() {
return Ok(Self::default());
}
let reader = serialize::read_message_from_flat_slice(
&mut &bytes[..],
Default::default(),
)?;
let cgr =
reader.get_root::<schema_capnp::code_generator_request::Reader>()?;
let mut idx = Self::default();
let mut src_ranges: HashMap<u64, (u32, u32)> = HashMap::new();
let mut doc_comments: HashMap<u64, String> = HashMap::new();
for si in cgr.get_source_info()?.iter() {
let s = si.get_start_byte();
let e = si.get_end_byte();
if s != 0 || e != 0 {
src_ranges.insert(si.get_id(), (s, e));
}
if si.has_doc_comment() {
let dc = si.get_doc_comment()?.to_string()?;
if !dc.is_empty() {
doc_comments.insert(si.get_id(), dc);
}
}
}
let mut display_name_by_id: HashMap<u64, String> = HashMap::new();
for node in cgr.get_nodes()?.iter() {
let dn = node.get_display_name()?.to_string()?;
let prefix_len = node.get_display_name_prefix_length() as usize;
let short = dn.get(prefix_len..).unwrap_or("").to_string();
display_name_by_id
.insert(node.get_id(), if short.is_empty() { dn } else { short });
}
for node in cgr.get_nodes()?.iter() {
let id = node.get_id();
let display_name = node.get_display_name()?.to_string()?;
let prefix_len = node.get_display_name_prefix_length() as usize;
let short_name = display_name.get(prefix_len..).unwrap_or("").to_string();
let file = file_of_display_name(&display_name);
let mut parameters: Vec<String> = Vec::new();
if node.has_parameters() {
for p in node.get_parameters()?.iter() {
parameters.push(p.get_name()?.to_string()?);
}
}
let mut fields: Vec<FieldInfo> = Vec::new();
let mut annotation_value_type: Option<u64> = None;
use schema_capnp::node::Which as NodeWhich;
let kind = match node.which() {
Ok(NodeWhich::File(())) => NodeKind::File,
Ok(NodeWhich::Struct(s)) => {
for f in s.get_fields()?.iter() {
let name = f.get_name()?.to_string()?;
let type_str = match f.which() {
Ok(schema_capnp::field::Slot(slot)) => {
let ty = slot.get_type()?;
format!(":{}", render_type(&ty, &display_name_by_id))
}
_ => String::new(),
};
fields.push(FieldInfo { name, type_str });
}
NodeKind::Struct
}
Ok(NodeWhich::Enum(_)) => NodeKind::Enum,
Ok(NodeWhich::Interface(_)) => NodeKind::Interface,
Ok(NodeWhich::Const(_)) => NodeKind::Const,
Ok(NodeWhich::Annotation(a)) => {
let ty = a.get_type()?;
annotation_value_type = type_target_id(&ty);
NodeKind::Annotation
}
_ => NodeKind::Other,
};
let mut start_byte = node.get_start_byte();
let mut end_byte = node.get_end_byte();
if start_byte == 0 && end_byte == 0 {
if let Some(&(s, e)) = src_ranges.get(&id) {
start_byte = s;
end_byte = e;
}
}
idx.nodes.insert(
id,
NodeInfo {
id,
display_name,
short_name,
kind,
file,
start_byte,
end_byte,
scope_id: node.get_scope_id(),
doc_comment: doc_comments.remove(&id),
parameters,
fields,
annotation_value_type,
},
);
}
for req in cgr.get_requested_files()?.iter() {
let filename = req.get_filename()?.to_string()?;
let path = PathBuf::from(&filename);
let mut idents: Vec<IdentRef> = Vec::new();
if req.has_file_source_info() {
let fsi = req.get_file_source_info()?;
for ident in fsi.get_identifiers()?.iter() {
let target = match ident.which() {
Ok(schema_capnp::code_generator_request::requested_file::file_source_info::identifier::TypeId(id)) => id,
Ok(schema_capnp::code_generator_request::requested_file::file_source_info::identifier::Member(_)) => 0,
_ => 0,
};
idents.push(IdentRef {
start_byte: ident.get_start_byte(),
end_byte: ident.get_end_byte(),
target_node_id: target,
});
}
}
idents.sort_by_key(|i| i.start_byte);
let mut imports = HashMap::new();
for imp in req.get_imports()?.iter() {
let name = imp.get_name()?.to_string()?;
imports.insert(imp.get_id(), name);
}
idx.files.insert(
path,
FileIndex {
identifiers: idents,
imports,
},
);
}
Ok(idx)
}
pub fn identifiers_at(&self, file: &Path, byte_offset: u32) -> Vec<IdentRef> {
let Some(fi) = self.lookup_file(file) else {
return Vec::new();
};
let mut hits: Vec<IdentRef> = fi
.identifiers
.iter()
.copied()
.filter(|i| byte_offset >= i.start_byte && byte_offset < i.end_byte)
.collect();
hits.sort_by_key(|i| i.end_byte - i.start_byte);
hits
}
pub fn identifier_at(
&self,
file: &Path,
byte_offset: u32,
) -> Option<IdentRef> {
self.identifiers_at(file, byte_offset).into_iter().next()
}
pub fn identifier_in_range(
&self,
file: &Path,
start: u32,
end: u32,
) -> Option<IdentRef> {
let fi = self.lookup_file(file)?;
fi.identifiers
.iter()
.copied()
.find(|i| i.start_byte == start && i.end_byte == end)
}
pub fn node(&self, id: u64) -> Option<&NodeInfo> {
self.nodes.get(&id)
}
pub fn remap_file(&mut self, from: &Path, to: &Path) {
let matches = |p: &Path| paths_match(p, from);
let old = std::mem::take(&mut self.files);
for (path, fi) in old {
let new_key = if matches(&path) {
to.to_path_buf()
} else {
path
};
self.files.insert(new_key, fi);
}
for node in self.nodes.values_mut() {
if matches(&node.file) {
node.file = to.to_path_buf();
}
}
}
pub fn import_petname(
&self,
requesting: &Path,
type_id: u64,
) -> Option<&str> {
self
.lookup_file(requesting)?
.imports
.get(&type_id)
.map(String::as_str)
}
fn lookup_file(&self, file: &Path) -> Option<&FileIndex> {
if let Some(v) = self.files.get(file) {
return Some(v);
}
let s = file.to_string_lossy();
let stripped = s.strip_prefix('/').unwrap_or(&s);
if let Some(v) = self.files.get(Path::new(stripped)) {
return Some(v);
}
let with_slash = format!("/{stripped}");
if let Some(v) = self.files.get(Path::new(&with_slash)) {
return Some(v);
}
let name = file.file_name()?;
self
.files
.iter()
.find(|(p, _)| p.file_name() == Some(name))
.map(|(_, v)| v)
}
pub fn find_node_by_short_name(
&self,
name: &str,
prefer_file: &Path,
) -> Option<&NodeInfo> {
let mut best: Option<&NodeInfo> = None;
for n in self.nodes.values() {
let leaf = n.short_name.rsplit('.').next().unwrap_or(&n.short_name);
if leaf != name {
continue;
}
if paths_match(&n.file, prefer_file) {
return Some(n);
}
if best.is_none() {
best = Some(n);
}
}
best
}
pub fn completion_candidates(&self) -> impl Iterator<Item = &NodeInfo> {
self.nodes.values().filter(|n| {
!n.short_name.is_empty()
&& matches!(
n.kind,
NodeKind::Struct
| NodeKind::Enum
| NodeKind::Interface
| NodeKind::Annotation
| NodeKind::Const
)
})
}
pub fn type_candidates(&self) -> impl Iterator<Item = &NodeInfo> {
self.nodes.values().filter(|n| {
!n.short_name.is_empty()
&& matches!(
n.kind,
NodeKind::Struct
| NodeKind::Enum
| NodeKind::Interface
| NodeKind::Const
)
})
}
pub fn annotation_candidates(&self) -> impl Iterator<Item = &NodeInfo> {
self.nodes.values().filter(|n| {
!n.short_name.is_empty() && matches!(n.kind, NodeKind::Annotation)
})
}
pub fn nested_candidates(&self, parent_id: u64) -> Vec<&NodeInfo> {
self
.nodes
.values()
.filter(|n| {
n.scope_id == parent_id
&& !n.short_name.is_empty()
&& !matches!(n.kind, NodeKind::File | NodeKind::Other)
})
.collect()
}
pub fn candidates_in_file(&self, file: &Path) -> Vec<&NodeInfo> {
self
.nodes
.values()
.filter(|n| {
!n.short_name.is_empty()
&& paths_match(&n.file, file)
&& !matches!(n.kind, NodeKind::File | NodeKind::Other)
})
.collect()
}
}
fn render_type(
ty: &schema_capnp::type_::Reader,
names: &HashMap<u64, String>,
) -> String {
use schema_capnp::type_::Which::*;
match ty.which() {
Ok(Void(())) => "Void".into(),
Ok(Bool(())) => "Bool".into(),
Ok(Int8(())) => "Int8".into(),
Ok(Int16(())) => "Int16".into(),
Ok(Int32(())) => "Int32".into(),
Ok(Int64(())) => "Int64".into(),
Ok(Uint8(())) => "UInt8".into(),
Ok(Uint16(())) => "UInt16".into(),
Ok(Uint32(())) => "UInt32".into(),
Ok(Uint64(())) => "UInt64".into(),
Ok(Float32(())) => "Float32".into(),
Ok(Float64(())) => "Float64".into(),
Ok(Text(())) => "Text".into(),
Ok(Data(())) => "Data".into(),
Ok(List(l)) => match l.get_element_type() {
Ok(inner) => format!("List({})", render_type(&inner, names)),
Err(_) => "List(?)".into(),
},
Ok(Enum(e)) => name_or_id(names, e.get_type_id()),
Ok(Struct(s)) => name_or_id(names, s.get_type_id()),
Ok(Interface(i)) => name_or_id(names, i.get_type_id()),
Ok(AnyPointer(_)) => "AnyPointer".into(),
Err(_) => "?".into(),
}
}
fn name_or_id(names: &HashMap<u64, String>, id: u64) -> String {
names
.get(&id)
.cloned()
.unwrap_or_else(|| format!("@0x{id:016x}"))
}
fn type_target_id(ty: &schema_capnp::type_::Reader) -> Option<u64> {
use schema_capnp::type_::Which::*;
match ty.which() {
Ok(Struct(s)) => Some(s.get_type_id()),
Ok(Enum(e)) => Some(e.get_type_id()),
Ok(Interface(i)) => Some(i.get_type_id()),
_ => None,
}
}
fn paths_match(a: &Path, b: &Path) -> bool {
if a == b {
return true;
}
let as_ = a.to_string_lossy();
let bs = b.to_string_lossy();
let ans = as_.strip_prefix('/').unwrap_or(&as_);
let bns = bs.strip_prefix('/').unwrap_or(&bs);
if ans == bns {
return true;
}
match (a.file_name(), b.file_name()) {
(Some(x), Some(y)) => x == y,
_ => false,
}
}
fn file_of_display_name(display_name: &str) -> PathBuf {
let file = match display_name.find(':') {
Some(i) => &display_name[..i],
None => display_name,
};
PathBuf::from(file)
}
fn _assert_anyhow_used() -> Result<()> {
Err(anyhow!("unused"))
}