100 lines
4.1 KiB
Rust
100 lines
4.1 KiB
Rust
//! Converts a flat collection of matches into a nested form suitable for replacement. When there
|
|
//! are multiple matches for a node, or that overlap, priority is given to the earlier rule. Nested
|
|
//! matches are only permitted if the inner match is contained entirely within a placeholder of an
|
|
//! outer match.
|
|
//!
|
|
//! For example, if our search pattern is `foo(foo($a))` and the code had `foo(foo(foo(foo(42))))`,
|
|
//! then we'll get 3 matches, however only the outermost and innermost matches can be accepted. The
|
|
//! middle match would take the second `foo` from the outer match.
|
|
|
|
use ide_db::FxHashMap;
|
|
use syntax::SyntaxNode;
|
|
|
|
use crate::{Match, SsrMatches};
|
|
|
|
pub(crate) fn nest_and_remove_collisions(
|
|
mut matches: Vec<Match>,
|
|
sema: &hir::Semantics<'_, ide_db::RootDatabase>,
|
|
) -> SsrMatches {
|
|
// We sort the matches by depth then by rule index. Sorting by depth means that by the time we
|
|
// see a match, any parent matches or conflicting matches will have already been seen. Sorting
|
|
// by rule_index means that if there are two matches for the same node, the rule added first
|
|
// will take precedence.
|
|
matches.sort_by(|a, b| a.depth.cmp(&b.depth).then_with(|| a.rule_index.cmp(&b.rule_index)));
|
|
let mut collector = MatchCollector::default();
|
|
for m in matches {
|
|
collector.add_match(m, sema);
|
|
}
|
|
collector.into()
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct MatchCollector {
|
|
matches_by_node: FxHashMap<SyntaxNode, Match>,
|
|
}
|
|
|
|
impl MatchCollector {
|
|
/// Attempts to add `m` to matches. If it conflicts with an existing match, it is discarded. If
|
|
/// it is entirely within the a placeholder of an existing match, then it is added as a child
|
|
/// match of the existing match.
|
|
fn add_match(&mut self, m: Match, sema: &hir::Semantics<'_, ide_db::RootDatabase>) {
|
|
let matched_node = m.matched_node.clone();
|
|
if let Some(existing) = self.matches_by_node.get_mut(&matched_node) {
|
|
try_add_sub_match(m, existing, sema);
|
|
return;
|
|
}
|
|
for ancestor in sema.ancestors_with_macros(m.matched_node.clone()) {
|
|
if let Some(existing) = self.matches_by_node.get_mut(&ancestor) {
|
|
try_add_sub_match(m, existing, sema);
|
|
return;
|
|
}
|
|
}
|
|
self.matches_by_node.insert(matched_node, m);
|
|
}
|
|
}
|
|
|
|
/// Attempts to add `m` as a sub-match of `existing`.
|
|
fn try_add_sub_match(
|
|
m: Match,
|
|
existing: &mut Match,
|
|
sema: &hir::Semantics<'_, ide_db::RootDatabase>,
|
|
) {
|
|
for p in existing.placeholder_values.values_mut() {
|
|
// Note, no need to check if p.range.file is equal to m.range.file, since we
|
|
// already know we're within `existing`.
|
|
if p.range.range.contains_range(m.range.range) {
|
|
// Convert the inner matches in `p` into a temporary MatchCollector. When
|
|
// we're done, we then convert it back into an SsrMatches. If we expected
|
|
// lots of inner matches, it might be worthwhile keeping a MatchCollector
|
|
// around for each placeholder match. However we expect most placeholder
|
|
// will have 0 and a few will have 1. More than that should hopefully be
|
|
// exceptional.
|
|
let mut collector = MatchCollector::default();
|
|
for m in std::mem::take(&mut p.inner_matches.matches) {
|
|
collector.matches_by_node.insert(m.matched_node.clone(), m);
|
|
}
|
|
collector.add_match(m, sema);
|
|
p.inner_matches = collector.into();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<MatchCollector> for SsrMatches {
|
|
fn from(mut match_collector: MatchCollector) -> Self {
|
|
let mut matches = SsrMatches::default();
|
|
for (_, m) in match_collector.matches_by_node.drain() {
|
|
matches.matches.push(m);
|
|
}
|
|
matches.matches.sort_by(|a, b| {
|
|
// Order matches by file_id then by start range. This should be sufficient since ranges
|
|
// shouldn't be overlapping.
|
|
a.range
|
|
.file_id
|
|
.cmp(&b.range.file_id)
|
|
.then_with(|| a.range.range.start().cmp(&b.range.range.start()))
|
|
});
|
|
matches
|
|
}
|
|
}
|