Skip to content

Commit 0f1e741

Browse files
committed
Add scope_utils module for efficient scope resolution and filtering
Introduce a new `scope_utils` module to streamline the process of resolving scope strings into file lists and creating SQL-like filters. Refactor existing search functionalities to utilize this common utility, reducing redundancy and improving maintainability. Implement functions `resolve_scope`, `create_scope_filter`, and `validate_scope_files` to handle scope-related operations efficiently. Update `tool_search.rs` and `tool_regex_search.rs` to integrate with the new utility, ensuring consistent scope handling across different tools.
1 parent f4e1b1e commit 0f1e741

File tree

4 files changed

+185
-72
lines changed

4 files changed

+185
-72
lines changed

refact-agent/engine/src/tools/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod tools_description;
22
pub mod tools_execute;
3+
pub mod scope_utils;
34

45
mod tool_ast_definition;
56
mod tool_ast_reference;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use std::sync::Arc;
2+
use tokio::sync::RwLock as ARwLock;
3+
4+
use crate::at_commands::at_file::{file_repair_candidates, return_one_candidate_or_a_good_error};
5+
use crate::files_correction::{correct_to_nearest_dir_path, get_project_dirs};
6+
use crate::global_context::GlobalContext;
7+
8+
/// Resolves a scope string into a list of files to search.
9+
///
10+
/// # Arguments
11+
///
12+
/// * `gcx` - Global context
13+
/// * `scope` - Scope string, can be "workspace", a directory path (ending with / or \), or a file path
14+
///
15+
/// # Returns
16+
///
17+
/// * `Ok(Vec<String>)` - List of file paths to search
18+
/// * `Err(String)` - Error message if scope resolution fails
19+
///
20+
/// # Examples
21+
///
22+
/// ```
23+
/// let files = resolve_scope(gcx.clone(), "workspace").await?;
24+
/// let files = resolve_scope(gcx.clone(), "src/").await?;
25+
/// let files = resolve_scope(gcx.clone(), "src/main.rs").await?;
26+
/// ```
27+
pub async fn resolve_scope(
28+
gcx: Arc<ARwLock<GlobalContext>>,
29+
scope: &str,
30+
) -> Result<Vec<String>, String> {
31+
let scope_string = scope.to_string();
32+
// Case 1: Workspace scope
33+
if scope == "workspace" {
34+
let workspace_files = gcx.read().await.documents_state.workspace_files.lock().unwrap().clone();
35+
return Ok(workspace_files.into_iter()
36+
.map(|f| f.to_string_lossy().to_string())
37+
.collect::<Vec<_>>());
38+
}
39+
40+
// Check if scope is a directory (ends with / or \)
41+
let scope_is_dir = scope.ends_with('/') || scope.ends_with('\\');
42+
43+
// Case 2: Directory scope
44+
if scope_is_dir {
45+
let dir_path = return_one_candidate_or_a_good_error(
46+
gcx.clone(),
47+
&scope_string,
48+
&correct_to_nearest_dir_path(gcx.clone(), &scope_string, false, 10).await,
49+
&get_project_dirs(gcx.clone()).await,
50+
true,
51+
).await?;
52+
53+
let workspace_files = gcx.read().await.documents_state.workspace_files.lock().unwrap().clone();
54+
return Ok(workspace_files.into_iter()
55+
.filter(|f| f.starts_with(&dir_path))
56+
.map(|f| f.to_string_lossy().to_string())
57+
.collect::<Vec<_>>());
58+
}
59+
60+
// Case 3: File scope (with fallback to directory if file not found)
61+
match return_one_candidate_or_a_good_error(
62+
gcx.clone(),
63+
&scope_string,
64+
&file_repair_candidates(gcx.clone(), &scope_string, 10, false).await,
65+
&get_project_dirs(gcx.clone()).await,
66+
false,
67+
).await {
68+
// File found
69+
Ok(file_path) => Ok(vec![file_path]),
70+
71+
// File not found, try as directory
72+
Err(file_err) => {
73+
match return_one_candidate_or_a_good_error(
74+
gcx.clone(),
75+
&scope_string,
76+
&correct_to_nearest_dir_path(gcx.clone(), &scope_string, false, 10).await,
77+
&get_project_dirs(gcx.clone()).await,
78+
true,
79+
).await {
80+
// Directory found
81+
Ok(dir_path) => {
82+
let workspace_files = gcx.read().await.documents_state.workspace_files.lock().unwrap().clone();
83+
Ok(workspace_files.into_iter()
84+
.filter(|f| f.starts_with(&dir_path))
85+
.map(|f| f.to_string_lossy().to_string())
86+
.collect::<Vec<_>>())
87+
},
88+
// Neither file nor directory found
89+
Err(_) => Err(file_err),
90+
}
91+
},
92+
}
93+
}
94+
95+
/// Creates a SQL-like filter string for the given scope.
96+
/// This is specifically for the search tool which uses SQL-like filters.
97+
///
98+
/// # Arguments
99+
///
100+
/// * `gcx` - Global context
101+
/// * `scope` - Scope string
102+
///
103+
/// # Returns
104+
///
105+
/// * `Ok(Option<String>)` - SQL-like filter string, or None for workspace scope
106+
/// * `Err(String)` - Error message if scope resolution fails
107+
pub async fn create_scope_filter(
108+
gcx: Arc<ARwLock<GlobalContext>>,
109+
scope: &str,
110+
) -> Result<Option<String>, String> {
111+
let scope_string = scope.to_string();
112+
if scope == "workspace" {
113+
return Ok(None);
114+
}
115+
116+
let scope_is_dir = scope.ends_with('/') || scope.ends_with('\\');
117+
118+
if scope_is_dir {
119+
let dir_path = return_one_candidate_or_a_good_error(
120+
gcx.clone(),
121+
&scope_string,
122+
&correct_to_nearest_dir_path(gcx.clone(), &scope_string, false, 10).await,
123+
&get_project_dirs(gcx.clone()).await,
124+
true,
125+
).await?;
126+
127+
return Ok(Some(format!("(scope LIKE '{}%')", dir_path)));
128+
}
129+
130+
match return_one_candidate_or_a_good_error(
131+
gcx.clone(),
132+
&scope_string,
133+
&file_repair_candidates(gcx.clone(), &scope_string, 10, false).await,
134+
&get_project_dirs(gcx.clone()).await,
135+
false,
136+
).await {
137+
Ok(file_path) => Ok(Some(format!("(scope = \"{}\")", file_path))),
138+
Err(file_err) => {
139+
match return_one_candidate_or_a_good_error(
140+
gcx.clone(),
141+
&scope_string,
142+
&correct_to_nearest_dir_path(gcx.clone(), &scope_string, false, 10).await,
143+
&get_project_dirs(gcx.clone()).await,
144+
true,
145+
).await {
146+
Ok(dir_path) => Ok(Some(format!("(scope LIKE '{}%')", dir_path))),
147+
Err(_) => Err(file_err),
148+
}
149+
},
150+
}
151+
}
152+
153+
/// Validates that the scope is not empty and returns an appropriate error message if it is.
154+
///
155+
/// # Arguments
156+
///
157+
/// * `files` - List of files resolved from the scope
158+
/// * `scope` - Original scope string for error reporting
159+
///
160+
/// # Returns
161+
///
162+
/// * `Ok(Vec<String>)` - The same list of files if not empty
163+
/// * `Err(String)` - Error message if the list is empty
164+
pub fn validate_scope_files(
165+
files: Vec<String>,
166+
scope: &str,
167+
) -> Result<Vec<String>, String> {
168+
if files.is_empty() {
169+
Err(format!("No files found in scope: {}", scope))
170+
} else {
171+
Ok(files)
172+
}
173+
}

