From 63f500b0ee55443a52f078060004c911a7532a14 Mon Sep 17 00:00:00 2001 From: David Lattimore Date: Wed, 22 Jul 2020 14:01:21 +1000 Subject: SSR: Use Definition::find_usages to speed up matching. When the search pattern contains a path, this substantially speeds up finding matches, especially if the path references a private item. --- crates/ra_ssr/src/search.rs | 131 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 5 deletions(-) (limited to 'crates/ra_ssr/src/search.rs') diff --git a/crates/ra_ssr/src/search.rs b/crates/ra_ssr/src/search.rs index ccc2d544a..9a4e35e96 100644 --- a/crates/ra_ssr/src/search.rs +++ b/crates/ra_ssr/src/search.rs @@ -1,17 +1,106 @@ //! Searching for matches. -use crate::{matching, resolving::ResolvedRule, Match, MatchFinder}; +use crate::{ + matching, + resolving::{ResolvedPath, ResolvedPattern, ResolvedRule}, + Match, MatchFinder, +}; use ra_db::FileRange; +use ra_ide_db::{ + defs::Definition, + search::{Reference, SearchScope}, +}; use ra_syntax::{ast, AstNode, SyntaxNode}; +/// A cache for the results of find_usages. This is for when we have multiple patterns that have the +/// same path. e.g. if the pattern was `foo::Bar` that can parse as a path, an expression, a type +/// and as a pattern. In each, the usages of `foo::Bar` are the same and we'd like to avoid finding +/// them more than once. +#[derive(Default)] +pub(crate) struct UsageCache { + usages: Vec<(Definition, Vec)>, +} + impl<'db> MatchFinder<'db> { /// Adds all matches for `rule` to `matches_out`. Matches may overlap in ways that make /// replacement impossible, so further processing is required in order to properly nest matches /// and remove overlapping matches. This is done in the `nesting` module. - pub(crate) fn find_matches_for_rule(&self, rule: &ResolvedRule, matches_out: &mut Vec) { - // FIXME: Use resolved paths in the pattern to find places to search instead of always - // scanning every node. - self.slow_scan(rule, matches_out); + pub(crate) fn find_matches_for_rule( + &self, + rule: &ResolvedRule, + usage_cache: &mut UsageCache, + matches_out: &mut Vec, + ) { + if pick_path_for_usages(&rule.pattern).is_none() { + self.slow_scan(rule, matches_out); + return; + } + self.find_matches_for_pattern_tree(rule, &rule.pattern, usage_cache, matches_out); + } + + fn find_matches_for_pattern_tree( + &self, + rule: &ResolvedRule, + pattern: &ResolvedPattern, + usage_cache: &mut UsageCache, + matches_out: &mut Vec, + ) { + if let Some(first_path) = pick_path_for_usages(pattern) { + let definition: Definition = first_path.resolution.clone().into(); + for reference in self.find_usages(usage_cache, definition) { + let file = self.sema.parse(reference.file_range.file_id); + if let Some(path) = self.sema.find_node_at_offset_with_descend::( + file.syntax(), + reference.file_range.range.start(), + ) { + if let Some(node_to_match) = self + .sema + .ancestors_with_macros(path.syntax().clone()) + .skip(first_path.depth as usize) + .next() + { + if let Ok(m) = + matching::get_match(false, rule, &node_to_match, &None, &self.sema) + { + matches_out.push(m); + } + } + } + } + } + } + + fn find_usages<'a>( + &self, + usage_cache: &'a mut UsageCache, + definition: Definition, + ) -> &'a [Reference] { + // Logically if a lookup succeeds we should just return it. Unfortunately returning it would + // extend the lifetime of the borrow, then we wouldn't be able to do the insertion on a + // cache miss. This is a limitation of NLL and is fixed with Polonius. For now we do two + // lookups in the case of a cache hit. + if usage_cache.find(&definition).is_none() { + let usages = definition.find_usages(&self.sema, Some(self.search_scope())); + usage_cache.usages.push((definition, usages)); + return &usage_cache.usages.last().unwrap().1; + } + usage_cache.find(&definition).unwrap() + } + + /// Returns the scope within which we want to search. We don't want un unrestricted search + /// scope, since we don't want to find references in external dependencies. + fn search_scope(&self) -> SearchScope { + // FIXME: We should ideally have a test that checks that we edit local roots and not library + // roots. This probably would require some changes to fixtures, since currently everything + // seems to get put into a single source root. + use ra_db::SourceDatabaseExt; + use ra_ide_db::symbol_index::SymbolsDatabase; + let mut files = Vec::new(); + for &root in self.sema.db.local_roots().iter() { + let sr = self.sema.db.source_root(root); + files.extend(sr.iter()); + } + SearchScope::files(&files) } fn slow_scan(&self, rule: &ResolvedRule, matches_out: &mut Vec) { @@ -59,3 +148,35 @@ impl<'db> MatchFinder<'db> { } } } + +impl UsageCache { + fn find(&mut self, definition: &Definition) -> Option<&[Reference]> { + // We expect a very small number of cache entries (generally 1), so a linear scan should be + // fast enough and avoids the need to implement Hash for Definition. + for (d, refs) in &self.usages { + if d == definition { + return Some(refs); + } + } + None + } +} + +/// Returns a path that's suitable for path resolution. We exclude builtin types, since they aren't +/// something that we can find references to. We then somewhat arbitrarily pick the path that is the +/// longest as this is hopefully more likely to be less common, making it faster to find. +fn pick_path_for_usages(pattern: &ResolvedPattern) -> Option<&ResolvedPath> { + // FIXME: Take the scope of the resolved path into account. e.g. if there are any paths that are + // private to the current module, then we definitely would want to pick them over say a path + // from std. Possibly we should go further than this and intersect the search scopes for all + // resolved paths then search only in that scope. + pattern + .resolved_paths + .iter() + .filter(|(_, p)| { + !matches!(p.resolution, hir::PathResolution::Def(hir::ModuleDef::BuiltinType(_))) + }) + .map(|(node, resolved)| (node.text().len(), resolved)) + .max_by(|(a, _), (b, _)| a.cmp(b)) + .map(|(_, resolved)| resolved) +} -- cgit v1.2.3