use std::iter; use base_db::{ span::{HirFileId, HirFileIdRepr, MacroFileId, SyntaxContextId}, FileId, FileRange, }; use either::Either; use syntax::{AstNode, SyntaxNode, SyntaxToken, TextRange}; use crate::{db, ExpansionInfo, HirFileIdExt as _}; /// `InFile` stores a value of `T` inside a particular file/syntax tree. /// /// Typical usages are: /// /// * `InFile` -- syntax node in a file /// * `InFile` -- ast node in a file /// * `InFile` -- offset in a file #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub struct InFileWrapper { pub file_id: FileKind, pub value: T, } pub type InFile = InFileWrapper; pub type InMacroFile = InFileWrapper; pub type InRealFile = InFileWrapper; impl InFileWrapper { pub fn new(file_id: FileKind, value: T) -> Self { Self { file_id, value } } pub fn map U, U>(self, f: F) -> InFileWrapper { InFileWrapper::new(self.file_id, f(self.value)) } } impl InFileWrapper { pub fn with_value(&self, value: U) -> InFileWrapper { InFileWrapper::new(self.file_id, value) } pub fn as_ref(&self) -> InFileWrapper { self.with_value(&self.value) } } impl InFileWrapper { pub fn cloned(&self) -> InFileWrapper { self.with_value(self.value.clone()) } } impl From> for InFile { fn from(InMacroFile { file_id, value }: InMacroFile) -> Self { InFile { file_id: file_id.into(), value } } } impl From> for InFile { fn from(InRealFile { file_id, value }: InRealFile) -> Self { InFile { file_id: file_id.into(), value } } } // region:transpose impls impl InFileWrapper> { pub fn transpose(self) -> Option> { Some(InFileWrapper::new(self.file_id, self.value?)) } } impl InFileWrapper> { pub fn transpose(self) -> Either, InFileWrapper> { match self.value { Either::Left(l) => Either::Left(InFileWrapper::new(self.file_id, l)), Either::Right(r) => Either::Right(InFileWrapper::new(self.file_id, r)), } } } // endregion:transpose impls // region:specific impls impl InFile { pub fn file_syntax(&self, db: &dyn db::ExpandDatabase) -> SyntaxNode { db.parse_or_expand(self.file_id) } } impl InRealFile { pub fn file_syntax(&self, db: &dyn db::ExpandDatabase) -> SyntaxNode { db.parse(self.file_id).syntax_node() } } impl InMacroFile { pub fn file_syntax(&self, db: &dyn db::ExpandDatabase) -> SyntaxNode { db.parse_macro_expansion(self.file_id).value.0.syntax_node() } } impl InFile<&SyntaxNode> { pub fn ancestors_with_macros( self, db: &dyn db::ExpandDatabase, ) -> impl Iterator> + Clone + '_ { iter::successors(Some(self.cloned()), move |node| match node.value.parent() { Some(parent) => Some(node.with_value(parent)), None => node.file_id.call_node(db), }) } /// Skips the attributed item that caused the macro invocation we are climbing up pub fn ancestors_with_macros_skip_attr_item( self, db: &dyn db::ExpandDatabase, ) -> impl Iterator> + '_ { let succ = move |node: &InFile| match node.value.parent() { Some(parent) => Some(node.with_value(parent)), None => { let parent_node = node.file_id.call_node(db)?; if node.file_id.is_attr_macro(db) { // macro call was an attributed item, skip it // FIXME: does this fail if this is a direct expansion of another macro? parent_node.map(|node| node.parent()).transpose() } else { Some(parent_node) } } }; iter::successors(succ(&self.cloned()), succ) } /// Falls back to the macro call range if the node cannot be mapped up fully. /// /// For attributes and derives, this will point back to the attribute only. /// For the entire item use [`InFile::original_file_range_full`]. pub fn original_file_range(self, db: &dyn db::ExpandDatabase) -> FileRange { match self.file_id.repr() { HirFileIdRepr::FileId(file_id) => FileRange { file_id, range: self.value.text_range() }, HirFileIdRepr::MacroFile(mac_file) => { if let Some((res, ctxt)) = ExpansionInfo::new(db, mac_file).map_node_range_up(db, self.value.text_range()) { // FIXME: Figure out an API that makes proper use of ctx, this only exists to // keep pre-token map rewrite behaviour. if ctxt.is_root() { return res; } } // Fall back to whole macro call. let loc = db.lookup_intern_macro_call(mac_file.macro_call_id); loc.kind.original_call_range(db) } } } /// Falls back to the macro call range if the node cannot be mapped up fully. pub fn original_file_range_full(self, db: &dyn db::ExpandDatabase) -> FileRange { match self.file_id.repr() { HirFileIdRepr::FileId(file_id) => FileRange { file_id, range: self.value.text_range() }, HirFileIdRepr::MacroFile(mac_file) => { if let Some((res, ctxt)) = ExpansionInfo::new(db, mac_file).map_node_range_up(db, self.value.text_range()) { // FIXME: Figure out an API that makes proper use of ctx, this only exists to // keep pre-token map rewrite behaviour. if ctxt.is_root() { return res; } } // Fall back to whole macro call. let loc = db.lookup_intern_macro_call(mac_file.macro_call_id); loc.kind.original_call_range_with_body(db) } } } /// Attempts to map the syntax node back up its macro calls. pub fn original_file_range_opt( self, db: &dyn db::ExpandDatabase, ) -> Option<(FileRange, SyntaxContextId)> { match self.file_id.repr() { HirFileIdRepr::FileId(file_id) => { Some((FileRange { file_id, range: self.value.text_range() }, SyntaxContextId::ROOT)) } HirFileIdRepr::MacroFile(mac_file) => { ExpansionInfo::new(db, mac_file).map_node_range_up(db, self.value.text_range()) } } } pub fn original_syntax_node( self, db: &dyn db::ExpandDatabase, ) -> Option> { // This kind of upmapping can only be achieved in attribute expanded files, // as we don't have node inputs otherwise and therefore can't find an `N` node in the input let file_id = match self.file_id.repr() { HirFileIdRepr::FileId(file_id) => { return Some(InRealFile { file_id, value: self.value.clone() }) } HirFileIdRepr::MacroFile(m) => m, }; if !self.file_id.is_attr_macro(db) { return None; } let (FileRange { file_id, range }, ctx) = ExpansionInfo::new(db, file_id).map_node_range_up(db, self.value.text_range())?; // FIXME: Figure out an API that makes proper use of ctx, this only exists to // keep pre-token map rewrite behaviour. if !ctx.is_root() { return None; } let anc = db.parse(file_id).syntax_node().covering_element(range); let kind = self.value.kind(); // FIXME: This heuristic is brittle and with the right macro may select completely unrelated nodes? let value = anc.ancestors().find(|it| it.kind() == kind)?; Some(InRealFile::new(file_id, value)) } } impl InFile { pub fn upmap_once( self, db: &dyn db::ExpandDatabase, ) -> Option>> { Some(self.file_id.expansion_info(db)?.map_range_up_once(db, self.value.text_range())) } /// Falls back to the macro call range if the node cannot be mapped up fully. pub fn original_file_range(self, db: &dyn db::ExpandDatabase) -> FileRange { match self.file_id.repr() { HirFileIdRepr::FileId(file_id) => FileRange { file_id, range: self.value.text_range() }, HirFileIdRepr::MacroFile(mac_file) => { if let Some(res) = self.original_file_range_opt(db) { return res; } // Fall back to whole macro call. let loc = db.lookup_intern_macro_call(mac_file.macro_call_id); loc.kind.original_call_range(db) } } } /// Attempts to map the syntax node back up its macro calls. pub fn original_file_range_opt(self, db: &dyn db::ExpandDatabase) -> Option { match self.file_id.repr() { HirFileIdRepr::FileId(file_id) => { Some(FileRange { file_id, range: self.value.text_range() }) } HirFileIdRepr::MacroFile(_) => { let (range, ctxt) = ascend_range_up_macros(db, self.map(|it| it.text_range())); // FIXME: Figure out an API that makes proper use of ctx, this only exists to // keep pre-token map rewrite behaviour. if ctxt.is_root() { Some(range) } else { None } } } } } impl InFile { /// Attempts to map the syntax node back up its macro calls. pub fn original_file_range(self, db: &dyn db::ExpandDatabase) -> FileRange { let (range, _ctxt) = ascend_range_up_macros(db, self); range } } impl InFile { pub fn descendants(self) -> impl Iterator> { self.value.syntax().descendants().filter_map(T::cast).map(move |n| self.with_value(n)) } pub fn original_ast_node(self, db: &dyn db::ExpandDatabase) -> Option> { // This kind of upmapping can only be achieved in attribute expanded files, // as we don't have node inputs otherwise and therefore can't find an `N` node in the input let file_id = match self.file_id.repr() { HirFileIdRepr::FileId(file_id) => { return Some(InRealFile { file_id, value: self.value }) } HirFileIdRepr::MacroFile(m) => m, }; if !self.file_id.is_attr_macro(db) { return None; } let (FileRange { file_id, range }, ctx) = ExpansionInfo::new(db, file_id) .map_node_range_up(db, self.value.syntax().text_range())?; // FIXME: Figure out an API that makes proper use of ctx, this only exists to // keep pre-token map rewrite behaviour. if !ctx.is_root() { return None; } // FIXME: This heuristic is brittle and with the right macro may select completely unrelated nodes? let anc = db.parse(file_id).syntax_node().covering_element(range); let value = anc.ancestors().find_map(N::cast)?; Some(InRealFile::new(file_id, value)) } pub fn syntax(&self) -> InFile<&SyntaxNode> { self.with_value(self.value.syntax()) } } fn ascend_range_up_macros( db: &dyn db::ExpandDatabase, range: InFile, ) -> (FileRange, SyntaxContextId) { match range.file_id.repr() { HirFileIdRepr::FileId(file_id) => { (FileRange { file_id, range: range.value }, SyntaxContextId::ROOT) } HirFileIdRepr::MacroFile(m) => { ExpansionInfo::new(db, m).map_token_range_up(db, range.value) } } }