Improve local tool execution and shell output panes
This commit is contained in:
@@ -24,10 +24,11 @@ pub(super) mod use_computer;
|
||||
pub(super) mod wait_for_events;
|
||||
|
||||
use std::any::Any;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use ai::agent::action_result::{InsertReviewCommentsResult, RequestCommandOutputResult};
|
||||
pub use ask_user_question::AskUserQuestionExecutor;
|
||||
@@ -52,10 +53,11 @@ use galaxy_util::file_type::is_buffer_binary;
|
||||
use galaxyui::r#async::{Spawnable, SpawnableOutput};
|
||||
use galaxyui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity};
|
||||
use grep::GrepExecutor;
|
||||
use instant::Instant;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use mime_guess::from_path;
|
||||
use notebooks::NotebookExecutor;
|
||||
use parking_lot::FairMutex;
|
||||
use parking_lot::{FairMutex, Mutex};
|
||||
use read_documents::ReadDocumentsExecutor;
|
||||
pub(super) use read_files::ReadFilesExecutor;
|
||||
use read_mcp_resource::ReadMCPResourceExecutor;
|
||||
@@ -96,7 +98,7 @@ use crate::ai::get_relevant_files::controller::GetRelevantFilesController;
|
||||
use crate::ai::{agent::AnyFileContent, paths::host_native_absolute_path};
|
||||
use crate::terminal::model::session::active_session::ActiveSession;
|
||||
use crate::terminal::model::session::command_executor::shell_quote_arg;
|
||||
use crate::terminal::model::session::{ExecuteCommandOptions, Session};
|
||||
use crate::terminal::model::session::{ExecuteCommandOptions, Session, SessionId};
|
||||
use crate::terminal::model_events::ModelEventDispatcher;
|
||||
use crate::terminal::shell::ShellType;
|
||||
use crate::terminal::{ShellLaunchData, TerminalModel};
|
||||
@@ -146,6 +148,34 @@ pub(super) enum RunningActionPhase {
|
||||
Parallel(ParallelExecutionPolicy),
|
||||
}
|
||||
|
||||
const GIT_REPOSITORY_CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
type GitRepositoryCacheEntries = HashMap<(SessionId, String), (Instant, bool)>;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct GitRepositoryCache {
|
||||
entries: Arc<Mutex<GitRepositoryCacheEntries>>,
|
||||
}
|
||||
|
||||
impl GitRepositoryCache {
|
||||
fn get(&self, session_id: SessionId, path: &str) -> Option<bool> {
|
||||
let key = (session_id, path.to_string());
|
||||
let mut entries = self.entries.lock();
|
||||
let (cached_at, is_repository) = entries.get(&key).copied()?;
|
||||
if cached_at.elapsed() < GIT_REPOSITORY_CACHE_TTL {
|
||||
Some(is_repository)
|
||||
} else {
|
||||
entries.remove(&key);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn insert(&self, session_id: SessionId, path: String, is_repository: bool) {
|
||||
let mut entries = self.entries.lock();
|
||||
entries.retain(|_, (cached_at, _)| cached_at.elapsed() < GIT_REPOSITORY_CACHE_TTL);
|
||||
entries.insert((session_id, path), (Instant::now(), is_repository));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ExecuteActionInput<'a> {
|
||||
action: &'a AIAgentAction,
|
||||
@@ -355,10 +385,21 @@ impl BlocklistAIActionExecutor {
|
||||
let request_file_edits_executor = ctx.add_model(|ctx| {
|
||||
RequestFileEditsExecutor::new(active_session.clone(), terminal_view_id, ctx)
|
||||
});
|
||||
let grep_executor =
|
||||
ctx.add_model(|_| GrepExecutor::new(active_session.clone(), terminal_view_id));
|
||||
let file_glob_executor =
|
||||
ctx.add_model(|_| FileGlobExecutor::new(active_session.clone(), terminal_view_id));
|
||||
let git_repository_cache = GitRepositoryCache::default();
|
||||
let grep_executor = ctx.add_model(|_| {
|
||||
GrepExecutor::new(
|
||||
active_session.clone(),
|
||||
terminal_view_id,
|
||||
git_repository_cache.clone(),
|
||||
)
|
||||
});
|
||||
let file_glob_executor = ctx.add_model(|_| {
|
||||
FileGlobExecutor::new(
|
||||
active_session.clone(),
|
||||
terminal_view_id,
|
||||
git_repository_cache,
|
||||
)
|
||||
});
|
||||
let read_mcp_resource_executor = ctx
|
||||
.add_model(|_| ReadMCPResourceExecutor::new(active_session.clone(), terminal_view_id));
|
||||
let call_mcp_tool_executor =
|
||||
@@ -1550,8 +1591,15 @@ fn build_is_git_repository_command(absolute_path: &str, shell_type: ShellType) -
|
||||
}
|
||||
|
||||
/// Returns true if the given path is a regular file on the session's filesystem.
|
||||
/// Runs a shell command on the session so it works for both local and remote sessions.
|
||||
/// Uses native metadata for compatible local sessions and a shell probe otherwise.
|
||||
async fn is_file_path(path: &str, session: &Session) -> bool {
|
||||
#[cfg(feature = "local_fs")]
|
||||
if session.is_local() && !session.is_wsl() && !session.is_msys2() {
|
||||
return async_fs::metadata(path)
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_file());
|
||||
}
|
||||
|
||||
let command = build_is_file_path_command(path, session.shell().shell_type());
|
||||
session
|
||||
.execute_command(&command, None, None, ExecuteCommandOptions::default())
|
||||
@@ -1574,6 +1622,20 @@ async fn is_git_repository(absolute_path: &str, session: &Session) -> anyhow::Re
|
||||
Ok(command_output.success())
|
||||
}
|
||||
|
||||
async fn is_git_repository_cached(
|
||||
absolute_path: &str,
|
||||
session: &Session,
|
||||
cache: &GitRepositoryCache,
|
||||
) -> anyhow::Result<bool> {
|
||||
if let Some(is_repository) = cache.get(session.id(), absolute_path) {
|
||||
return Ok(is_repository);
|
||||
}
|
||||
|
||||
let is_repository = is_git_repository(absolute_path, session).await?;
|
||||
cache.insert(session.id(), absolute_path.to_string(), is_repository);
|
||||
Ok(is_repository)
|
||||
}
|
||||
|
||||
fn get_server_output_id(
|
||||
conversation_id: AIConversationId,
|
||||
ctx: &mut AppContext,
|
||||
|
||||
@@ -26,13 +26,14 @@ use crate::{send_telemetry_from_app_ctx, TelemetryEvent};
|
||||
const FILE_GLOB_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
use super::{
|
||||
get_server_output_id, is_git_repository, ActionExecution, AnyActionExecution,
|
||||
ExecuteActionInput, PreprocessActionInput,
|
||||
get_server_output_id, is_git_repository_cached, ActionExecution, AnyActionExecution,
|
||||
ExecuteActionInput, GitRepositoryCache, PreprocessActionInput,
|
||||
};
|
||||
|
||||
pub struct FileGlobExecutor {
|
||||
active_session: ModelHandle<ActiveSession>,
|
||||
terminal_view_id: EntityId,
|
||||
git_repository_cache: GitRepositoryCache,
|
||||
}
|
||||
|
||||
fn log_file_glob_error(conversation_id: AIConversationId, ctx: &mut AppContext) {
|
||||
@@ -41,10 +42,15 @@ fn log_file_glob_error(conversation_id: AIConversationId, ctx: &mut AppContext)
|
||||
}
|
||||
|
||||
impl FileGlobExecutor {
|
||||
pub fn new(active_session: ModelHandle<ActiveSession>, terminal_view_id: EntityId) -> Self {
|
||||
pub(super) fn new(
|
||||
active_session: ModelHandle<ActiveSession>,
|
||||
terminal_view_id: EntityId,
|
||||
git_repository_cache: GitRepositoryCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
active_session,
|
||||
terminal_view_id,
|
||||
git_repository_cache,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,11 +140,18 @@ impl FileGlobExecutor {
|
||||
let patterns_clone = patterns.clone();
|
||||
let conversation_id_clone = input.conversation_id;
|
||||
let is_file_glob_v2 = is_file_glob_v2(&input);
|
||||
let git_repository_cache = self.git_repository_cache.clone();
|
||||
ActionExecution::new_async(
|
||||
async move {
|
||||
match run_file_glob(patterns_clone, absolute_path, session, shell_launch_data)
|
||||
.with_timeout(FILE_GLOB_TIMEOUT)
|
||||
.await
|
||||
match run_file_glob(
|
||||
patterns_clone,
|
||||
absolute_path,
|
||||
session,
|
||||
shell_launch_data,
|
||||
git_repository_cache,
|
||||
)
|
||||
.with_timeout(FILE_GLOB_TIMEOUT)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(anyhow::anyhow!("File glob operation timed out")),
|
||||
@@ -204,6 +217,7 @@ async fn run_file_glob(
|
||||
absolute_path: String,
|
||||
session: Option<Arc<Session>>,
|
||||
shell_launch_data: Option<ShellLaunchData>,
|
||||
git_repository_cache: GitRepositoryCache,
|
||||
) -> anyhow::Result<FileGlobV2Result> {
|
||||
if patterns.is_empty() {
|
||||
return Err(anyhow::anyhow!("No patterns provided to file_glob"));
|
||||
@@ -213,12 +227,13 @@ async fn run_file_glob(
|
||||
};
|
||||
let shell_type = session.shell().shell_type();
|
||||
|
||||
let is_in_git_repo = is_git_repository(&absolute_path, session.as_ref())
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to run command to check if in git repository: {e:?}");
|
||||
false
|
||||
});
|
||||
let is_in_git_repo =
|
||||
is_git_repository_cached(&absolute_path, session.as_ref(), &git_repository_cache)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to run command to check if in git repository: {e:?}");
|
||||
false
|
||||
});
|
||||
|
||||
if is_in_git_repo {
|
||||
run_git_ls_files_command(
|
||||
|
||||
@@ -5,14 +5,14 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use galaxy_util::standardized_path::StandardizedPath;
|
||||
use galaxyui::r#async::FutureExt as AsyncFutureExt;
|
||||
use galaxyui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity};
|
||||
|
||||
use super::{
|
||||
get_server_output_id, is_file_path, is_git_repository, ActionExecution, AnyActionExecution,
|
||||
ExecuteActionInput, PreprocessActionInput,
|
||||
get_server_output_id, is_file_path, is_git_repository_cached, ActionExecution,
|
||||
AnyActionExecution, ExecuteActionInput, GitRepositoryCache, PreprocessActionInput,
|
||||
};
|
||||
use crate::ai::agent::conversation::AIConversationId;
|
||||
use crate::ai::agent::redaction::redact_secrets;
|
||||
@@ -30,6 +30,8 @@ use crate::terminal::ShellLaunchData;
|
||||
use crate::{send_telemetry_from_app_ctx, PrivacySettings, TelemetryEvent};
|
||||
|
||||
const GREP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const GREP_MAX_MATCHES: usize = 1_000;
|
||||
const GREP_MAX_MATCHES_PER_FILE: usize = 50;
|
||||
const NON_ZERO_EXIT_CODE_ERROR: &str = "Grep command exited with non-zero exit code";
|
||||
|
||||
/// Information about the Grep call that resulted in an error, used to send
|
||||
@@ -179,13 +181,19 @@ fn log_grep_error(
|
||||
pub struct GrepExecutor {
|
||||
active_session: ModelHandle<ActiveSession>,
|
||||
terminal_view_id: EntityId,
|
||||
git_repository_cache: GitRepositoryCache,
|
||||
}
|
||||
|
||||
impl GrepExecutor {
|
||||
pub fn new(active_session: ModelHandle<ActiveSession>, terminal_view_id: EntityId) -> Self {
|
||||
pub(super) fn new(
|
||||
active_session: ModelHandle<ActiveSession>,
|
||||
terminal_view_id: EntityId,
|
||||
git_repository_cache: GitRepositoryCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
active_session,
|
||||
terminal_view_id,
|
||||
git_repository_cache,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,11 +272,18 @@ impl GrepExecutor {
|
||||
let absolute_path_clone = absolute_path.clone();
|
||||
let working_directory_clone = current_working_directory.clone();
|
||||
let conversation_id_clone = input.conversation_id;
|
||||
let git_repository_cache = self.git_repository_cache.clone();
|
||||
ActionExecution::new_async(
|
||||
async move {
|
||||
match run_grep(queries_clone, absolute_path, session, shell_launch_data)
|
||||
.with_timeout(GREP_TIMEOUT)
|
||||
.await
|
||||
match run_grep(
|
||||
queries_clone,
|
||||
absolute_path,
|
||||
session,
|
||||
shell_launch_data,
|
||||
git_repository_cache,
|
||||
)
|
||||
.with_timeout(GREP_TIMEOUT)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(_) => Err(GrepError::new("Grep operation timed out".to_string())),
|
||||
@@ -345,6 +360,7 @@ async fn run_grep(
|
||||
absolute_path: String,
|
||||
session: Option<Arc<Session>>,
|
||||
shell_launch_data: Option<ShellLaunchData>,
|
||||
git_repository_cache: GitRepositoryCache,
|
||||
) -> Result<GrepResult, GrepError> {
|
||||
if queries.is_empty() {
|
||||
return Err(GrepError::new("No queries provided to grep".to_string()));
|
||||
@@ -373,13 +389,13 @@ async fn run_grep(
|
||||
Cow::Borrowed(absolute_path.as_str())
|
||||
};
|
||||
|
||||
// TODO(CODE-239): Cache the result of this check.
|
||||
let is_grep_in_git_repo = is_git_repository(&execute_directory, &session)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to run command to check if in git repository: {e:?}");
|
||||
false
|
||||
});
|
||||
let is_grep_in_git_repo =
|
||||
is_git_repository_cached(&execute_directory, &session, &git_repository_cache)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("Failed to run command to check if in git repository: {e:?}");
|
||||
false
|
||||
});
|
||||
let shell_type = session.shell().shell_type();
|
||||
|
||||
// The most optimized tool to perform the search is `git grep`;
|
||||
@@ -428,10 +444,13 @@ async fn run_grep(
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
async fn run_ripgrep(queries: &[String], absolute_path: String) -> Result<GrepResult, GrepError> {
|
||||
let path = PathBuf::from(absolute_path);
|
||||
let result = galaxy_ripgrep::search::search(queries, &[path], false, false).await;
|
||||
let result = galaxy_ripgrep::search::search_streaming(queries, &[path], false, false);
|
||||
|
||||
match result {
|
||||
Ok(matches) => {
|
||||
let mut matches = matches.take(GREP_MAX_MATCHES + 1).collect::<Vec<_>>().await;
|
||||
let truncated = matches.len() > GREP_MAX_MATCHES;
|
||||
matches.truncate(GREP_MAX_MATCHES);
|
||||
let mut files_map: HashMap<PathBuf, Vec<GrepLineMatch>> = HashMap::new();
|
||||
for m in matches {
|
||||
files_map
|
||||
@@ -448,7 +467,10 @@ async fn run_ripgrep(queries: &[String], absolute_path: String) -> Result<GrepRe
|
||||
matched_lines,
|
||||
})
|
||||
.collect();
|
||||
Ok(GrepResult::Success { matched_files })
|
||||
Ok(GrepResult::Success {
|
||||
matched_files,
|
||||
truncated,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(GrepError::new(format!("Ripgrep search failed: {e}"))),
|
||||
}
|
||||
@@ -482,7 +504,10 @@ async fn run_git_grep_command(
|
||||
shell_launch_data,
|
||||
Some(execute_directory.to_string()),
|
||||
)
|
||||
.map(|matched_files| GrepResult::Success { matched_files })
|
||||
.map(|(matched_files, truncated)| GrepResult::Success {
|
||||
matched_files,
|
||||
truncated,
|
||||
})
|
||||
.map_err(|e| {
|
||||
GrepError::new(e.to_string())
|
||||
.with_command(grep_command)
|
||||
@@ -496,6 +521,7 @@ async fn run_git_grep_command(
|
||||
// matches.
|
||||
Ok(GrepResult::Success {
|
||||
matched_files: vec![],
|
||||
truncated: false,
|
||||
})
|
||||
} else {
|
||||
Err(GrepError::new_for_non_zero_exit_code()
|
||||
@@ -531,7 +557,10 @@ async fn run_grep_command(
|
||||
shell_launch_data,
|
||||
Some(execute_directory.to_string()),
|
||||
)
|
||||
.map(|matched_files| GrepResult::Success { matched_files })
|
||||
.map(|(matched_files, truncated)| GrepResult::Success {
|
||||
matched_files,
|
||||
truncated,
|
||||
})
|
||||
.map_err(|e| {
|
||||
GrepError::new(e.to_string())
|
||||
.with_command(grep_command)
|
||||
@@ -545,6 +574,7 @@ async fn run_grep_command(
|
||||
// matches.
|
||||
Ok(GrepResult::Success {
|
||||
matched_files: vec![],
|
||||
truncated: false,
|
||||
})
|
||||
} else {
|
||||
Err(GrepError::new_for_non_zero_exit_code()
|
||||
@@ -580,7 +610,10 @@ async fn run_select_string_command(
|
||||
shell_launch_data,
|
||||
Some(execute_directory.to_string()),
|
||||
)
|
||||
.map(|matched_files| GrepResult::Success { matched_files })
|
||||
.map(|(matched_files, truncated)| GrepResult::Success {
|
||||
matched_files,
|
||||
truncated,
|
||||
})
|
||||
.map_err(|e| {
|
||||
GrepError::new(e.to_string())
|
||||
.with_command(select_string_command)
|
||||
@@ -595,7 +628,9 @@ async fn run_select_string_command(
|
||||
|
||||
fn build_git_grep_command(queries: &[String], target_path: &str, shell_type: ShellType) -> String {
|
||||
// This command works on all the shells we support (even PowerShell).
|
||||
let mut grep_command = "git --no-pager grep --color=never --untracked -nIE".to_string();
|
||||
let mut grep_command = format!(
|
||||
"git --no-pager grep --color=never --untracked -nIE -m {GREP_MAX_MATCHES_PER_FILE}"
|
||||
);
|
||||
for query in queries {
|
||||
// Queries can originate from model output and project instructions. Keep
|
||||
// them as grep arguments so shell substitutions like $() are inert.
|
||||
@@ -613,7 +648,8 @@ fn build_grep_command(queries: &[String], target_path: &str, shell_type: ShellTy
|
||||
// * "-I" ignores binary files
|
||||
// * "-H" prints file name headers
|
||||
// * "-E" uses extended regex expressions
|
||||
let mut grep_command = "grep --color=never -nrIHE --devices=skip".to_string();
|
||||
let mut grep_command =
|
||||
format!("grep --color=never -nrIHE --devices=skip -m {GREP_MAX_MATCHES_PER_FILE}");
|
||||
for query in queries {
|
||||
// Queries can originate from model output and project instructions. Keep
|
||||
// them as grep arguments so shell substitutions like $() are inert.
|
||||
@@ -627,7 +663,7 @@ fn build_select_string_command(queries: &[String], target_path: &str) -> String
|
||||
// We enable the `-CaseSensitive` flag to match the default behavior of grep.
|
||||
// TODO(CODE-239): Make this command more efficient when searching a file.
|
||||
format!(
|
||||
"Get-ChildItem -Path {} -Recurse -File | Select-String -NoEmphasis -CaseSensitive -Pattern {}",
|
||||
"Get-ChildItem -Path {} -Recurse -File | Select-String -NoEmphasis -CaseSensitive -Pattern {} | Select-Object -First {}",
|
||||
shell_quote_arg(target_path, ShellType::PowerShell),
|
||||
queries
|
||||
.iter()
|
||||
@@ -635,7 +671,8 @@ fn build_select_string_command(queries: &[String], target_path: &str) -> String
|
||||
// strings, so patterns must be single-quoted data arguments.
|
||||
.map(|q| shell_quote_arg(q, ShellType::PowerShell))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
.join(","),
|
||||
GREP_MAX_MATCHES + 1,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -648,10 +685,11 @@ fn parse_grep_output(
|
||||
output: &str,
|
||||
shell_launch_data: Option<ShellLaunchData>,
|
||||
current_working_directory: Option<String>,
|
||||
) -> anyhow::Result<Vec<GrepFileMatch>> {
|
||||
) -> anyhow::Result<(Vec<GrepFileMatch>, bool)> {
|
||||
let mut matched_files = HashMap::new();
|
||||
let mut lines = output.trim().lines();
|
||||
|
||||
for line in output.trim().split("\n") {
|
||||
for line in lines.by_ref().take(GREP_MAX_MATCHES) {
|
||||
let mut parts = line.split(":");
|
||||
let file = parts.next();
|
||||
let line_number = parts.next();
|
||||
@@ -677,17 +715,24 @@ fn parse_grep_output(
|
||||
.push(GrepLineMatch { line_number });
|
||||
}
|
||||
|
||||
Ok(matched_files
|
||||
.into_iter()
|
||||
.map(|(file, matched_lines)| GrepFileMatch {
|
||||
file_path: host_native_absolute_path(
|
||||
file,
|
||||
&shell_launch_data,
|
||||
¤t_working_directory,
|
||||
),
|
||||
matched_lines,
|
||||
})
|
||||
.collect())
|
||||
let truncated = lines.next().is_some()
|
||||
|| matched_files
|
||||
.values()
|
||||
.any(|matched_lines| matched_lines.len() >= GREP_MAX_MATCHES_PER_FILE);
|
||||
Ok((
|
||||
matched_files
|
||||
.into_iter()
|
||||
.map(|(file, matched_lines)| GrepFileMatch {
|
||||
file_path: host_native_absolute_path(
|
||||
file,
|
||||
&shell_launch_data,
|
||||
¤t_working_directory,
|
||||
),
|
||||
matched_lines,
|
||||
})
|
||||
.collect(),
|
||||
truncated,
|
||||
))
|
||||
}
|
||||
|
||||
impl Entity for GrepExecutor {
|
||||
|
||||
@@ -81,7 +81,7 @@ fn build_git_grep_command_single_quotes_shell_substitution() {
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
"git --no-pager grep --color=never --untracked -nIE -e '$(touch /tmp/warp-poc); `id`' '/tmp/repo path'"
|
||||
"git --no-pager grep --color=never --untracked -nIE -m 50 -e '$(touch /tmp/warp-poc); `id`' '/tmp/repo path'"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ fn build_grep_command_escapes_single_quotes() {
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
r#"grep --color=never -nrIHE --devices=skip -e 'owner'"'"'s code' '/tmp/repo'"#
|
||||
r#"grep --color=never -nrIHE --devices=skip -m 50 -e 'owner'"'"'s code' '/tmp/repo'"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,23 @@ fn build_select_string_command_single_quotes_powershell_substitution() {
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
r#"Get-ChildItem -Path 'C:\repo path' -Recurse -File | Select-String -NoEmphasis -CaseSensitive -Pattern '$(New-Item C:\pwn); ''literal'''"#
|
||||
r#"Get-ChildItem -Path 'C:\repo path' -Recurse -File | Select-String -NoEmphasis -CaseSensitive -Pattern '$(New-Item C:\pwn); ''literal''' | Select-Object -First 1001"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_grep_output_reports_when_results_reach_the_per_file_limit() {
|
||||
let output = (1..=GREP_MAX_MATCHES_PER_FILE)
|
||||
.map(|line| format!("src/lib.rs:{line}:match"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let (matched_files, truncated) = parse_grep_output(&output, None, None).unwrap();
|
||||
|
||||
assert_eq!(matched_files.len(), 1);
|
||||
assert_eq!(
|
||||
matched_files[0].matched_lines.len(),
|
||||
GREP_MAX_MATCHES_PER_FILE
|
||||
);
|
||||
assert!(truncated);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
use futures::future::{join_all, BoxFuture};
|
||||
use futures::FutureExt;
|
||||
use galaxyui::{Entity, EntityId, ModelContext, ModelHandle, SingletonEntity};
|
||||
|
||||
@@ -236,14 +236,24 @@ impl ReadFilesExecutor {
|
||||
// Local path.
|
||||
ActionExecution::Async {
|
||||
execute_future: Box::pin(async move {
|
||||
let result = read_local_file_context(
|
||||
&locations,
|
||||
current_working_directory,
|
||||
shell,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let reads = locations.iter().map(|location| {
|
||||
read_local_file_context(
|
||||
std::slice::from_ref(location),
|
||||
current_working_directory.clone(),
|
||||
shell.clone(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
});
|
||||
let mut result = super::ReadFileContextResult {
|
||||
file_contexts: Vec::new(),
|
||||
missing_files: Vec::new(),
|
||||
};
|
||||
for file_result in join_all(reads).await {
|
||||
let file_result = file_result?;
|
||||
result.file_contexts.extend(file_result.file_contexts);
|
||||
result.missing_files.extend(file_result.missing_files);
|
||||
}
|
||||
if result.missing_files.is_empty() {
|
||||
Ok(ReadFilesResult::Success {
|
||||
files: result.file_contexts,
|
||||
|
||||
Reference in New Issue
Block a user