//! Span maps for real files and macro expansions.
use span::{EditionedFileId, HirFileId, HirFileIdRepr, MacroFileId, Span, SyntaxContext};
use stdx::TupleExt;
use syntax::{AstNode, TextRange, ast};
use triomphe::Arc;
pub use span::RealSpanMap;
use crate::{attrs::collect_attrs, db::ExpandDatabase};
pub type ExpansionSpanMap = span::SpanMap<SyntaxContext>;
/// Spanmap for a macro file or a real file
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SpanMap {
/// Spanmap for a macro file
ExpansionSpanMap(Arc<ExpansionSpanMap>),
/// Spanmap for a real file
RealSpanMap(Arc<RealSpanMap>),
}
#[derive(Copy, Clone)]
pub enum SpanMapRef<'a> {
/// Spanmap for a macro file
ExpansionSpanMap(&'a ExpansionSpanMap),
/// Spanmap for a real file
RealSpanMap(&'a RealSpanMap),
}
impl syntax_bridge::SpanMapper<Span> for SpanMap {
fn span_for(&self, range: TextRange) -> Span {
self.span_for_range(range)
}
}
impl syntax_bridge::SpanMapper<Span> for SpanMapRef<'_> {
fn span_for(&self, range: TextRange) -> Span {
self.span_for_range(range)
}
}
impl SpanMap {
pub fn span_for_range(&self, range: TextRange) -> Span {
match self {
// FIXME: Is it correct for us to only take the span at the start? This feels somewhat
// wrong. The context will be right, but the range could be considered wrong. See
// https://github.com/rust-lang/rust/issues/23480, we probably want to fetch the span at
// the start and end, then merge them like rustc does in `Span::to
Self::ExpansionSpanMap(span_map) => span_map.span_at(range.start()),
Self::RealSpanMap(span_map) => span_map.span_for_range(range),
}
}
pub fn as_ref(&self) -> SpanMapRef<'_> {
match self {
Self::ExpansionSpanMap(span_map) => SpanMapRef::ExpansionSpanMap(span_map),
Self::RealSpanMap(span_map) => SpanMapRef::RealSpanMap(span_map),
}
}
#[inline]
pub(crate) fn new(db: &dyn ExpandDatabase, file_id: HirFileId) -> SpanMap {
match file_id.repr() {
HirFileIdRepr::FileId(file_id) => SpanMap::RealSpanMap(db.real_span_map(file_id)),
HirFileIdRepr::MacroFile(m) => {
SpanMap::ExpansionSpanMap(db.parse_macro_expansion(m).value.1)
}
}
}
}
impl SpanMapRef<'_> {
pub fn span_for_range(self, range: TextRange) -> Span {
match self {
Self::ExpansionSpanMap(span_map) => span_map.span_at(range.start()),
Self::RealSpanMap(span_map) => span_map.span_for_range(range),
}
}
}
pub(crate) fn real_span_map(
db: &dyn ExpandDatabase,
editioned_file_id: EditionedFileId,
) -> Arc<RealSpanMap> {
use syntax::ast::HasModuleItem;
let mut pairs = vec![(syntax::TextSize::new(0), span::ROOT_ERASED_FILE_AST_ID)];
let ast_id_map = db.ast_id_map(editioned_file_id.into());
let file_id = base_db::EditionedFileId::new(db, editioned_file_id);
let tree = db.parse(file_id).tree();
// This is an incrementality layer. Basically we can't use absolute ranges for our spans as that
// would mean we'd invalidate everything whenever we type. So instead we make the text ranges
// relative to some AstIds reducing the risk of invalidation as typing somewhere no longer
// affects all following spans in the file.
// There is some stuff to bear in mind here though, for one, the more "anchors" we create, the
// easier it gets to invalidate things again as spans are as stable as their anchor's ID.
// The other problem is proc-macros. Proc-macros have a `Span::join` api that allows them
// to join two spans that come from the same file. rust-analyzer's proc-macro server
// can only join two spans if they belong to the same anchor though, as the spans are relative
// to that anchor. To do cross anchor joining we'd need to access to the ast id map to resolve
// them again, something we might get access to in the future. But even then, proc-macros doing
// this kind of joining makes them as stable as the AstIdMap (which is basically changing on
// every input of the file)…
let item_to_entry =
|item: ast::Item| (item.syntax().text_range().start(), ast_id_map.ast_id(&item).erase());
// Top level items make for great anchors as they are the most stable and a decent boundary
pairs.extend(tree.items().map(item_to_entry));
// Unfortunately, assoc items are very common in Rust, so descend into those as well and make
// them anchors too, but only if they have no attributes attached, as those might be proc-macros
// and using different anchors inside of them will prevent spans from being joinable.
tree.items().for_each(|item| match &item {
ast::Item::ExternBlock(it)
if !collect_attrs(it).map(TupleExt::tail).any(|it| it.is_left()) =>
{
if let Some(extern_item_list) = it.extern_item_list() {
pairs.extend(
extern_item_list.extern_items().map(ast::Item::from).map(item_to_entry),
);
}
}
ast::Item::Impl(it) if !collect_attrs(it).map(TupleExt::tail).any(|it| it.is_left()) => {
if let Some(assoc_item_list) = it.assoc_item_list() {
pairs.extend(assoc_item_list.assoc_items().map(ast::Item::from).map(item_to_entry));
}
}
ast::Item::Module(it) if !collect_attrs(it).map(TupleExt::tail).any(|it| it.is_left()) => {
if let Some(item_list) = it.item_list() {
pairs.extend(item_list.items().map(item_to_entry));
}
}
ast::Item::Trait(it) if !collect_attrs(it).map(TupleExt::tail).any(|it| it.is_left()) => {
if let Some(assoc_item_list) = it.assoc_item_list() {
pairs.extend(assoc_item_list.assoc_items().map(ast::Item::from).map(item_to_entry));
}
}
_ => (),
});
Arc::new(RealSpanMap::from_file(
editioned_file_id,
pairs.into_boxed_slice(),
tree.syntax().text_range().end(),
))
}
pub(crate) fn expansion_span_map(
db: &dyn ExpandDatabase,
file_id: MacroFileId,
) -> Arc<ExpansionSpanMap> {
db.parse_macro_expansion(file_id).value.1
}