From 86716def714752261edebf711fe7a8a2dbb46f32 Mon Sep 17 00:00:00 2001
From: Marek Kaput <marek.kaput@swmansion.com>
Date: Tue, 14 Jan 2025 11:28:08 +0100
Subject: [PATCH] Scaffold search scopes mechanism

commit-id:69f01fb6
---
 src/lang/inspect/defs.rs                      |  7 +++
 src/lang/inspect/{usages.rs => usages/mod.rs} | 33 +++++-------
 src/lang/inspect/usages/search_scope.rs       | 53 +++++++++++++++++++
 3 files changed, 72 insertions(+), 21 deletions(-)
 rename src/lang/inspect/{usages.rs => usages/mod.rs} (83%)
 create mode 100644 src/lang/inspect/usages/search_scope.rs

diff --git a/src/lang/inspect/defs.rs b/src/lang/inspect/defs.rs
index 6b1b5ec7..c72227e5 100644
--- a/src/lang/inspect/defs.rs
+++ b/src/lang/inspect/defs.rs
@@ -32,6 +32,7 @@ use tracing::error;
 
 use crate::lang::db::{AnalysisDatabase, LsSemanticGroup, LsSyntaxGroup};
 use crate::lang::inspect::usages::FindUsages;
+use crate::lang::inspect::usages::search_scope::SearchScope;
 
 /// Keeps information about the symbol that is being searched for/inspected.
 ///
@@ -144,6 +145,12 @@ impl SymbolDef {
         }
     }
 
