hir_expand/
span_map.rs

1//! Span maps for real files and macro expansions.
2
3use span::{Span, SyntaxContext};
4use syntax::{AstNode, TextRange, ast};
5use triomphe::Arc;
6
7pub use span::RealSpanMap;
8
9use crate::{HirFileId, MacroCallId, db::ExpandDatabase};
10
11pub type ExpansionSpanMap = span::SpanMap<SyntaxContext>;
12
13/// Spanmap for a macro file or a real file
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub enum SpanMap {
16    /// Spanmap for a macro file
17    ExpansionSpanMap(Arc<ExpansionSpanMap>),
18    /// Spanmap for a real file
19    RealSpanMap(Arc<RealSpanMap>),
20}
21
22#[derive(Copy, Clone)]
23pub enum SpanMapRef<'a> {
24    /// Spanmap for a macro file
25    ExpansionSpanMap(&'a ExpansionSpanMap),
26    /// Spanmap for a real file
27    RealSpanMap(&'a RealSpanMap),
28}
29
30impl syntax_bridge::SpanMapper<Span> for SpanMap {
31    fn span_for(&self, range: TextRange) -> Span {
32        self.span_for_range(range)
33    }
34}
35
36impl syntax_bridge::SpanMapper<Span> for SpanMapRef<'_> {
37    fn span_for(&self, range: TextRange) -> Span {
38        self.span_for_range(range)
39    }
40}
41
42impl SpanMap {
43    pub fn span_for_range(&self, range: TextRange) -> Span {
44        match self {
45            // FIXME: Is it correct for us to only take the span at the start? This feels somewhat
46            // wrong. The context will be right, but the range could be considered wrong. See
47            // https://github.com/rust-lang/rust/issues/23480, we probably want to fetch the span at
48            // the start and end, then merge them like rustc does in `Span::to
49            Self::ExpansionSpanMap(span_map) => span_map.span_at(range.start()),
50            Self::RealSpanMap(span_map) => span_map.span_for_range(range),
51        }
52    }
53
54    pub fn as_ref(&self) -> SpanMapRef<'_> {
55        match self {
56            Self::ExpansionSpanMap(span_map) => SpanMapRef::ExpansionSpanMap(span_map),
57            Self::RealSpanMap(span_map) => SpanMapRef::RealSpanMap(span_map),
58        }
59    }
60
61    #[inline]
62    pub(crate) fn new(db: &dyn ExpandDatabase, file_id: HirFileId) -> SpanMap {
63        match file_id {
64            HirFileId::FileId(file_id) => SpanMap::RealSpanMap(db.real_span_map(file_id)),
65            HirFileId::MacroFile(m) => {
66                SpanMap::ExpansionSpanMap(db.parse_macro_expansion(m).value.1)
67            }
68        }
69    }
70}
71
72impl SpanMapRef<'_> {
73    pub fn span_for_range(self, range: TextRange) -> Span {
74        match self {
75            Self::ExpansionSpanMap(span_map) => span_map.span_at(range.start()),
76            Self::RealSpanMap(span_map) => span_map.span_for_range(range),
77        }
78    }
79}
80
81pub(crate) fn real_span_map(
82    db: &dyn ExpandDatabase,
83    editioned_file_id: base_db::EditionedFileId,
84) -> Arc<RealSpanMap> {
85    use syntax::ast::HasModuleItem;
86    let mut pairs = vec![(syntax::TextSize::new(0), span::ROOT_ERASED_FILE_AST_ID)];
87    let ast_id_map = db.ast_id_map(editioned_file_id.into());
88
89    let tree = db.parse(editioned_file_id).tree();
90    // This is an incrementality layer. Basically we can't use absolute ranges for our spans as that
91    // would mean we'd invalidate everything whenever we type. So instead we make the text ranges
92    // relative to some AstIds reducing the risk of invalidation as typing somewhere no longer
93    // affects all following spans in the file.
94    // There is some stuff to bear in mind here though, for one, the more "anchors" we create, the
95    // easier it gets to invalidate things again as spans are as stable as their anchor's ID.
96    // The other problem is proc-macros. Proc-macros have a `Span::join` api that allows them
97    // to join two spans that come from the same file. rust-analyzer's proc-macro server
98    // can only join two spans if they belong to the same anchor though, as the spans are relative
99    // to that anchor. To do cross anchor joining we'd need to access to the ast id map to resolve
100    // them again, something we might get access to in the future. But even then, proc-macros doing
101    // this kind of joining makes them as stable as the AstIdMap (which is basically changing on
102    // every input of the file)…
103
104    let item_to_entry =
105        |item: ast::Item| (item.syntax().text_range().start(), ast_id_map.ast_id(&item).erase());
106    // Top level items make for great anchors as they are the most stable and a decent boundary
107    pairs.extend(tree.items().map(item_to_entry));
108    // Unfortunately, assoc items are very common in Rust, so descend into those as well and make
109    // them anchors too, but only if they have no attributes attached, as those might be proc-macros
110    // and using different anchors inside of them will prevent spans from being joinable.
111    tree.items().for_each(|item| match &item {
112        ast::Item::ExternBlock(it) if ast::attrs_including_inner(it).next().is_none() => {
113            if let Some(extern_item_list) = it.extern_item_list() {
114                pairs.extend(
115                    extern_item_list.extern_items().map(ast::Item::from).map(item_to_entry),
116                );
117            }
118        }
119        ast::Item::Impl(it) if ast::attrs_including_inner(it).next().is_none() => {
120            if let Some(assoc_item_list) = it.assoc_item_list() {
121                pairs.extend(assoc_item_list.assoc_items().map(ast::Item::from).map(item_to_entry));
122            }
123        }
124        ast::Item::Module(it) if ast::attrs_including_inner(it).next().is_none() => {
125            if let Some(item_list) = it.item_list() {
126                pairs.extend(item_list.items().map(item_to_entry));
127            }
128        }
129        ast::Item::Trait(it) if ast::attrs_including_inner(it).next().is_none() => {
130            if let Some(assoc_item_list) = it.assoc_item_list() {
131                pairs.extend(assoc_item_list.assoc_items().map(ast::Item::from).map(item_to_entry));
132            }
133        }
134        _ => (),
135    });
136
137    Arc::new(RealSpanMap::from_file(
138        editioned_file_id.editioned_file_id(db),
139        pairs.into_boxed_slice(),
140        tree.syntax().text_range().end(),
141    ))
142}
143
144pub(crate) fn expansion_span_map(
145    db: &dyn ExpandDatabase,
146    file_id: MacroCallId,
147) -> Arc<ExpansionSpanMap> {
148    db.parse_macro_expansion(file_id).value.1
149}