refact-agent/engine/src/tools/tool_regex_search.rs

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ use tokio::sync::RwLock as ARwLock;
1313

1414

1515
use crate::at_commands::at_commands::{vec_context_file_to_context_tools, AtCommandsContext};
16-
use crate::at_commands::at_file::{file_repair_candidates, return_one_candidate_or_a_good_error};
1716
use crate::call_validation::{ChatMessage, ChatContent, ContextEnum, ContextFile};
18-
use crate::files_correction::{correct_to_nearest_dir_path, get_project_dirs, shortify_paths};
17+
use crate::files_correction::shortify_paths;
1918
use crate::files_in_workspace::get_file_text_from_memory_or_disk;
2019
use crate::global_context::GlobalContext;
20+
use crate::tools::scope_utils::{resolve_scope, validate_scope_files};
2121
use crate::tools::tools_description::Tool;
2222

2323
pub struct ToolRegexSearch;
@@ -72,40 +72,10 @@ async fn search_files_with_regex(
7272
) -> Result<Vec<ContextFile>, String> {
7373
let regex = Regex::new(pattern).map_err(|e| format!("Invalid regex pattern: {}", e))?;
7474

75-
let files_to_search = if scope == "workspace" {
76-
let workspace_files = gcx.read().await.documents_state.workspace_files.lock().unwrap().clone();
77-
workspace_files.into_iter().map(|f| f.to_string_lossy().to_string()).collect::<Vec<_>>()
78-
} else {
79-
let scope_is_dir = scope.ends_with('/') || scope.ends_with('\\');
80-
81-
if scope_is_dir {
82-
let dir_path = return_one_candidate_or_a_good_error(
83-
gcx.clone(),
84-
scope,
85-
&correct_to_nearest_dir_path(gcx.clone(), scope, false, 10).await,
86-
&get_project_dirs(gcx.clone()).await,
87-
true,
88-
).await?;
89-
90-
let workspace_files = gcx.read().await.documents_state.workspace_files.lock().unwrap().clone();
91-
workspace_files.into_iter()
92-
.filter(|f| f.starts_with(&dir_path))
93-
.map(|f| f.to_string_lossy().to_string())
94-
.collect::<Vec<_>>()
95-
} else {
96-
vec![return_one_candidate_or_a_good_error(
97-
gcx.clone(),
98-
scope,
99-
&file_repair_candidates(gcx.clone(), scope, 10, false).await,
100-
&get_project_dirs(gcx.clone()).await,
101-
false,
102-
).await?]
103-
}
104-
};
105-
106-
if files_to_search.is_empty() {
107-
return Err(format!("No files found in scope: {}", scope));
108-
}
75+
// Use the common function to resolve the scope
76+
let files_to_search = resolve_scope(gcx.clone(), scope)
77+
.await
78+
.and_then(|files| validate_scope_files(files, scope))?;
10979

11080
// Send initial progress update
11181
if let Some(tx) = &subchat_tx {

refact-agent/engine/src/tools/tool_search.rs

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ use itertools::Itertools;
88
use tokio::sync::Mutex as AMutex;
99

1010
use crate::at_commands::at_commands::{vec_context_file_to_context_tools, AtCommandsContext};
11-
use crate::at_commands::at_file::{file_repair_candidates, return_one_candidate_or_a_good_error};
1211
use crate::at_commands::at_search::execute_at_search;
13-
use crate::files_correction::{correct_to_nearest_dir_path, get_project_dirs};
12+
use crate::tools::scope_utils::create_scope_filter;
1413
use crate::tools::tools_description::Tool;
1514
use crate::call_validation::{ChatMessage, ChatContent, ContextEnum, ContextFile};
1615

@@ -23,42 +22,12 @@ async fn execute_att_search(
2322
scope: &String,
2423
) -> Result<Vec<ContextFile>, String> {
2524
let gcx = ccx.lock().await.global_context.clone();
26-
if scope == "workspace" {
27-
return execute_at_search(ccx.clone(), &query, None).await
28-
}
29-
let scope_is_dir = scope.ends_with('/') || scope.ends_with('\\');
30-
31-
let filter = if scope_is_dir {
32-
return_one_candidate_or_a_good_error(
33-
gcx.clone(),
34-
scope,
35-
&correct_to_nearest_dir_path(gcx.clone(), scope, false, 10).await,
36-
&get_project_dirs(gcx.clone()).await,
37-
true,
38-
).await.map(|dir| format!("(scope LIKE '{}%')", dir))?
39-
} else {
40-
match return_one_candidate_or_a_good_error(
41-
gcx.clone(),
42-
scope,
43-
&file_repair_candidates(gcx.clone(), scope, 10, false).await,
44-
&get_project_dirs(gcx.clone()).await,
45-
false,
46-
).await {
47-
Ok(file) => format!("(scope = \"{}\")", file),
48-
Err(file_err) => {
49-
return_one_candidate_or_a_good_error(
50-
gcx.clone(),
51-
scope,
52-
&correct_to_nearest_dir_path(gcx.clone(), scope, false, 10).await,
53-
&get_project_dirs(gcx.clone()).await,
54-
true,
55-
).await.map(|dir| format!("(scope LIKE '{}%')", dir)).map_err(|_| file_err)?
56-
},
57-
}
58-
};
25+
26+
// Use the common function to create a scope filter
27+
let filter = create_scope_filter(gcx.clone(), scope).await?;
5928

6029
info!("att-search: filter: {:?}", filter);
61-
execute_at_search(ccx.clone(), &query, Some(filter)).await
30+
execute_at_search(ccx.clone(), &query, filter).await
6231
}
6332

6433
#[async_trait]

0 commit comments

Comments
 (0)