+    /// Builds a search scope for finding usages of this symbol.
+    pub fn search_scope(&self, db: &AnalysisDatabase) -> SearchScope {
+        // TODO(mkaput): Narrow down the scope as much as possible for particular symbol kinds.
+        SearchScope::everything(db)
+    }
+
     /// Starts a find-usages search for this symbol.
     pub fn usages<'a>(&'a self, db: &'a AnalysisDatabase) -> FindUsages<'a> {
         FindUsages::new(self, db)
diff --git a/src/lang/inspect/usages.rs b/src/lang/inspect/usages/mod.rs
similarity index 83%
rename from src/lang/inspect/usages.rs
rename to src/lang/inspect/usages/mod.rs
index 0ceae66c..20e5c9cf 100644
--- a/src/lang/inspect/usages.rs
+++ b/src/lang/inspect/usages/mod.rs
@@ -1,9 +1,5 @@
-use std::collections::HashMap;
 use std::ops::ControlFlow;
-use std::sync::Arc;
 
-use cairo_lang_defs::db::DefsGroup;
-use cairo_lang_filesystem::db::FilesGroup;
 use cairo_lang_filesystem::ids::FileId;
 use cairo_lang_filesystem::span::{TextOffset, TextSpan, TextWidth};
 use cairo_lang_syntax::node::ast::TerminalIdentifier;
@@ -15,6 +11,8 @@ use smol_str::format_smolstr;
 use crate::lang::db::{AnalysisDatabase, LsSyntaxGroup};
 use crate::lang::inspect::defs::SymbolDef;
 
+pub mod search_scope;
+
 macro_rules! flow {
     ($expr:expr) => {
         let ControlFlow::Continue(()) = $expr else {
@@ -23,7 +21,6 @@ macro_rules! flow {
     };
 }
 
-// TODO(mkaput): Implement search scopes: for example, variables will never be used in other files.
 // TODO(mkaput): Deal with `crate` keyword.
 /// An implementation of the find-usages functionality.
 ///
@@ -61,6 +58,10 @@ impl<'a> FindUsages<'a> {
     pub fn search(self, sink: &mut dyn FnMut(FoundUsage) -> ControlFlow<(), ()>) {
         let db = self.db;
 
+        // TODO(mkaput): When needed, allow setting search scope externally, via a field in
+        //   FindUsages and set_scope/in_scope methods like RA does.
+        let search_scope = self.symbol.search_scope(db);
+
         let needle = match self.symbol {
             // Small optimisation for inline macros: we can be sure that any usages will have a `!`
             // at the end, so we do not need to search for occurrences without it.
@@ -70,9 +71,9 @@ impl<'a> FindUsages<'a> {
 
         let finder = Finder::new(needle.as_bytes());
 
-        for (file, text) in Self::scope_files(db) {
+        for (file, text, search_span) in search_scope.file_contents(db) {
             // Search occurrences of the symbol's name.
-            for offset in Self::match_offsets(&finder, &text) {
+            for offset in Self::match_offsets(&finder, &text, search_span) {
                 if let Some(node) = db.find_syntax_node_at_offset(file, offset) {
                     if let Some(identifier) = TerminalIdentifier::cast_token(db.upcast(), node) {
                         flow!(self.found_identifier(identifier, sink));
@@ -82,27 +83,17 @@ impl<'a> FindUsages<'a> {
         }
     }
 
-    fn scope_files(db: &AnalysisDatabase) -> impl Iterator<Item = (FileId, Arc<str>)> + '_ {
-        let mut files = HashMap::new();
-        for crate_id in db.crates() {
-            for &module_id in db.crate_modules(crate_id).iter() {
-                if let Ok(file_id) = db.module_main_file(module_id) {
-                    if let Some(text) = db.file_content(file_id) {
-                        files.insert(file_id, text);
-                    }
-                }
-            }
-        }
-        files.into_iter()
-    }
-
     fn match_offsets<'b>(
         finder: &'b Finder<'b>,
         text: &'b str,
+        search_span: Option<TextSpan>,
     ) -> impl Iterator<Item = TextOffset> + use<'b> {
         finder
             .find_iter(text.as_bytes())
             .map(|offset| TextWidth::at(text, offset).as_offset())
+            .filter(move |&offset| {
+                search_span.is_none_or(|span| span.start <= offset && offset <= span.end)
+            })
             .filter(|offset| {
                 // Reject matches that are not at word boundaries - for example, an identifier
                 // `core` will never be a direct usage of a needle `or`.
diff --git a/src/lang/inspect/usages/search_scope.rs b/src/lang/inspect/usages/search_scope.rs
new file mode 100644
index 00000000..d22a42db
--- /dev/null
+++ b/src/lang/inspect/usages/search_scope.rs
@@ -0,0 +1,53 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use cairo_lang_defs::db::DefsGroup;
+use cairo_lang_filesystem::db::FilesGroup;
+use cairo_lang_filesystem::ids::FileId;
+use cairo_lang_filesystem::span::TextSpan;
+
+use crate::lang::db::AnalysisDatabase;
+
+#[derive(Clone, Default)]
+pub struct SearchScope {
+    /// A collection of all files constituting this search scope, with optional text spans to
+    /// narrow down searching ranges.
+    entries: HashMap<FileId, Option<TextSpan>>,
+}
+
+impl SearchScope {
+    /// Builds a new empty search scope.
+    pub fn empty() -> Self {
+        Self::default()
+    }
+
+    /// Builds a search scope spanning an entire set of analysed crates.
+    #[tracing::instrument(skip_all)]
+    pub fn everything(db: &AnalysisDatabase) -> Self {
+        let mut this = Self::empty();
+        for crate_id in db.crates() {
+            for &module_id in db.crate_modules(crate_id).iter() {
+                if let Ok(file_id) = db.module_main_file(module_id) {
+                    this.entries.insert(file_id, None);
+                }
+            }
+        }
+        this
+    }
+
+    /// Creates an iterator over all files and the optional search scope text spans.
+    pub fn files_and_spans(&self) -> impl Iterator<Item = (FileId, Option<TextSpan>)> + use<'_> {
+        self.entries.iter().map(|(&file, &span)| (file, span))
+    }
+
+    /// Creates an iterator over all files, their contents and the optional search scope text spans.
+    pub fn files_contents_and_spans<'a, 'b>(
+        &'a self,
+        db: &'b AnalysisDatabase,
+    ) -> impl Iterator<Item = (FileId, Arc<str>, Option<TextSpan>)> + use<'a, 'b> {
+        self.files_and_spans().filter_map(move |(file, span)| {
+            let text = db.file_content(file)?;
+            Some((file, text, span))
+        })
+    }
+}