Add local project indexing and search guidance
This commit is contained in:
Generated
+1
@@ -275,6 +275,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"virtual-fs",
|
||||
"warp_multi_agent_api",
|
||||
"warp_search_core",
|
||||
"watcher",
|
||||
]
|
||||
|
||||
|
||||
@@ -4,9 +4,14 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::{
|
||||
LocalIndexStatus, LocalProjectIndexEvent, LocalProjectIndexManager,
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::join_all;
|
||||
use galaxy_cli::agent::Harness;
|
||||
@@ -90,6 +95,9 @@ pub fn prepare_environment(
|
||||
if should_subscribe_to_index_updates && result.is_err() {
|
||||
let _ = spawner
|
||||
.spawn(|_, ctx| {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.unsubscribe_from_model(&LocalProjectIndexManager::handle(ctx));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||
})
|
||||
.await;
|
||||
@@ -201,6 +209,9 @@ async fn prepare_environment_impl(
|
||||
} else if should_index_codebase && source_repos.is_empty() {
|
||||
let _ = spawner
|
||||
.spawn(|_, ctx| {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.unsubscribe_from_model(&LocalProjectIndexManager::handle(ctx));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||
})
|
||||
.await;
|
||||
@@ -235,6 +246,9 @@ fn record_codebase_indexing(
|
||||
setup_events.record_value_detached(SetupStep::EnvironmentCodebaseIndexing, async move {
|
||||
let _ = spawner
|
||||
.spawn(|_, ctx| {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.unsubscribe_from_model(&LocalProjectIndexManager::handle(ctx));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||
})
|
||||
.await;
|
||||
@@ -255,6 +269,9 @@ fn record_codebase_indexing(
|
||||
}
|
||||
let _ = spawner
|
||||
.spawn(|_, ctx| {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.unsubscribe_from_model(&LocalProjectIndexManager::handle(ctx));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||
})
|
||||
.await;
|
||||
@@ -489,6 +506,7 @@ pub(super) async fn register_cloned_repo(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
async fn subscribe_to_codebase_index_events(
|
||||
spawner: &ModelSpawner<TerminalDriver>,
|
||||
repo_channels: Arc<Mutex<HashMap<PathBuf, oneshot::Sender<()>>>>,
|
||||
@@ -496,11 +514,10 @@ async fn subscribe_to_codebase_index_events(
|
||||
spawner
|
||||
.spawn(move |_, ctx| {
|
||||
let repo_channels = Arc::clone(&repo_channels);
|
||||
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), move |_, _, event, ctx| {
|
||||
if !matches!(
|
||||
event,
|
||||
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
|
||||
) {
|
||||
ctx.subscribe_to_model(
|
||||
&CodebaseIndexManager::handle(ctx),
|
||||
move |_, _, event, ctx| {
|
||||
if !matches!(event, CodebaseIndexManagerEvent::SyncStateUpdated { .. }) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -511,22 +528,15 @@ async fn subscribe_to_codebase_index_events(
|
||||
.expect("repo channel map lock should not be poisoned");
|
||||
|
||||
for repo in channels.keys() {
|
||||
let Some(status) =
|
||||
manager.get_codebase_index_status_for_path(repo, ctx)
|
||||
let Some(status) = manager.get_codebase_index_status_for_path(repo, ctx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if status.has_synced_version() {
|
||||
repos_to_notify.push(repo.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
if !status.has_pending() && status.last_sync_successful() == Some(false) {
|
||||
safe_warn!(
|
||||
safe: ("Codebase index sync failed for a repo; unblocking environment setup"),
|
||||
full: ("Codebase index sync failed for {repo:?}; unblocking environment setup")
|
||||
);
|
||||
if status.has_synced_version()
|
||||
|| (!status.has_pending()
|
||||
&& status.last_sync_successful() == Some(false))
|
||||
{
|
||||
repos_to_notify.push(repo.clone());
|
||||
}
|
||||
}
|
||||
@@ -536,12 +546,58 @@ async fn subscribe_to_codebase_index_events(
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
async fn subscribe_to_codebase_index_events(
|
||||
spawner: &ModelSpawner<TerminalDriver>,
|
||||
repo_channels: Arc<Mutex<HashMap<PathBuf, oneshot::Sender<()>>>>,
|
||||
) -> Result<(), PrepareEnvironmentError> {
|
||||
spawner
|
||||
.spawn(move |_, ctx| {
|
||||
let repo_channels = Arc::clone(&repo_channels);
|
||||
ctx.subscribe_to_model(
|
||||
&LocalProjectIndexManager::handle(ctx),
|
||||
move |_, _, event, _ctx| {
|
||||
if !matches!(event, LocalProjectIndexEvent::StatusChanged { .. }) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut repos_to_notify = Vec::new();
|
||||
let mut channels = repo_channels
|
||||
.lock()
|
||||
.expect("repo channel map lock should not be poisoned");
|
||||
for repo in channels.keys() {
|
||||
// The manager event identifies the changed root, while the channel map can
|
||||
// contain nested paths. Re-check each path so a root-level completion wakes
|
||||
// the exact environment request that owns it.
|
||||
if LocalProjectIndexManager::as_ref(_ctx).is_ready_for_path(repo)
|
||||
|| matches!(
|
||||
LocalProjectIndexManager::as_ref(_ctx).status_for_path(repo),
|
||||
Some((_, LocalIndexStatus::Failed { .. }))
|
||||
)
|
||||
{
|
||||
repos_to_notify.push(repo.clone());
|
||||
}
|
||||
}
|
||||
for repo in repos_to_notify {
|
||||
if let Some(tx) = channels.remove(&repo) {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
#[tracing::instrument(skip_all, err, fields(tags.cloud_agent = true, repo = %repo_name))]
|
||||
async fn index_repo_codebase(
|
||||
repo_name: &str,
|
||||
@@ -592,6 +648,57 @@ async fn index_repo_codebase(
|
||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
#[tracing::instrument(skip_all, err, fields(tags.cloud_agent = true, repo = %repo_name))]
|
||||
async fn index_repo_codebase(
|
||||
repo_name: &str,
|
||||
working_dir: &Path,
|
||||
repo_channels: Arc<Mutex<HashMap<PathBuf, oneshot::Sender<()>>>>,
|
||||
spawner: &ModelSpawner<TerminalDriver>,
|
||||
) -> Result<Option<oneshot::Receiver<()>>, PrepareEnvironmentError> {
|
||||
let repo_path = working_dir.join(repo_name);
|
||||
|
||||
safe_info!(
|
||||
safe: ("Trying to index repository for codebase context"),
|
||||
full: ("Trying to index {:?} for codebase context", repo_path)
|
||||
);
|
||||
|
||||
let repo_path_for_spawn = repo_path.clone();
|
||||
spawner
|
||||
.spawn(move |_, ctx| {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
repo_channels
|
||||
.lock()
|
||||
.expect("repo channel map lock should not be poisoned")
|
||||
.insert(repo_path_for_spawn.clone(), tx);
|
||||
|
||||
let indexing_result = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(repo_path_for_spawn.clone(), ctx)
|
||||
});
|
||||
if let Err(error) = indexing_result {
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
repo_channels
|
||||
.lock()
|
||||
.expect("repo channel map lock should not be poisoned")
|
||||
.remove(&repo_path_for_spawn);
|
||||
return None;
|
||||
}
|
||||
|
||||
if LocalProjectIndexManager::as_ref(ctx).is_ready_for_path(&repo_path_for_spawn) {
|
||||
repo_channels
|
||||
.lock()
|
||||
.expect("repo channel map lock should not be poisoned")
|
||||
.remove(&repo_path_for_spawn);
|
||||
None
|
||||
} else {
|
||||
Some(rx)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
||||
}
|
||||
|
||||
/// Execute a command in the context of a terminal session.
|
||||
async fn execute_command(
|
||||
command: String,
|
||||
|
||||
@@ -2,7 +2,10 @@ use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use markdown_parser::FormattedTextFragment;
|
||||
use rand::seq::SliceRandom;
|
||||
use warpui::keymap::Keystroke;
|
||||
@@ -411,6 +414,9 @@ impl AITip for AgentTip {
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
return !LocalProjectIndexManager::as_ref(app).is_ready_for_path(root);
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
return CodebaseIndexManager::as_ref(app)
|
||||
.get_codebase_index_status_for_path(root, app)
|
||||
.is_none();
|
||||
|
||||
@@ -1558,6 +1558,11 @@ pub fn extract_system_prompt(
|
||||
));
|
||||
}
|
||||
let has_tool = |name: &str| tool_names.iter().any(|tool_name| tool_name == name);
|
||||
if has_tool("search_codebase") {
|
||||
prompt.push_str(
|
||||
"- For source-code discovery, semantic questions, or finding an unfamiliar implementation, MUST use `search_codebase` first. Never use `grep` to discover or search source code when `search_codebase` is available; reserve `grep` for exact known text in non-code files. Use `file_glob` only to locate filenames and `read_files` for focused follow-up context.\n",
|
||||
);
|
||||
}
|
||||
if [
|
||||
"read_files",
|
||||
"apply_file_diffs",
|
||||
@@ -1569,7 +1574,7 @@ pub fn extract_system_prompt(
|
||||
.any(has_tool)
|
||||
{
|
||||
prompt.push_str(
|
||||
"- Use filesystem/search tools to understand the codebase and focused diff tools to edit \
|
||||
"- Use the appropriate filesystem/search tool to understand the codebase and focused diff tools to edit \
|
||||
it. Use paths rooted in the working directory and absolute paths when a schema requires \
|
||||
them.\n",
|
||||
);
|
||||
@@ -1783,7 +1788,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "grep".to_string(),
|
||||
description: "Search for regex patterns in files. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Use read_files afterward to see context around matches. Pass ALL patterns you need in one call.".to_string(),
|
||||
description: "Search for regex patterns in files. Use only for exact known text in non-code files; never use this to discover or search source code when search_codebase is available. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Use read_files afterward to see context around matches. Pass ALL patterns you need in one call.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1795,7 +1800,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "file_glob".to_string(),
|
||||
description: "Find files matching glob patterns. Uses git ls-files in git repos. Returns absolute file paths of matches. Common patterns: '**/*.rs', 'src/**/*.ts', '**/Cargo.toml'. Pass ALL patterns in one call.".to_string(),
|
||||
description: "Find filenames matching glob patterns. Use this to locate files, not to search their contents. Uses git ls-files in git repos. Returns absolute file paths of matches. Common patterns: '**/*.rs', 'src/**/*.ts', '**/Cargo.toml'. Pass ALL patterns in one call.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1807,7 +1812,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "search_codebase".to_string(),
|
||||
description: "Semantic code search across the indexed codebase. Use for finding relevant code by meaning rather than exact text match. Better than grep for conceptual queries like 'authentication logic' or 'error handling for database connections'.".to_string(),
|
||||
description: "Primary tool for discovering relevant source code across the indexed codebase. MUST use this first for all source-code discovery and conceptual, semantic, or structural searches such as authentication logic or database error handling. Never use grep to search source code when this tool is available.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -107,6 +107,38 @@ fn advertised_tools_follow_client_capabilities_and_include_local_subagents() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bedrock_prompt_prioritizes_semantic_code_search_over_regex_search() {
|
||||
let request = api::Request {
|
||||
settings: Some(api::request::Settings {
|
||||
supported_tools: vec![
|
||||
api::ToolType::SearchCodebase.into(),
|
||||
api::ToolType::Grep.into(),
|
||||
api::ToolType::FileGlob.into(),
|
||||
api::ToolType::ReadFiles.into(),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let prompt = extract_system_prompt(&request, &[]).expect("system prompt");
|
||||
assert!(prompt.contains("MUST use `search_codebase` first"));
|
||||
assert!(prompt.contains("Never use `grep` to discover or search source code"));
|
||||
|
||||
let tools = extract_tools(&request);
|
||||
let search_codebase = tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "search_codebase")
|
||||
.expect("search_codebase tool");
|
||||
let grep = tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "grep")
|
||||
.expect("grep tool");
|
||||
assert!(search_codebase.description.contains("MUST use this first"));
|
||||
assert!(grep.description.contains("non-code files"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_provider_advertises_local_tool_history_recall() {
|
||||
let request = api::Request {
|
||||
|
||||
@@ -226,6 +226,7 @@ impl GetFilesExecutor {
|
||||
GetRelevantFilesError::Missing => {
|
||||
"The current directory isn't within a git repository, which is necessary to search for relevant files.".to_owned()
|
||||
}
|
||||
GetRelevantFilesError::LocalIndexUnavailable(message) => message,
|
||||
};
|
||||
ActionExecution::Sync(AIAgentActionResultType::GetFiles(
|
||||
GetFilesResult::Error(error_message),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
@@ -273,6 +275,7 @@ impl SearchCodebaseExecutor {
|
||||
),
|
||||
},
|
||||
Err(e) => {
|
||||
self.active_searches.remove(id);
|
||||
log::warn!("Failed to send remote get_relevant_files request: {e:?}");
|
||||
ActionExecution::Sync(AIAgentActionResultType::SearchCodebase(
|
||||
SearchCodebaseResult::Failed {
|
||||
@@ -385,6 +388,7 @@ impl SearchCodebaseExecutor {
|
||||
),
|
||||
},
|
||||
Err(e) => {
|
||||
self.active_searches.remove(id);
|
||||
log::warn!("Failed to send get_relevant_files request for directory: {e:?}");
|
||||
|
||||
let error_message = match e {
|
||||
@@ -397,6 +401,7 @@ impl SearchCodebaseExecutor {
|
||||
GetRelevantFilesError::Missing => {
|
||||
"The current directory isn't within a git repository, which is necessary to search for relevant files.".to_owned()
|
||||
}
|
||||
GetRelevantFilesError::LocalIndexUnavailable(message) => message,
|
||||
};
|
||||
ActionExecution::Sync(AIAgentActionResultType::SearchCodebase(
|
||||
SearchCodebaseResult::Failed {
|
||||
@@ -465,6 +470,13 @@ impl SearchCodebaseExecutor {
|
||||
pwd
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Some(root) = app
|
||||
.try_get_singleton_model_as_ref::<LocalProjectIndexManager>()
|
||||
.and_then(|manager| manager.root_for_path(&search_dir))
|
||||
{
|
||||
return Some(root);
|
||||
}
|
||||
self.get_relevant_files_controller
|
||||
.as_ref(app)
|
||||
.root_directory_for_search(&search_dir, app)
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::ui_components::blended_colors;
|
||||
use crate::ui_components::icons::Icon;
|
||||
|
||||
const SPEEDBUMP_HEADER: &str = "Index Codebase?";
|
||||
const SPEEDBUMP_TEXT: &str = "Indexing helps agents quickly understand context and provide targeted solutions. Code is never stored on the server.";
|
||||
const SPEEDBUMP_TEXT: &str = "Galaxy builds and searches this project index on your device. Source files are read from disk when results are returned to the agent.";
|
||||
/// Uniform padding around the banner
|
||||
const PADDING: f32 = 12.;
|
||||
/// Text for the button that allows execution
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use chrono::Local;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_graphql::generic_string_object::GenericStringObjectFormat as GraphQLFormat;
|
||||
@@ -65,10 +66,12 @@ pub(super) fn input_context_for_request(
|
||||
context.push(AIAgentContext::ExecutionEnvironment(env));
|
||||
}
|
||||
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||
&& FeatureFlag::CrossRepoContext.is_enabled()
|
||||
{
|
||||
let session_context = SessionContext::from_session(active_session, app);
|
||||
let should_add_codebase_context = FeatureFlag::CrossRepoContext.is_enabled()
|
||||
&& ((session_context.is_remote() && FeatureFlag::FullSourceCodeEmbedding.is_enabled())
|
||||
|| (!session_context.is_remote()
|
||||
&& cfg!(all(feature = "local_fs", not(target_family = "wasm")))));
|
||||
if should_add_codebase_context {
|
||||
if session_context.is_remote() {
|
||||
add_remote_codebase_context(&mut context, &session_context, app);
|
||||
} else {
|
||||
@@ -91,27 +94,28 @@ pub(super) fn input_context_for_request(
|
||||
context.into()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
fn add_local_codebase_context(context: &mut Vec<AIAgentContext>, app: &AppContext) {
|
||||
for (codebase_path, status) in
|
||||
CodebaseIndexManager::as_ref(app).get_codebase_index_statuses(app)
|
||||
{
|
||||
// TODO(daniel): We should figure out a mechanism for handling stale codebases.
|
||||
if status.has_synced_version() {
|
||||
// For now, we pass the name of the directory as the name of the
|
||||
// codebase.
|
||||
for (codebase_path, status) in LocalProjectIndexManager::as_ref(app).statuses() {
|
||||
if matches!(
|
||||
status,
|
||||
ai::index::local_project_index::LocalIndexStatus::Ready { .. }
|
||||
) {
|
||||
let codebase_name = codebase_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
|
||||
context.push(AIAgentContext::Codebase {
|
||||
name: codebase_name.into(),
|
||||
path: codebase_path.to_string_lossy().into(),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn add_local_codebase_context(_context: &mut Vec<AIAgentContext>, _app: &AppContext) {}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn add_remote_codebase_context(
|
||||
context: &mut Vec<AIAgentContext>,
|
||||
|
||||
@@ -47,9 +47,16 @@ fn codebase_indexing_enabled(
|
||||
surface: CodebaseAutoIndexingSurface,
|
||||
codebase_context_enabled: bool,
|
||||
) -> bool {
|
||||
match surface {
|
||||
// Native local indexing is structural/lexical and does not depend on the remote
|
||||
// embedding feature. Remote indexing retains its existing feature gates.
|
||||
CodebaseAutoIndexingSurface::Local => codebase_context_enabled,
|
||||
CodebaseAutoIndexingSurface::Remote => {
|
||||
FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||
&& surface.required_feature_enabled()
|
||||
&& codebase_context_enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn codebase_auto_indexing_enabled(
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn local_auto_indexing_requires_full_source_code_embedding_codebase_context_and_auto_indexing() {
|
||||
fn local_auto_indexing_requires_codebase_context_and_auto_indexing() {
|
||||
{
|
||||
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(false);
|
||||
assert!(!codebase_auto_indexing_enabled(
|
||||
assert!(codebase_auto_indexing_enabled(
|
||||
CodebaseAutoIndexingSurface::Local,
|
||||
true,
|
||||
true,
|
||||
));
|
||||
}
|
||||
{
|
||||
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(true);
|
||||
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(false);
|
||||
assert!(codebase_auto_indexing_enabled(
|
||||
CodebaseAutoIndexingSurface::Local,
|
||||
true,
|
||||
|
||||
@@ -2,25 +2,38 @@ use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::RetrievalID;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use ai::index::locations::CodeContextLocation;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use anyhow::anyhow;
|
||||
use futures_util::stream::AbortHandle;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use instant::Instant;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use crate::ai::agent::SearchCodebaseFailureReason;
|
||||
use crate::ai::agent::{AIAgentActionId, SearchCodebaseResult};
|
||||
use crate::ai::blocklist::SessionContext;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::ai::get_relevant_files::api::{FileContext as FileContextRequest, GetRelevantFiles};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use crate::ai::get_relevant_files::controller::GetRelevantFilesError::LocalIndexUnavailable;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::ai::outline::{OutlineStatus, RepoOutlines};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::report_error;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::server::server_api::{AIApiError, ServerApiProvider};
|
||||
use crate::{report_error, send_telemetry_from_ctx, TelemetryEvent};
|
||||
use crate::{send_telemetry_from_ctx, TelemetryEvent};
|
||||
#[cfg_attr(not(target_family = "wasm"), path = "remote_search/native.rs")]
|
||||
#[cfg_attr(target_family = "wasm", path = "remote_search/wasm.rs")]
|
||||
mod remote_search;
|
||||
@@ -60,6 +73,20 @@ pub enum GetRelevantFilesRequestTarget {
|
||||
requested_codebase_path: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Lifecycle state for the two-stage `get_files` relevant-file query.
|
||||
///
|
||||
/// The first stage discovers paths through `GetRelevantFilesController`; the second stage reads
|
||||
/// current file contents in `GetFilesExecutor`. Keeping this state separate from the controller's
|
||||
/// cancellation handles preserves the existing action protocol for both local and legacy search.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GetRelevantFilesStatus {
|
||||
Pending { root_repo_path: PathBuf },
|
||||
InFlight { root_repo_path: PathBuf },
|
||||
Success { file_paths: Vec<PathBuf> },
|
||||
Failed { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GetRelevantFilesError {
|
||||
#[error("Repo outline is still being computed.")]
|
||||
@@ -68,15 +95,18 @@ pub enum GetRelevantFilesError {
|
||||
CreateFailed,
|
||||
#[error("Failed to create outline.")]
|
||||
Missing,
|
||||
#[error("Local project index is unavailable: {0}")]
|
||||
LocalIndexUnavailable(String),
|
||||
}
|
||||
|
||||
/// This enum allows us to use both the existing structure for outline-based indexing
|
||||
/// and the new full source code indexing manager/model.
|
||||
enum RequestHandle {
|
||||
/// Used with outline-based indexing.
|
||||
/// Used with outline-based indexing and remote search.
|
||||
AbortHandle(AbortHandle),
|
||||
|
||||
/// Used with full source code indexing.
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
RetrievalID {
|
||||
repo_path: PathBuf,
|
||||
retrieval_id: RetrievalID,
|
||||
@@ -88,6 +118,7 @@ impl RequestHandle {
|
||||
fn abort(&mut self, ctx: &mut AppContext) {
|
||||
match self {
|
||||
RequestHandle::AbortHandle(abort_handle) => abort_handle.abort(),
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
RequestHandle::RetrievalID {
|
||||
repo_path,
|
||||
retrieval_id,
|
||||
@@ -112,15 +143,63 @@ pub struct GetRelevantFilesController {
|
||||
/// This allows several SearchCodebase actions to be active at once without newer requests
|
||||
/// cancelling unrelated older ones.
|
||||
pending_requests: std::collections::HashMap<AIAgentActionId, RequestHandle>,
|
||||
/// Status for the two-stage `get_files` relevant-file query protocol.
|
||||
relevant_file_statuses: std::collections::HashMap<AIAgentActionId, GetRelevantFilesStatus>,
|
||||
}
|
||||
|
||||
impl GetRelevantFilesController {
|
||||
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
let codebase_manager = CodebaseIndexManager::handle(ctx);
|
||||
ctx.subscribe_to_model(&codebase_manager, Self::handle_codebase_manager_event);
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn root_for_status(&self, directory: &Path, app: &AppContext) -> PathBuf {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
app.try_get_singleton_model_as_ref::<LocalProjectIndexManager>()
|
||||
.and_then(|manager| manager.root_for_path(directory))
|
||||
.unwrap_or_else(|| directory.to_path_buf())
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
self.root_directory_for_search(directory, app)
|
||||
.unwrap_or_else(|| directory.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues the first stage of a relevant-file query for `GetFilesExecutor`.
|
||||
pub fn queue_request(
|
||||
&mut self,
|
||||
action_id: AIAgentActionId,
|
||||
directory: &Path,
|
||||
app: &AppContext,
|
||||
) {
|
||||
let root_repo_path = self.root_for_status(directory, app);
|
||||
self.relevant_file_statuses.insert(
|
||||
action_id,
|
||||
GetRelevantFilesStatus::Pending { root_repo_path },
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the current status of a relevant-file query.
|
||||
pub fn status(&self, action_id: &AIAgentActionId) -> Option<&GetRelevantFilesStatus> {
|
||||
self.relevant_file_statuses.get(action_id)
|
||||
}
|
||||
|
||||
fn set_relevant_file_status(
|
||||
&mut self,
|
||||
action_id: &AIAgentActionId,
|
||||
status: GetRelevantFilesStatus,
|
||||
) {
|
||||
self.relevant_file_statuses
|
||||
.insert(action_id.clone(), status);
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn pending_request_details_for_retrieval_id(
|
||||
&self,
|
||||
pending_retrieval_id: &RetrievalID,
|
||||
@@ -140,6 +219,7 @@ impl GetRelevantFilesController {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn handle_codebase_manager_event(
|
||||
&mut self,
|
||||
_: ModelHandle<CodebaseIndexManager>,
|
||||
@@ -213,7 +293,38 @@ impl GetRelevantFilesController {
|
||||
self.cancel_request_for_action(&action_id, ctx);
|
||||
match target {
|
||||
GetRelevantFilesRequestTarget::Local { directory } => {
|
||||
self.send_local_request(&directory, query, partial_path_segments, action_id, ctx)
|
||||
let root_repo_path = self
|
||||
.relevant_file_statuses
|
||||
.get(&action_id)
|
||||
.and_then(|status| match status {
|
||||
GetRelevantFilesStatus::Pending { root_repo_path }
|
||||
| GetRelevantFilesStatus::InFlight { root_repo_path } => {
|
||||
Some(root_repo_path.clone())
|
||||
}
|
||||
GetRelevantFilesStatus::Success { .. }
|
||||
| GetRelevantFilesStatus::Failed { .. } => None,
|
||||
})
|
||||
.unwrap_or_else(|| self.root_for_status(&directory, ctx));
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::InFlight { root_repo_path },
|
||||
);
|
||||
let result = self.send_local_request(
|
||||
&directory,
|
||||
query,
|
||||
partial_path_segments,
|
||||
action_id.clone(),
|
||||
ctx,
|
||||
);
|
||||
if let Err(error) = &result {
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Failed {
|
||||
message: error.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
GetRelevantFilesRequestTarget::Remote {
|
||||
session_context,
|
||||
@@ -237,8 +348,45 @@ impl GetRelevantFilesController {
|
||||
action_id: AIAgentActionId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Result<(), GetRelevantFilesError> {
|
||||
const MINIMUM_FILE_COUNT_FOR_API_CALL: usize = 2;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
let Some(local_index) =
|
||||
ctx.try_get_singleton_model_as_ref::<LocalProjectIndexManager>()
|
||||
else {
|
||||
return Err(LocalIndexUnavailable(
|
||||
"The local project index is not available in this session.".to_string(),
|
||||
));
|
||||
};
|
||||
if local_index.is_searchable_for_path(directory) {
|
||||
let locations: HashSet<CodeContextLocation> = local_index
|
||||
.search(directory, &query, partial_path_segments.map(Vec::as_slice))
|
||||
.map_err(|error| LocalIndexUnavailable(error.to_string()))?
|
||||
.into_iter()
|
||||
.map(|hit| CodeContextLocation::WholeFile(hit.path))
|
||||
.collect();
|
||||
let file_paths = locations
|
||||
.iter()
|
||||
.map(|location| location.path().clone())
|
||||
.collect();
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Success { file_paths },
|
||||
);
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::Locations(Arc::new(locations)),
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
return Err(LocalIndexUnavailable(
|
||||
"The local project index is not ready. Run /init or /index first.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let _ = (directory, query, partial_path_segments, action_id, ctx);
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||
let codebase_mgr = CodebaseIndexManager::handle(ctx);
|
||||
if let Some(base_path) = codebase_mgr.as_ref(ctx).root_path_for_codebase(directory) {
|
||||
@@ -268,22 +416,33 @@ impl GetRelevantFilesController {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
match RepoOutlines::as_ref(ctx).get_outline(directory) {
|
||||
Some((OutlineStatus::Complete(outline), base_path)) => {
|
||||
let server_api = ServerApiProvider::as_ref(ctx).get();
|
||||
|
||||
let file_outlines = outline.to_file_symbols(partial_path_segments);
|
||||
if file_outlines.len() < MINIMUM_FILE_COUNT_FOR_API_CALL {
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::Locations(Arc::new(
|
||||
let locations: Arc<HashSet<CodeContextLocation>> = Arc::new(
|
||||
file_outlines
|
||||
.into_iter()
|
||||
.map(|file| {
|
||||
CodeContextLocation::WholeFile(PathBuf::from(file.path))
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
);
|
||||
let file_paths = locations
|
||||
.iter()
|
||||
.map(|location| location.path().clone())
|
||||
.collect();
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Success { file_paths },
|
||||
);
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::Locations(locations),
|
||||
});
|
||||
} else {
|
||||
let outline_request = GetRelevantFiles {
|
||||
@@ -342,6 +501,9 @@ impl GetRelevantFilesController {
|
||||
None => Err(GetRelevantFilesError::Missing),
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
unreachable!("native local search returns before the legacy outline fallback")
|
||||
}
|
||||
|
||||
fn send_remote_request(
|
||||
&mut self,
|
||||
@@ -375,6 +537,7 @@ impl GetRelevantFilesController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn handle_relevant_file_paths_result(
|
||||
&mut self,
|
||||
relevant_file_locations: anyhow::Result<Arc<HashSet<CodeContextLocation>>>,
|
||||
@@ -386,12 +549,27 @@ impl GetRelevantFilesController {
|
||||
}
|
||||
match relevant_file_locations {
|
||||
Ok(relevant_file_locations) => {
|
||||
let file_paths = relevant_file_locations
|
||||
.iter()
|
||||
.map(|location| location.path().clone())
|
||||
.collect();
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Success { file_paths },
|
||||
);
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::Locations(relevant_file_locations),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
let message = e.to_string();
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Failed {
|
||||
message: message.clone(),
|
||||
},
|
||||
);
|
||||
report_error!(anyhow!(e).context("get_relevant_files failed"));
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Error { action_id });
|
||||
}
|
||||
@@ -421,6 +599,14 @@ impl GetRelevantFilesController {
|
||||
|
||||
/// Returns the path to the root directory for a codebase search where pwd is `directory`.
|
||||
pub fn root_directory_for_search(&self, directory: &Path, app: &AppContext) -> Option<PathBuf> {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
app.try_get_singleton_model_as_ref::<LocalProjectIndexManager>()
|
||||
.and_then(|manager| manager.root_for_path(directory))
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
let mut start = None;
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||
start = CodebaseIndexManager::as_ref(app).root_path_for_codebase(directory);
|
||||
@@ -431,6 +617,7 @@ impl GetRelevantFilesController {
|
||||
.map(|(_, root)| root)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root_directory_for_remote_search(
|
||||
&self,
|
||||
|
||||
@@ -3,15 +3,21 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use ai::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
|
||||
use ai::workspace::{WorkspaceMetadata, WorkspaceMetadataEvent};
|
||||
use anyhow::Context;
|
||||
use chrono::Utc;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use galaxy_core::channel::ChannelState;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use galaxy_core::execution_mode::AppExecutionMode;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use galaxy_util::{local_or_remote_path::LocalOrRemotePath, standardized_path::StandardizedPath};
|
||||
@@ -46,6 +52,7 @@ use crate::code::lsp_telemetry::LspTelemetryEvent;
|
||||
use crate::persistence::ModelEvent;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::server::server_api::ServerApiProvider;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::settings::CodeSettings;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::terminal::local_shell::LocalShellState;
|
||||
@@ -55,6 +62,12 @@ use crate::{report_if_error, send_telemetry_from_ctx, TelemetryEvent};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::{view_components::DismissibleToast, workspace::ToastStack};
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
fn local_project_indexing_enabled(ctx: &AppContext) -> bool {
|
||||
ctx.try_get_singleton_model_as_ref::<AppExecutionMode>()
|
||||
.is_some_and(|mode| mode.local_project_indexing_enabled())
|
||||
}
|
||||
|
||||
/// Represents whether an LSP server is enabled or disabled for a workspace.
|
||||
///
|
||||
/// This is also used in underlying sqlite type persistence. We should be careful
|
||||
@@ -239,6 +252,7 @@ impl PersistedWorkspace {
|
||||
})
|
||||
.collect();
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
|
||||
match event {
|
||||
@@ -266,7 +280,7 @@ impl PersistedWorkspace {
|
||||
..
|
||||
} = event
|
||||
{
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
me.clean_up_deleted_indices(ctx);
|
||||
|
||||
me.trigger_incremental_sync_for_conversation(*terminal_surface_id, ctx);
|
||||
@@ -310,12 +324,68 @@ impl PersistedWorkspace {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
if !cfg!(any(
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx) {
|
||||
ctx.subscribe_to_model(
|
||||
&UserWorkspaces::handle(ctx),
|
||||
|me, _, user_workspaces_event, ctx| {
|
||||
if let UserWorkspacesEvent::CodebaseContextEnablementChanged =
|
||||
user_workspaces_event
|
||||
{
|
||||
me.on_settings_changed(ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx) {
|
||||
ctx.subscribe_to_model(
|
||||
&BlocklistAIHistoryModel::handle(ctx),
|
||||
|me, _, event, ctx| {
|
||||
if let BlocklistAIHistoryEvent::StartedNewConversation {
|
||||
terminal_surface_id,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
me.trigger_incremental_sync_for_conversation(*terminal_surface_id, ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx) {
|
||||
ctx.subscribe_to_model(&ProjectContextModel::handle(ctx), |me, _, event, _ctx| {
|
||||
if let ProjectContextModelEvent::KnownRulesChanged(delta) = event {
|
||||
let mut events = vec![];
|
||||
|
||||
if !delta.discovered_rules.is_empty() {
|
||||
events.push(ModelEvent::UpsertProjectRules {
|
||||
project_rule_paths: delta.discovered_rules.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if !delta.deleted_rules.is_empty() {
|
||||
events.push(ModelEvent::DeleteProjectRules {
|
||||
path: delta.deleted_rules.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if !events.is_empty() {
|
||||
me.save_to_db(events);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx)
|
||||
&& !cfg!(any(
|
||||
test,
|
||||
feature = "fast_dev",
|
||||
feature = "integration_tests"
|
||||
)) && CodebaseIndexManager::as_ref(ctx).is_indexing_enabled()
|
||||
))
|
||||
{
|
||||
ctx.subscribe_to_model(&DetectedRepositories::handle(ctx), |me, _, event, ctx| {
|
||||
let DetectedRepositoriesEvent::DetectedGitRepo { repository, .. } = event;
|
||||
@@ -634,6 +704,33 @@ impl PersistedWorkspace {
|
||||
|
||||
/// Enables or disables codebase indexing according to the setting.
|
||||
fn maybe_enable_codebase_indexing(ctx: &mut ModelContext<Self>) {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
if local_project_indexing_enabled(ctx)
|
||||
&& should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx)
|
||||
{
|
||||
for root in auto_index_candidate_roots(
|
||||
all_working_directories(ctx)
|
||||
.into_iter()
|
||||
.filter_map(|directory| {
|
||||
DetectedRepositories::as_ref(ctx)
|
||||
.get_root_for_path(&LocalOrRemotePath::Local(directory))
|
||||
.and_then(|root| root.to_local_path().map(Path::to_path_buf))
|
||||
}),
|
||||
|_| true,
|
||||
) {
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(root.to_path_buf(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
if !manager.is_indexing_enabled() {
|
||||
return;
|
||||
@@ -648,6 +745,7 @@ impl PersistedWorkspace {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn enable_codebase_indexing(
|
||||
manager: &mut CodebaseIndexManager,
|
||||
ctx: &mut ModelContext<CodebaseIndexManager>,
|
||||
@@ -661,7 +759,7 @@ impl PersistedWorkspace {
|
||||
ctx,
|
||||
);
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[cfg(all(feature = "local_fs", target_family = "wasm"))]
|
||||
if should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx) {
|
||||
let roots = all_working_directories(ctx).into_iter().filter_map(|dir| {
|
||||
DetectedRepositories::as_ref(ctx)
|
||||
@@ -683,6 +781,17 @@ impl PersistedWorkspace {
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx)
|
||||
&& should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx)
|
||||
{
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(directory_path.clone(), ctx)
|
||||
}) {
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||
&& UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx)
|
||||
&& *CodeSettings::as_ref(ctx).auto_indexing_enabled
|
||||
@@ -700,6 +809,8 @@ impl PersistedWorkspace {
|
||||
/// scanning, and emits
|
||||
/// [`PersistedWorkspaceEvent::WorkspaceAdded`] so subscribers can refresh their UI.
|
||||
pub fn user_added_workspace(&mut self, path: PathBuf, ctx: &mut ModelContext<Self>) {
|
||||
#[cfg(feature = "local_fs")]
|
||||
let path = dunce::canonicalize(&path).unwrap_or(path);
|
||||
let now = Utc::now();
|
||||
|
||||
match self.workspaces.get_mut(&path) {
|
||||
@@ -850,7 +961,15 @@ impl PersistedWorkspace {
|
||||
if let Some(pwd) = pwd {
|
||||
let directory_path = PathBuf::from(pwd);
|
||||
|
||||
// Trigger an incremental sync through the CodebaseIndexManager
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(directory_path.clone(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to refresh local project index: {error:#}");
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
||||
if let Err(e) = codebase_manager
|
||||
.trigger_incremental_sync_for_path(&directory_path, ctx)
|
||||
@@ -907,7 +1026,7 @@ impl PersistedWorkspace {
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn clean_up_deleted_indices(&self, ctx: &mut ModelContext<Self>) {
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
||||
codebase_manager.clean_up_deleted_indices(ctx);
|
||||
@@ -1417,6 +1536,13 @@ impl PersistedWorkspace {
|
||||
}
|
||||
|
||||
fn send_active_indexed_repos_changed_telemetry<T: Entity>(ctx: &mut ModelContext<T>) {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let total = if local_project_indexing_enabled(ctx) {
|
||||
LocalProjectIndexManager::as_ref(ctx).statuses().count()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let total = CodebaseIndexManager::as_ref(ctx).num_active_indices();
|
||||
let hit_max = AIRequestUsageModel::as_ref(ctx).hit_codebase_index_limit(total);
|
||||
send_telemetry_from_ctx!(
|
||||
|
||||
@@ -34,10 +34,11 @@ const CODE_INSTRUCTIONS: &str = r#"## Instructions
|
||||
|
||||
### Doing Tasks
|
||||
The user will primarily request software engineering tasks: solving bugs, adding features, refactoring, explaining code, and more. For these tasks:
|
||||
1. Use search tools to understand the codebase and the user's query. Search extensively — both in parallel and sequentially.
|
||||
2. Implement the solution using all tools available to you.
|
||||
3. Verify the solution if possible with tests. NEVER assume a specific test framework — check the project first.
|
||||
4. After making changes, run lint/typecheck/build commands if you know them, to ensure correctness.
|
||||
1. For source-code discovery, semantic questions, or finding an unfamiliar implementation, use `search_codebase` first. Never use `grep` to discover or search source code when `search_codebase` is available; reserve it for exact known text in non-code files. Use `file_glob` to locate filenames and `read_files` for focused follow-up context.
|
||||
2. Search extensively — both in parallel and sequentially.
|
||||
3. Implement the solution using all tools available to you.
|
||||
4. Verify the solution if possible with tests. NEVER assume a specific test framework — check the project first.
|
||||
5. After making changes, run lint/typecheck/build commands if you know them, to ensure correctness.
|
||||
|
||||
NEVER commit changes unless the user explicitly asks you to.
|
||||
|
||||
@@ -62,7 +63,7 @@ CRITICAL: You are in READ-ONLY planning mode. You MUST NOT:
|
||||
|
||||
You MAY:
|
||||
- Read files
|
||||
- Search the codebase (grep, glob)
|
||||
- Search the codebase with `search_codebase`; use `file_glob` for filenames and `grep` only for exact known text in non-code files
|
||||
- Run read-only shell commands (ls, cat, git log, git status)
|
||||
- Ask the user clarifying questions
|
||||
|
||||
@@ -116,7 +117,8 @@ const CODE_TOOL_GUIDELINES: &str = r#"## Tool Usage
|
||||
You have been given every tool you need to complete your tasks. Use them to achieve results with as few calls and as little back-and-forth as possible.
|
||||
|
||||
**How to choose tools:**
|
||||
- For reading, writing, searching, and navigating files on the local filesystem, use your filesystem tools (`read_files`, `file_glob`, `grep`, `apply_file_diffs`).
|
||||
- For source-code discovery, use `search_codebase` first. Use `file_glob` to locate filenames and `read_files` to inspect focused files. Never use `grep` to discover or search source code when `search_codebase` is available; reserve it for exact known text in non-code files.
|
||||
- For writing and editing files, use `apply_file_diffs`.
|
||||
- For running commands, installing packages, building, testing, and any shell operation, use `run_shell_command`.
|
||||
- For tasks that require interacting with external services, web UIs, or capabilities not covered by your filesystem and shell tools, use your MCP tools.
|
||||
- For complex multi-step tasks where a single script would replace many tool calls, write code (Python, Node, bash) via `run_shell_command` to reduce round-trips. But never use scripts for simple operations that a single command handles.
|
||||
@@ -140,10 +142,10 @@ If you intend to call multiple tools and there are no dependencies between the c
|
||||
|
||||
const PLAN_TOOL_GUIDELINES: &str = r#"## Tool Usage
|
||||
You have access to read-only tools for exploring the codebase:
|
||||
- `read_files` — Read file contents
|
||||
- `grep` — Search for patterns in files
|
||||
- `search_codebase` — Primary tool for source-code discovery and semantic or structural questions
|
||||
- `file_glob` — Find files by glob pattern
|
||||
- `search_codebase` — Semantic code search
|
||||
- `read_files` — Read focused file contents after discovery
|
||||
- `grep` — Search exact known text in non-code files only
|
||||
- `run_shell_command` — ONLY for read-only commands (ls, git log, git status, etc.)
|
||||
- `ask_user_question` — Ask the user for clarification
|
||||
|
||||
@@ -151,10 +153,10 @@ You MUST NOT use `apply_file_diffs`, `create_documents`, `edit_documents`, or an
|
||||
|
||||
const REVIEW_TOOL_GUIDELINES: &str = r#"## Tool Usage
|
||||
You have access to tools for examining the code under review:
|
||||
- `read_files` — Read file contents to understand context
|
||||
- `grep` — Search for patterns to find related code
|
||||
- `file_glob` — Find related files
|
||||
- `search_codebase` — Semantic search for related implementations
|
||||
- `search_codebase` — Primary tool for discovering relevant source code and understanding implementations semantically
|
||||
- `file_glob` — Find related filenames
|
||||
- `read_files` — Read focused file contents after discovery
|
||||
- `grep` — Search exact known text in non-code files, or use only as a fallback when the semantic index is unavailable
|
||||
- `run_shell_command` — For read-only commands (git diff, git log, etc.)
|
||||
|
||||
Use these tools to gather context needed for a thorough review. You should read the files being changed and their surrounding context before providing feedback."#;
|
||||
|
||||
@@ -16,6 +16,7 @@ mod prompt_builder_tests {
|
||||
assert!(prompt.system_prompt.contains("Galaxy"));
|
||||
assert!(prompt.system_prompt.contains("coding mode"));
|
||||
assert!(prompt.system_prompt.contains("/home/user/project"));
|
||||
assert!(prompt.system_prompt.contains("search_codebase` first"));
|
||||
assert!(!prompt.tools.is_empty());
|
||||
// Code mode should have apply_file_diffs
|
||||
assert!(prompt.tools.iter().any(|t| t.name == "apply_file_diffs"));
|
||||
@@ -31,6 +32,33 @@ mod prompt_builder_tests {
|
||||
assert!(!prompt.tools.iter().any(|t| t.name == "apply_file_diffs"));
|
||||
// But should have read_files
|
||||
assert!(prompt.tools.iter().any(|t| t.name == "read_files"));
|
||||
assert!(prompt.system_prompt.contains("search_codebase` first"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_semantic_search_is_advertised_before_regex_search() {
|
||||
let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic).build();
|
||||
let search_index = prompt
|
||||
.tools
|
||||
.iter()
|
||||
.position(|tool| tool.name == "search_codebase")
|
||||
.expect("search_codebase tool");
|
||||
let grep_index = prompt
|
||||
.tools
|
||||
.iter()
|
||||
.position(|tool| tool.name == "grep")
|
||||
.expect("grep tool");
|
||||
assert!(search_index < grep_index);
|
||||
assert!(prompt
|
||||
.tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "search_codebase")
|
||||
.is_some_and(|tool| tool.description.contains("all source-code discovery")));
|
||||
assert!(prompt
|
||||
.tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "grep")
|
||||
.is_some_and(|tool| tool.description.contains("non-code files")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -23,9 +23,9 @@ fn code_tools() -> Vec<ToolDefinition> {
|
||||
run_shell_command(),
|
||||
read_files(),
|
||||
apply_file_diffs(),
|
||||
grep(),
|
||||
file_glob(),
|
||||
search_codebase(),
|
||||
file_glob(),
|
||||
grep(),
|
||||
write_to_long_running_shell_command(),
|
||||
interrupt_shell_command(),
|
||||
read_shell_command_output(),
|
||||
@@ -48,9 +48,9 @@ fn code_tools() -> Vec<ToolDefinition> {
|
||||
fn plan_tools() -> Vec<ToolDefinition> {
|
||||
vec![
|
||||
read_files(),
|
||||
grep(),
|
||||
file_glob(),
|
||||
search_codebase(),
|
||||
file_glob(),
|
||||
grep(),
|
||||
run_shell_command_readonly(),
|
||||
ask_user_question(),
|
||||
start_agent(),
|
||||
@@ -64,9 +64,9 @@ fn plan_tools() -> Vec<ToolDefinition> {
|
||||
fn review_tools() -> Vec<ToolDefinition> {
|
||||
vec![
|
||||
read_files(),
|
||||
grep(),
|
||||
file_glob(),
|
||||
search_codebase(),
|
||||
file_glob(),
|
||||
grep(),
|
||||
run_shell_command_readonly(),
|
||||
]
|
||||
}
|
||||
@@ -136,7 +136,7 @@ fn apply_file_diffs() -> ToolDefinition {
|
||||
fn grep() -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "grep".to_string(),
|
||||
description: "Search for up to 3 focused regex patterns per call. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Use read_files afterward to see context around matches.".to_string(),
|
||||
description: "Search for up to 3 focused regex patterns per call. Use only for exact known text in non-code files; never use this to discover or search source code when search_codebase is available. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Use read_files afterward to see context around matches.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -151,7 +151,7 @@ fn grep() -> ToolDefinition {
|
||||
fn file_glob() -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "file_glob".to_string(),
|
||||
description: "Find files matching up to 3 focused glob patterns per call. Uses git ls-files in git repos. Returns absolute file paths of matches. Make another call for more patterns.".to_string(),
|
||||
description: "Find filenames matching up to 3 focused glob patterns per call. Use this to locate files, not to search their contents. Uses git ls-files in git repos. Returns absolute file paths of matches. Make another call for more patterns.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -166,7 +166,7 @@ fn file_glob() -> ToolDefinition {
|
||||
fn search_codebase() -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "search_codebase".to_string(),
|
||||
description: "Semantic code search across the indexed codebase. Use for finding relevant code by meaning rather than exact text match. Better than grep for conceptual queries like 'authentication logic' or 'error handling for database connections'.".to_string(),
|
||||
description: "Primary tool for discovering relevant source code in the local project index. Use this first for all source-code discovery and conceptual, semantic, or structural searches before reading files. Never use grep to search source code when this tool is available.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -917,13 +917,19 @@ fn build_system_prompt(
|
||||
.join(", "),
|
||||
);
|
||||
prompt.push_str(".\nNever invent tool names or parameters. Check command exit codes and tool error results before claiming success.\n");
|
||||
let has_search_codebase = tools.iter().any(|tool| tool.name == "search_codebase");
|
||||
if has_search_codebase {
|
||||
prompt.push_str(
|
||||
"For source-code discovery, semantic questions, or finding an unfamiliar implementation, MUST use `search_codebase` first. Never use `grep` to discover or search source code when `search_codebase` is available; reserve `grep` for exact known text in non-code files. Use `file_glob` only to locate filenames and `read_files` for focused follow-up context.\n",
|
||||
);
|
||||
}
|
||||
if tools.iter().any(|tool| tool.name == "run_shell_command") {
|
||||
let has_file_tools = tools
|
||||
.iter()
|
||||
.any(|tool| matches!(tool.name.as_str(), "file_glob" | "grep" | "read_files"));
|
||||
if has_file_tools {
|
||||
prompt.push_str(
|
||||
"Prefer `file_glob`, `grep`, and `read_files` for file discovery, content search, and file reading when they are available. Reserve `run_shell_command` for operations those specialized tools cannot perform; do not use shell `find`, `grep`, `rg`, `cat`, `head`, or `tail` as substitutes.\n",
|
||||
"Prefer `file_glob` for filenames and `read_files` for focused content when they are available. Reserve `run_shell_command` for operations specialized tools cannot perform; do not use shell `find`, `grep`, `rg`, `cat`, `head`, or `tail` as substitutes.\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,15 +154,18 @@ fn builds_a_rig_turn_directly_from_galaxy_request_state() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_prompt_prefers_specialized_file_tools_over_shell_substitutes() {
|
||||
fn system_prompt_prioritizes_semantic_code_search_over_regex_search() {
|
||||
let mut params = RequestParams::new_for_test();
|
||||
params.input = vec![user_query("Find every Rust file containing ProviderRun")];
|
||||
params.input = vec![user_query(
|
||||
"Find the implementation that handles ProviderRun",
|
||||
)];
|
||||
|
||||
let prepared = prepare_rig_turn(
|
||||
&config(),
|
||||
params,
|
||||
vec![
|
||||
ToolType::RunShellCommand,
|
||||
ToolType::SearchCodebase,
|
||||
ToolType::FileGlob,
|
||||
ToolType::Grep,
|
||||
ToolType::ReadFiles,
|
||||
@@ -171,7 +174,11 @@ fn system_prompt_prefers_specialized_file_tools_over_shell_substitutes() {
|
||||
);
|
||||
let prompt = prepared.request.system_prompt.expect("system prompt");
|
||||
|
||||
assert!(prompt.contains("Prefer `file_glob`, `grep`, and `read_files`"));
|
||||
assert!(prompt.contains("MUST use `search_codebase` first"));
|
||||
assert!(prompt.contains("Never use `grep` to discover or search source code"));
|
||||
assert!(
|
||||
prompt.contains("Prefer `file_glob` for filenames and `read_files` for focused content")
|
||||
);
|
||||
assert!(prompt.contains("do not use shell `find`, `grep`, `rg`, `cat`, `head`, or `tail`"));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalIndexStatus;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use galaxyui::integration::{AssertionOutcome, StepData, TestStep};
|
||||
use galaxyui::{async_assert, App, ReadModel, SingletonEntity, UpdateModel, WindowId};
|
||||
use settings::Setting;
|
||||
@@ -51,6 +56,11 @@ pub fn sync_current_codebase_index() -> TestStep {
|
||||
};
|
||||
|
||||
// Kick off codebase indexing at the current directory.
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
app.update_model(&LocalProjectIndexManager::handle(app), |manager, ctx| {
|
||||
let _ = manager.index_directory(canonicalized_path.clone(), ctx);
|
||||
});
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
app.update_model(&CodebaseIndexManager::handle(app), |manager, ctx| {
|
||||
manager.index_directory(canonicalized_path.clone(), ctx);
|
||||
});
|
||||
@@ -67,10 +77,29 @@ pub fn sync_current_codebase_index() -> TestStep {
|
||||
.get::<String, PathBuf>(CWD_DATA_KEY.into())
|
||||
.expect("No cwd");
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let status =
|
||||
app.read_model(&LocalProjectIndexManager::handle(app), |manager, _ctx| {
|
||||
manager
|
||||
.status_for_path(cwd)
|
||||
.map(|(_, status)| status.clone())
|
||||
});
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let status = app.read_model(&CodebaseIndexManager::handle(app), |manager, ctx| {
|
||||
manager.get_codebase_index_status_for_path(cwd, ctx)
|
||||
});
|
||||
match status {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
Some(LocalIndexStatus::Ready { .. }) => AssertionOutcome::Success,
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
Some(status) => {
|
||||
async_assert!(
|
||||
matches!(status, LocalIndexStatus::Ready { .. }),
|
||||
"Codebase index for {} should be ready",
|
||||
cwd.display()
|
||||
)
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
Some(status) => {
|
||||
async_assert!(
|
||||
status.has_synced_version()
|
||||
|
||||
+19
-3
@@ -1155,10 +1155,15 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
||||
.spawn(galaxy_logging::rotate_log_files())
|
||||
.detach();
|
||||
|
||||
let local_project_indexing_enabled = matches!(
|
||||
launch_mode,
|
||||
LaunchMode::App { .. } | LaunchMode::CommandLine { .. } | LaunchMode::Test { .. }
|
||||
);
|
||||
ctx.add_singleton_model(|ctx| {
|
||||
AppExecutionMode::new(
|
||||
AppExecutionMode::new_with_local_project_indexing(
|
||||
launch_mode.execution_mode(),
|
||||
launch_mode.is_sandboxed(),
|
||||
local_project_indexing_enabled,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
@@ -1988,6 +1993,14 @@ pub(crate) fn initialize_app(
|
||||
} else {
|
||||
ctx.add_singleton_model(|ctx| RepoOutlines::new_with_indexing_enabled(false, ctx));
|
||||
}
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if matches!(
|
||||
launch_mode,
|
||||
LaunchMode::App { .. } | LaunchMode::CommandLine { .. } | LaunchMode::Test { .. }
|
||||
) {
|
||||
ctx.add_singleton_model(::ai::index::local_project_index::LocalProjectIndexManager::new);
|
||||
}
|
||||
|
||||
ctx.add_singleton_model(|ctx| {
|
||||
galaxy_core::sync_queue::SyncQueue::<SyncTask>::new_with_rate_limit(
|
||||
&ctx.background_executor(),
|
||||
@@ -2215,7 +2228,10 @@ pub(crate) fn initialize_app(
|
||||
ctx.add_singleton_model(DefaultTerminal::new);
|
||||
|
||||
ctx.add_singleton_model(|ctx| {
|
||||
let should_restore_indices = launch_mode.supports_indexing()
|
||||
let legacy_indexing_enabled = matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. })
|
||||
|| !cfg!(all(feature = "local_fs", not(target_family = "wasm")));
|
||||
let should_restore_indices = legacy_indexing_enabled
|
||||
&& launch_mode.supports_indexing()
|
||||
&& (matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. })
|
||||
|| UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx));
|
||||
let indices_to_restore = if should_restore_indices {
|
||||
@@ -2231,7 +2247,7 @@ pub(crate) fn initialize_app(
|
||||
codebase_limits.max_files_per_repo,
|
||||
codebase_limits.embedding_generation_batch_size,
|
||||
server_api_provider.as_ref(ctx).get(),
|
||||
launch_mode.supports_indexing(),
|
||||
launch_mode.supports_indexing() && legacy_indexing_enabled,
|
||||
);
|
||||
if matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. }) {
|
||||
codebase_index_config = codebase_index_config.defer_persisted_index_restore();
|
||||
|
||||
@@ -196,6 +196,8 @@ fn initialize_app(app: &mut App) {
|
||||
app.add_singleton_model(voice_input::VoiceInput::new);
|
||||
#[cfg(feature = "local_fs")]
|
||||
app.add_singleton_model(RepoMetadataModel::new);
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
app.add_singleton_model(ai::index::local_project_index::LocalProjectIndexManager::new);
|
||||
app.add_singleton_model(SkillManager::new);
|
||||
app.add_singleton_model(FileSearchModel::new);
|
||||
app.add_singleton_model(|_| crate::code_review::git_repo_model::GitRepoModels::new());
|
||||
|
||||
@@ -8,6 +8,8 @@ use cfg_if::cfg_if;
|
||||
use galaxy_core::context_flag::ContextFlag;
|
||||
use galaxy_core::safe_error;
|
||||
use galaxy_core::user_preferences::GetUserPreferences as _;
|
||||
#[cfg(feature = "voice_input")]
|
||||
use galaxyui_core::event::KeyState;
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use onboarding::{
|
||||
|
||||
@@ -209,9 +209,9 @@ pub const OPEN_CODE_REVIEW: StaticCommand = StaticCommand {
|
||||
|
||||
pub const INDEX: StaticCommand = StaticCommand {
|
||||
name: "/index",
|
||||
description: "Index this codebase",
|
||||
description: "Build or refresh the local project index",
|
||||
icon_path: "bundled/svg/find-all.svg",
|
||||
availability: Availability::REPOSITORY
|
||||
availability: Availability::LOCAL
|
||||
.union(Availability::CODEBASE_CONTEXT)
|
||||
.union(Availability::AI_ENABLED),
|
||||
auto_enter_ai_mode: false,
|
||||
@@ -220,11 +220,9 @@ pub const INDEX: StaticCommand = StaticCommand {
|
||||
|
||||
pub const INIT: StaticCommand = StaticCommand {
|
||||
name: "/init",
|
||||
description: "Index this codebase and generate an AGENTS.md file",
|
||||
description: "Build a local project index and generate an AGENTS.md file",
|
||||
icon_path: "bundled/svg/warp-2.svg",
|
||||
availability: Availability::REPOSITORY
|
||||
.union(Availability::AGENT_VIEW)
|
||||
.union(Availability::AI_ENABLED),
|
||||
availability: Availability::LOCAL.union(Availability::AI_ENABLED),
|
||||
auto_enter_ai_mode: true,
|
||||
argument: None,
|
||||
};
|
||||
|
||||
@@ -5,13 +5,9 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(
|
||||
feature = "local_fs",
|
||||
not(target_family = "wasm"),
|
||||
not(any(test, feature = "integration_tests"))
|
||||
))]
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManagerEvent;
|
||||
use ai::index::local_project_index::LocalProjectIndexEvent;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::{LocalIndexStatus, LocalProjectIndexManager};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::paths::home_relative_path;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
@@ -129,23 +125,17 @@ impl AgentAssistedEnvironmentModal {
|
||||
create_button,
|
||||
};
|
||||
|
||||
#[cfg(all(
|
||||
feature = "local_fs",
|
||||
not(target_family = "wasm"),
|
||||
not(any(test, feature = "integration_tests"))
|
||||
))]
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
let index_manager = CodebaseIndexManager::handle(ctx);
|
||||
let index_manager = LocalProjectIndexManager::handle(ctx);
|
||||
ctx.subscribe_to_model(&index_manager, |me, _, event, ctx| {
|
||||
if !me.visible {
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
|
||||
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
|
||||
| CodebaseIndexManagerEvent::RemoveExpiredIndexMetadata { .. }
|
||||
| CodebaseIndexManagerEvent::IndexMetadataUpdated { .. } => {
|
||||
LocalProjectIndexEvent::StatusChanged { .. }
|
||||
| LocalProjectIndexEvent::IndexRemoved { .. } => {
|
||||
me.refresh_available_repos(ctx);
|
||||
if me.available_repos.is_empty() {
|
||||
me.maybe_start_available_repos_loading(ctx);
|
||||
@@ -734,10 +724,10 @@ impl View for AgentAssistedEnvironmentModal {
|
||||
fn available_indexed_repos(app: &AppContext) -> Vec<RepoEntry> {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
let mut repos: Vec<RepoEntry> = CodebaseIndexManager::as_ref(app)
|
||||
.get_codebase_index_statuses(app)
|
||||
.filter_map(|(root, status)| {
|
||||
status.has_synced_version().then(|| {
|
||||
let mut repos: Vec<RepoEntry> = LocalProjectIndexManager::as_ref(app)
|
||||
.statuses()
|
||||
.filter(|(_, status)| matches!(status, LocalIndexStatus::Ready { .. }))
|
||||
.map(|(root, _)| {
|
||||
let name = root
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
@@ -748,7 +738,6 @@ fn available_indexed_repos(app: &AppContext) -> Vec<RepoEntry> {
|
||||
path: root.clone(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
repos.sort_by_key(|a| a.name.to_lowercase());
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxyui::elements::{ChildView, Empty};
|
||||
use galaxyui::platform::WindowStyle;
|
||||
@@ -17,8 +20,11 @@ fn init_modal_test_models(app: &mut App) {
|
||||
app.add_singleton_model(|_| Appearance::mock());
|
||||
app.add_singleton_model(|_| ToastStack);
|
||||
|
||||
// The modal queries CodebaseIndexManager for locally indexed repos.
|
||||
// Register a test instance so `available_indexed_repos(...)` doesn't panic.
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
app.add_singleton_model(|ctx| {
|
||||
LocalProjectIndexManager::new_at(tempfile::tempdir().unwrap().keep(), ctx)
|
||||
});
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
app.add_singleton_model(|ctx| {
|
||||
CodebaseIndexManager::new_for_test(ServerApiProvider::as_ref(ctx).get(), ctx)
|
||||
});
|
||||
|
||||
@@ -2,11 +2,17 @@ use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexFinishedStatus, CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
CodebaseIndexStatus, CodebaseIndexingError,
|
||||
};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::SyncProgress;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::{
|
||||
LocalIndexStatus, LocalProjectIndexEvent, LocalProjectIndexManager,
|
||||
};
|
||||
use ai::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
|
||||
use ai::workspace::WorkspaceMetadata;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
@@ -170,6 +176,44 @@ enum IndexingRefreshAction {
|
||||
RequestRemote,
|
||||
Resync,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
fn local_status_presentation(
|
||||
status: Option<&LocalIndexStatus>,
|
||||
appearance: &Appearance,
|
||||
) -> IndexingStatusPresentation {
|
||||
let theme = appearance.theme();
|
||||
match status {
|
||||
None => IndexingStatusPresentation {
|
||||
text: Cow::from("No index created"),
|
||||
color: theme.disabled_ui_text_color().into_solid(),
|
||||
icon: Some(Icon::SlashCircle),
|
||||
refresh_action: None,
|
||||
show_delete: false,
|
||||
},
|
||||
Some(LocalIndexStatus::Indexing) => IndexingStatusPresentation {
|
||||
text: Cow::from("Indexing..."),
|
||||
color: theme.disabled_ui_text_color().into_solid(),
|
||||
icon: None,
|
||||
refresh_action: None,
|
||||
show_delete: true,
|
||||
},
|
||||
Some(LocalIndexStatus::Ready { file_count }) => IndexingStatusPresentation {
|
||||
text: Cow::from(format!("Ready ({file_count} files)")),
|
||||
color: theme.ansi_fg_green(),
|
||||
icon: Some(Icon::Check),
|
||||
refresh_action: Some(IndexingRefreshAction::Resync),
|
||||
show_delete: true,
|
||||
},
|
||||
Some(LocalIndexStatus::Failed { .. }) => IndexingStatusPresentation {
|
||||
text: Cow::from("Failed"),
|
||||
color: theme.ui_error_color(),
|
||||
icon: Some(Icon::AlertTriangle),
|
||||
refresh_action: Some(IndexingRefreshAction::Resync),
|
||||
show_delete: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
pub struct CodeSettingsPageView {
|
||||
page: PageType<Self>,
|
||||
active_subpage: Option<CodeSubpage>,
|
||||
@@ -196,34 +240,52 @@ pub struct CodeSettingsPageView {
|
||||
|
||||
impl CodeSettingsPageView {
|
||||
pub fn new(ctx: &mut ViewContext<CodeSettingsPageView>) -> Self {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let codebase_count = {
|
||||
let index_manager = LocalProjectIndexManager::handle(ctx);
|
||||
let count = index_manager.as_ref(ctx).statuses().count();
|
||||
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
LocalProjectIndexEvent::StatusChanged { .. }
|
||||
| LocalProjectIndexEvent::IndexRemoved { .. }
|
||||
) {
|
||||
let count = index.as_ref(ctx).statuses().count();
|
||||
me.codebase_manual_resync_mouse_states
|
||||
.resize_with(count, Default::default);
|
||||
me.codebase_delete_mouse_states
|
||||
.resize_with(count, Default::default);
|
||||
me.resize_workspace_mouse_states(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
});
|
||||
count
|
||||
};
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let codebase_count = {
|
||||
let index_manager = CodebaseIndexManager::handle(ctx);
|
||||
let codebase_count = index_manager
|
||||
let count = index_manager
|
||||
.as_ref(ctx)
|
||||
.get_codebase_index_statuses(ctx)
|
||||
.count();
|
||||
|
||||
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
|
||||
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
|
||||
) {
|
||||
let codebase_count = index.as_ref(ctx).get_codebase_index_statuses(ctx).count();
|
||||
|
||||
// Only update mouse states if the number of codebases changed
|
||||
if me.codebase_manual_resync_mouse_states.len() != codebase_count {
|
||||
// Resize the vector to match the new codebase count, but preserve the existing mouse states
|
||||
let count = index.as_ref(ctx).get_codebase_index_statuses(ctx).count();
|
||||
me.codebase_manual_resync_mouse_states
|
||||
.resize_with(codebase_count, Default::default);
|
||||
.resize_with(count, Default::default);
|
||||
me.codebase_delete_mouse_states
|
||||
.resize_with(codebase_count, Default::default);
|
||||
}
|
||||
|
||||
.resize_with(count, Default::default);
|
||||
me.resize_workspace_mouse_states(ctx);
|
||||
|
||||
ctx.notify();
|
||||
}
|
||||
});
|
||||
count
|
||||
};
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let remote_codebase_count = {
|
||||
@@ -552,6 +614,18 @@ impl CodeSettingsPageView {
|
||||
if let Some(directory_path) = paths.first() {
|
||||
let path = PathBuf::from(directory_path);
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
PersistedWorkspace::handle(ctx).update(ctx, |workspace, ctx| {
|
||||
workspace.user_added_workspace(path.clone(), ctx);
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(path.clone(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
});
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(path, ctx);
|
||||
});
|
||||
@@ -697,11 +771,25 @@ impl TypedActionView for CodeSettingsPageView {
|
||||
ctx.notify();
|
||||
}
|
||||
CodeSettingsPageAction::ManualResync(repo_path) => {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(repo_path.clone(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to refresh local project index: {error:#}");
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.try_manual_resync_codebase(repo_path, ctx);
|
||||
});
|
||||
}
|
||||
CodeSettingsPageAction::DeleteIndex(repo_path) => {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.remove_index_for_path(repo_path.clone(), ctx);
|
||||
});
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.drop_index(repo_path.clone(), ctx);
|
||||
});
|
||||
@@ -1112,6 +1200,7 @@ impl CodePageWidget {
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
if codebase_indexing_enabled && !CodebaseIndexManager::as_ref(app).can_create_new_indices()
|
||||
{
|
||||
rows.push(self.render_settings_subtext(
|
||||
@@ -1372,7 +1461,10 @@ impl CodePageWidget {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let codebase_manager = CodebaseIndexManager::as_ref(app);
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let local_index_manager = LocalProjectIndexManager::as_ref(app);
|
||||
let lsp_manager = LspManagerModel::as_ref(app);
|
||||
let persisted_workspace = PersistedWorkspace::as_ref(app);
|
||||
|
||||
@@ -1383,6 +1475,11 @@ impl CodePageWidget {
|
||||
let workspace_path = &workspace.path;
|
||||
|
||||
// Get codebase index status if it exists
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let local_status = local_index_manager
|
||||
.status_for_path(workspace_path)
|
||||
.map(|(_, status)| status);
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let index_status =
|
||||
codebase_manager.get_codebase_index_status_for_path(workspace_path, app);
|
||||
|
||||
@@ -1403,7 +1500,17 @@ impl CodePageWidget {
|
||||
.unwrap_or_default();
|
||||
|
||||
// Skip workspaces that have neither an index nor any LSP servers
|
||||
if index_status.is_none() && all_servers.is_empty() {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let (has_index, index_presentation) = (
|
||||
local_status.is_some(),
|
||||
local_status_presentation(local_status, appearance),
|
||||
);
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let (has_index, index_presentation) = (
|
||||
index_status.is_some(),
|
||||
self.local_indexing_status_presentation(index_status.as_ref(), appearance),
|
||||
);
|
||||
if !has_index && all_servers.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1431,7 +1538,7 @@ impl CodePageWidget {
|
||||
|
||||
content.add_child(self.render_workspace_row(
|
||||
workspace_path,
|
||||
index_status.as_ref(),
|
||||
index_presentation,
|
||||
&all_servers,
|
||||
lsp_manager,
|
||||
resync_mouse,
|
||||
@@ -1485,7 +1592,7 @@ impl CodePageWidget {
|
||||
fn render_workspace_row(
|
||||
&self,
|
||||
workspace_path: &Path,
|
||||
index_status: Option<&CodebaseIndexStatus>,
|
||||
index_presentation: IndexingStatusPresentation,
|
||||
all_servers: &[(LSPServerType, EnablementState)],
|
||||
lsp_manager: &LspManagerModel,
|
||||
resync_mouse: MouseStateHandle,
|
||||
@@ -1568,7 +1675,7 @@ impl CodePageWidget {
|
||||
// Indexing section (always rendered per design)
|
||||
workspace_content.add_child(self.render_indexing_subsection(
|
||||
workspace_path,
|
||||
index_status,
|
||||
index_presentation,
|
||||
resync_mouse,
|
||||
delete_mouse,
|
||||
appearance,
|
||||
@@ -1671,13 +1778,13 @@ impl CodePageWidget {
|
||||
fn render_indexing_subsection(
|
||||
&self,
|
||||
workspace_path: &Path,
|
||||
index_status: Option<&CodebaseIndexStatus>,
|
||||
presentation: IndexingStatusPresentation,
|
||||
resync_mouse: MouseStateHandle,
|
||||
delete_mouse: MouseStateHandle,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
self.render_indexing_subsection_for_target(
|
||||
self.local_indexing_status_presentation(index_status, appearance),
|
||||
presentation,
|
||||
Some(LocalOrRemotePath::Local(workspace_path.to_path_buf())),
|
||||
resync_mouse,
|
||||
delete_mouse,
|
||||
@@ -1730,6 +1837,7 @@ impl CodePageWidget {
|
||||
column.finish()
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn local_indexing_status_presentation(
|
||||
&self,
|
||||
index_state: Option<&CodebaseIndexStatus>,
|
||||
@@ -2569,6 +2677,7 @@ impl SettingsWidget for CodebaseIndexingCategorizedWidget {
|
||||
Some(AUTO_INDEX_DESCRIPTION.into()),
|
||||
));
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
if !CodebaseIndexManager::as_ref(app).can_create_new_indices() {
|
||||
content.add_child(
|
||||
ui_builder
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::{LocalProjectIndexEvent, LocalProjectIndexManager};
|
||||
use galaxy_util::path::user_friendly_path;
|
||||
use galaxyui::elements::{
|
||||
Border, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, Hoverable,
|
||||
@@ -76,6 +79,19 @@ pub(super) enum DirectoryColorAddPickerEvent {
|
||||
|
||||
impl DirectoryColorAddPicker {
|
||||
pub(super) fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let local_index_manager = LocalProjectIndexManager::handle(ctx);
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.subscribe_to_model(&local_index_manager, |me, _, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
LocalProjectIndexEvent::StatusChanged { .. }
|
||||
| LocalProjectIndexEvent::IndexRemoved { .. }
|
||||
) {
|
||||
me.refresh_items(ctx);
|
||||
}
|
||||
});
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
|
||||
// Refresh for any event that may change the set of indexed codebase paths or
|
||||
// persisted workspaces: new index created, sync state updated (which covers
|
||||
@@ -190,6 +206,12 @@ impl DirectoryColorAddPicker {
|
||||
}
|
||||
|
||||
fn refresh_items(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let indexed_paths: HashSet<PathBuf> = LocalProjectIndexManager::as_ref(ctx)
|
||||
.statuses()
|
||||
.map(|(path, _)| path.clone())
|
||||
.collect();
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let indexed_paths: HashSet<PathBuf> = CodebaseIndexManager::as_ref(ctx)
|
||||
.get_codebase_paths()
|
||||
.cloned()
|
||||
|
||||
@@ -276,6 +276,8 @@ pub fn initialize_app(app: &mut App) {
|
||||
app.add_singleton_model(RepoMetadataModel::new);
|
||||
app.add_singleton_model(FileSearchModel::new);
|
||||
app.add_singleton_model(RepoOutlines::new_for_test);
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
app.add_singleton_model(ai::index::local_project_index::LocalProjectIndexManager::new);
|
||||
#[cfg(feature = "voice_input")]
|
||||
app.add_singleton_model(voice_input::VoiceInput::new);
|
||||
app.add_singleton_model(|ctx| {
|
||||
|
||||
+75
-20
@@ -71,6 +71,8 @@ use action::RememberForWormholing;
|
||||
pub use action::{AgentOnboardingVersion, OnboardingIntention, OnboardingVersion, TerminalAction};
|
||||
use ai::api_keys::{ApiKeyManager, AwsCredentialsState};
|
||||
use ai::index::full_source_code_embedding::manager::{BuildSource, CodebaseIndexManager};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use async_channel::{Receiver, Sender};
|
||||
use base64::Engine as _;
|
||||
use block_banner::{render_wormholing_banner, WormholeBannerState};
|
||||
@@ -7362,6 +7364,15 @@ impl TerminalView {
|
||||
BlocklistAIActionEvent::InitProject(_) => {
|
||||
self.on_next_conversation_finished(|me, _reason, ctx| {
|
||||
if let Some(path) = me.pwd() {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(PathBuf::from(path), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(PathBuf::from(path), ctx);
|
||||
});
|
||||
@@ -10619,7 +10630,16 @@ impl TerminalView {
|
||||
});
|
||||
}
|
||||
|
||||
// Index the codebase
|
||||
// Index the project.
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(banner_state.repo_path.clone(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(banner_state.repo_path.clone(), ctx);
|
||||
});
|
||||
@@ -13835,6 +13855,13 @@ impl TerminalView {
|
||||
self.update_focused_terminal_info(ctx);
|
||||
|
||||
if let Some(working_directory) = self.active_session_path_if_local(ctx) {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(working_directory.clone(), ctx)
|
||||
}) {
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, _ctx| {
|
||||
manager.handle_session_bootstrapped(&working_directory);
|
||||
});
|
||||
@@ -14200,10 +14227,7 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(pwd_path) = self
|
||||
.pwd()
|
||||
.and_then(|pwd| Path::new(&pwd).canonicalize().ok())
|
||||
else {
|
||||
let Some(pwd_path) = self.active_session_path_if_local(ctx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -14246,6 +14270,18 @@ impl TerminalView {
|
||||
me.redetermine_terminal_focus(ctx);
|
||||
}
|
||||
InitProjectModelEvent::StepCompleted(_) => {}
|
||||
InitProjectModelEvent::CodebaseIndexFailed(message) => {
|
||||
let window_id = ctx.window_id();
|
||||
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
toast_stack.add_ephemeral_toast(
|
||||
DismissibleToast::error(format!(
|
||||
"Local project indexing failed: {message}"
|
||||
)),
|
||||
window_id,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
}
|
||||
InitProjectModelEvent::Cancelled => {
|
||||
me.active_init_project_model = None;
|
||||
// Mark conversation as cancelled
|
||||
@@ -26002,6 +26038,13 @@ impl TerminalView {
|
||||
return;
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(active_session_path.clone(), ctx)
|
||||
}) {
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.build_and_sync_codebase_index(
|
||||
BuildSource::FromPath(active_session_path.as_path()),
|
||||
@@ -26011,7 +26054,7 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
fn write_codebase_index(&self, _ctx: &mut ViewContext<Self>) {
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
let Some(working_directory_str) = self.pwd() else {
|
||||
log::error!("No working directory found for terminal session");
|
||||
@@ -27431,26 +27474,38 @@ impl TypedActionView for TerminalView {
|
||||
}
|
||||
SummarizeConversation => self.summarize_conversation(ctx),
|
||||
IndexProjectSpeedbump => {
|
||||
let codebase_context_enabled =
|
||||
UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx);
|
||||
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() && codebase_context_enabled {
|
||||
#[cfg(feature = "local_fs")]
|
||||
if let Some(current_dir) = self.pwd() {
|
||||
let directory = PathBuf::from(¤t_dir);
|
||||
|
||||
if let Ok(repo_path) = directory.canonicalize() {
|
||||
// Start indexing the codebase
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(repo_path.clone(), ctx);
|
||||
});
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Some(repo_path) = self
|
||||
.active_session_path_if_local(ctx)
|
||||
.and_then(|path| dunce::canonicalize(path).ok())
|
||||
{
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(repo_path.clone(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
self.remove_codebase_index_speedbump_banner(ctx);
|
||||
self.insert_codebase_index_speedbump_banner(
|
||||
repo_path, true, /* show_is_indexing */
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
let codebase_context_enabled =
|
||||
UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx);
|
||||
if codebase_context_enabled {
|
||||
if let Some(current_dir) = self.pwd() {
|
||||
let directory = PathBuf::from(¤t_dir);
|
||||
if let Ok(repo_path) = directory.canonicalize() {
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(repo_path.clone(), ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ pub mod model;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use lsp::supported_servers::LSPServerType;
|
||||
use lsp_server_selector::{create_lsp_server_selector, LSPServerInfo};
|
||||
@@ -40,7 +43,7 @@ use crate::view_components::DismissibleToast;
|
||||
use crate::workspace::ToastStack;
|
||||
use crate::{send_telemetry_from_ctx, TelemetryEvent};
|
||||
|
||||
const ONBOARDING_TEXT: &str = "Great - let's begin setting up this project! Would you like to give me permission to index this codebase? It allows me to quickly understand context and provide more targeted solutions when working in this codebase. No code is stored on Galaxy servers.";
|
||||
const ONBOARDING_TEXT: &str = "Great - let's begin setting up this project! Would you like to let Galaxy build a local project index? It allows me to quickly understand context and provide more targeted solutions while keeping the index on your device.";
|
||||
const ALREADY_SETUP_TEXT: &str = "It looks like this project has already been initialized. You can re-generate the GALAXY.md for this codebase by clicking the button below.";
|
||||
// Native Galaxy rules file format.
|
||||
pub const FILES_TO_CHECK: [&str; 4] = ["GALAXY.md", "AGENTS.md", "WARP.md", "CLAUDE.md"];
|
||||
@@ -644,10 +647,11 @@ impl InitStepBlock {
|
||||
.render(app)
|
||||
.finish()
|
||||
}
|
||||
InitStepStatus::Running => {
|
||||
// Codebase context doesn't have a "running" state
|
||||
Empty::new().finish()
|
||||
}
|
||||
InitStepStatus::Running => RenderableAction::new("Building local project index", app)
|
||||
.with_icon(in_progress_icon(Appearance::as_ref(app)).finish())
|
||||
.with_content_item_spacing()
|
||||
.render(app)
|
||||
.finish(),
|
||||
InitStepStatus::Completed(result) => {
|
||||
self.render_completed_codebase_context(result, app)
|
||||
}
|
||||
@@ -1053,15 +1057,30 @@ impl TypedActionView for InitStepBlock {
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
InitProjectBlockAction::IndexCodebase(directory) => {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(directory.clone(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
return;
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(directory.clone(), ctx);
|
||||
});
|
||||
send_telemetry_from_ctx!(
|
||||
TelemetryEvent::AgentModeSetupCodebaseContextAction {
|
||||
action: AgentModeSetupCodebaseContextActionType::IndexCodebase,
|
||||
},
|
||||
ctx
|
||||
);
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(directory.clone(), ctx);
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
self.model.update(ctx, |model, ctx| {
|
||||
model.mark_step_running(InitStepKind::CodebaseContext, ctx);
|
||||
});
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
self.model.update(ctx, |model, ctx| {
|
||||
model.mark_step_completed(
|
||||
InitStepKind::CodebaseContext,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::{
|
||||
LocalIndexStatus, LocalProjectIndexEvent, LocalProjectIndexManager,
|
||||
};
|
||||
use ai::project_context::model::ProjectContextModel;
|
||||
use enum_iterator::Sequence;
|
||||
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
|
||||
@@ -10,7 +15,6 @@ use lsp::supported_servers::LSPServerType;
|
||||
use repo_metadata::repositories::DetectedRepositories;
|
||||
|
||||
use crate::ai::persisted_workspace::PersistedWorkspace;
|
||||
use crate::settings::CodeSettings;
|
||||
use crate::terminal::view::init_project::lsp_server_selector::LSPServerInfo;
|
||||
use crate::terminal::view::init_project::{
|
||||
CodebaseIndexingResult, CreateEnvironmentResult, InitActionResult, LanguageServersResult,
|
||||
@@ -124,7 +128,11 @@ impl InitProjectModel {
|
||||
path_env_var: Option<String>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let is_already_setup = !Self::should_have_available_steps(&pwd_path, ctx);
|
||||
#[cfg(feature = "local_fs")]
|
||||
let root_path = dunce::canonicalize(&pwd_path).unwrap_or(pwd_path);
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
let root_path = pwd_path;
|
||||
let is_already_setup = !Self::should_have_available_steps(&root_path, ctx);
|
||||
|
||||
Self {
|
||||
steps: [None, None, None, None, None],
|
||||
@@ -132,7 +140,7 @@ impl InitProjectModel {
|
||||
is_cancelled: false,
|
||||
is_already_setup,
|
||||
#[cfg(feature = "local_fs")]
|
||||
root_path: pwd_path,
|
||||
root_path,
|
||||
path_env_var,
|
||||
}
|
||||
}
|
||||
@@ -154,6 +162,17 @@ impl InitProjectModel {
|
||||
);
|
||||
|
||||
// Start async computations for subsequent steps
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
let local_index_manager = LocalProjectIndexManager::handle(ctx);
|
||||
ctx.subscribe_to_model(&local_index_manager, |model, _, event, ctx| {
|
||||
if let LocalProjectIndexEvent::StatusChanged { root_path, status } = event {
|
||||
if root_path == &model.root_path {
|
||||
model.handle_local_index_status(status, ctx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
self.compute_codebase_context_step(&pwd_path, ctx);
|
||||
if self.path_env_var.is_some() {
|
||||
self.compute_language_servers_step(&pwd_path, ctx);
|
||||
@@ -176,14 +195,12 @@ impl InitProjectModel {
|
||||
|
||||
/// Check if there are any steps that need user action
|
||||
pub fn should_have_available_steps(path: &Path, ctx: &galaxyui::AppContext) -> bool {
|
||||
// Note that we consider auto-indexing setting to true to satisfy the codebase context step.
|
||||
// This avoids the potential race condition with the banner showing just when we start auto-indexing.
|
||||
// /init is an explicit user request, so its indexing step must remain available even
|
||||
// when background auto-indexing is enabled. The auto-index setting only controls whether
|
||||
// navigation/startup starts indexing without this explicit action.
|
||||
let has_pending_codebase_context = UserWorkspaces::as_ref(ctx)
|
||||
.is_codebase_context_enabled(ctx)
|
||||
&& CodebaseIndexManager::as_ref(ctx)
|
||||
.get_codebase_index_status_for_path(path, ctx)
|
||||
.is_none()
|
||||
&& !*CodeSettings::as_ref(ctx).auto_indexing_enabled;
|
||||
&& !Self::codebase_index_ready(path, ctx);
|
||||
|
||||
let has_pending_project_scoped_rules = ProjectContextModel::as_ref(ctx)
|
||||
.find_applicable_project_rules(&LocalOrRemotePath::Local(path.to_path_buf()))
|
||||
@@ -192,6 +209,18 @@ impl InitProjectModel {
|
||||
has_pending_codebase_context || has_pending_project_scoped_rules
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
fn codebase_index_ready(path: &Path, ctx: &galaxyui::AppContext) -> bool {
|
||||
LocalProjectIndexManager::as_ref(ctx).is_ready_for_path(path)
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn codebase_index_ready(path: &Path, ctx: &galaxyui::AppContext) -> bool {
|
||||
CodebaseIndexManager::as_ref(ctx)
|
||||
.get_codebase_index_status_for_path(path, ctx)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn get_step(&self, kind: InitStepKind) -> Option<&InitStep> {
|
||||
self.steps
|
||||
.get(kind as usize)
|
||||
@@ -368,9 +397,12 @@ impl InitProjectModel {
|
||||
return;
|
||||
}
|
||||
|
||||
let codebase_index_manager = CodebaseIndexManager::handle(ctx);
|
||||
let is_indexed = codebase_index_manager
|
||||
.as_ref(ctx)
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let is_indexed = LocalProjectIndexManager::as_ref(ctx)
|
||||
.status_for_path(pwd_path)
|
||||
.is_some_and(|(_, status)| matches!(status, LocalIndexStatus::Ready { .. }));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let is_indexed = CodebaseIndexManager::as_ref(ctx)
|
||||
.get_codebase_index_status_for_path(pwd_path, ctx)
|
||||
.is_some();
|
||||
|
||||
@@ -397,6 +429,47 @@ impl InitProjectModel {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
fn handle_local_index_status(
|
||||
&mut self,
|
||||
status: &LocalIndexStatus,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if !matches!(
|
||||
self.get_step(InitStepKind::CodebaseContext)
|
||||
.map(|step| &step.status),
|
||||
Some(InitStepStatus::Running)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let root_path = self.root_path.clone();
|
||||
let Some(step) = self.get_step_mut(InitStepKind::CodebaseContext) else {
|
||||
return;
|
||||
};
|
||||
match status {
|
||||
LocalIndexStatus::Indexing => {
|
||||
step.status = InitStepStatus::Running;
|
||||
ctx.notify();
|
||||
}
|
||||
LocalIndexStatus::Ready { .. } => {
|
||||
step.status = InitStepStatus::Completed(InitActionResult::CodebaseContext(
|
||||
CodebaseIndexingResult::Accepted,
|
||||
));
|
||||
ctx.emit(InitProjectModelEvent::StepCompleted(
|
||||
InitStepKind::CodebaseContext,
|
||||
));
|
||||
self.maybe_emit_next_step(ctx);
|
||||
}
|
||||
LocalIndexStatus::Failed { message } => {
|
||||
step.status = InitStepStatus::Ready(InitStepData::CodebaseContext {
|
||||
pwd_path: root_path,
|
||||
});
|
||||
ctx.notify();
|
||||
ctx.emit(InitProjectModelEvent::CodebaseIndexFailed(message.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_language_servers_step(&mut self, pwd_path: &Path, ctx: &mut ModelContext<Self>) {
|
||||
// Start as Pending
|
||||
self.set_step(
|
||||
@@ -584,6 +657,8 @@ pub enum InitProjectModelEvent {
|
||||
InitCompleted,
|
||||
/// Trigger AGENTS.md generation slash command
|
||||
GenerateProjectRules,
|
||||
/// The local index failed and can be retried from the same step.
|
||||
CodebaseIndexFailed(String),
|
||||
/// Trigger AGENTS.md regeneration
|
||||
RegenerateProjectRules,
|
||||
/// View codebase context status
|
||||
|
||||
@@ -156,6 +156,8 @@ pub fn initialize_app_for_terminal_view(app: &mut App) {
|
||||
model
|
||||
});
|
||||
app.add_singleton_model(FileSearchModel::new);
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
app.add_singleton_model(ai::index::local_project_index::LocalProjectIndexManager::new);
|
||||
app.add_singleton_model(|_| GitRepoModels::new());
|
||||
app.add_singleton_model(RepoOutlines::new_for_test);
|
||||
app.add_singleton_model(HomeDirectoryWatcher::new_for_test);
|
||||
|
||||
@@ -231,6 +231,8 @@ pub(crate) fn initialize_app(app: &mut App) {
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
app.add_singleton_model(RepoMetadataModel::new);
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
app.add_singleton_model(ai::index::local_project_index::LocalProjectIndexManager::new);
|
||||
app.add_singleton_model(search::files::model::FileSearchModel::new);
|
||||
|
||||
#[cfg(windows)]
|
||||
|
||||
@@ -60,6 +60,7 @@ priority-queue = "2.3.1"
|
||||
repo_metadata.workspace = true
|
||||
uuid.workspace = true
|
||||
unicode-width.workspace = true
|
||||
warp_search_core.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
@@ -0,0 +1,695 @@
|
||||
//! Persistent local-only structural and lexical project search.
|
||||
//!
|
||||
//! This index stores only bounded search metadata. Search callers read the current file contents
|
||||
//! after applying their normal permission checks; no source is sent to a server by this module.
|
||||
|
||||
#![cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use galaxy_core::paths::state_dir;
|
||||
use galaxy_util::standardized_path::StandardizedPath;
|
||||
use galaxyui_core::{Entity, ModelContext, SingletonEntity};
|
||||
use itertools::Itertools;
|
||||
use repo_metadata::{RepoMetadataEvent, RepositoryIdentifier};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use warp_search_core::define_search_schema;
|
||||
use warp_search_core::searcher::{SimpleFullTextSearcher, DEFAULT_MEMORY_BUDGET};
|
||||
|
||||
use crate::index::build_outline;
|
||||
|
||||
const INDEX_SCHEMA_VERSION: u32 = 1;
|
||||
const MAX_INDEX_FILES: usize = 5_000;
|
||||
const MAX_INDEXED_FILE_BYTES: usize = 3 * 1_000_000;
|
||||
const MAX_INDEXED_BODY_BYTES: usize = 256_000;
|
||||
const INDEX_DIRECTORY_NAME: &str = "local_project_indices";
|
||||
const CURRENT_FILE_NAME: &str = "CURRENT";
|
||||
const METADATA_FILE_NAME: &str = "metadata.json";
|
||||
|
||||
// Field weights intentionally prioritize symbols and paths over implementation text.
|
||||
define_search_schema!(
|
||||
schema_name: LOCAL_PROJECT_INDEX_SCHEMA,
|
||||
config_name: LocalProjectIndexSchema,
|
||||
search_doc: LocalProjectSearchDocument,
|
||||
identifying_doc: LocalProjectIndexId,
|
||||
search_fields: [symbol: 8.0, path: 5.0, metadata: 4.0, body: 1.0],
|
||||
id_fields: [file_path: String]
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LocalIndexStatus {
|
||||
Indexing,
|
||||
Ready { file_count: usize },
|
||||
Failed { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LocalSearchHit {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LocalProjectIndexEvent {
|
||||
StatusChanged {
|
||||
root_path: PathBuf,
|
||||
status: LocalIndexStatus,
|
||||
},
|
||||
IndexRemoved {
|
||||
root_path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct LocalIndexMetadata {
|
||||
root_path: String,
|
||||
schema_version: u32,
|
||||
generation: String,
|
||||
file_count: usize,
|
||||
}
|
||||
|
||||
struct LocalProjectIndex {
|
||||
root_path: PathBuf,
|
||||
searcher: SimpleFullTextSearcher<LocalProjectIndexSchema>,
|
||||
file_count: usize,
|
||||
}
|
||||
|
||||
impl LocalProjectIndex {
|
||||
fn search(
|
||||
&self,
|
||||
query: &str,
|
||||
partial_path_segments: Option<&[String]>,
|
||||
) -> Result<Vec<LocalSearchHit>> {
|
||||
let matches = self.searcher.search_id(query)?;
|
||||
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.filter_map(|matched| {
|
||||
let path = PathBuf::from(matched.values.file_path);
|
||||
let relative_path = path.strip_prefix(&self.root_path).ok()?;
|
||||
if partial_path_segments.is_some_and(|segments| {
|
||||
!segments.is_empty()
|
||||
&& !segments
|
||||
.iter()
|
||||
.any(|segment| relative_path.to_string_lossy().contains(segment))
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
Some(LocalSearchHit { path })
|
||||
})
|
||||
.unique_by(|hit| hit.path.clone())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocalProjectIndexManager {
|
||||
indices: HashMap<PathBuf, LocalProjectIndex>,
|
||||
statuses: HashMap<PathBuf, LocalIndexStatus>,
|
||||
pending_rebuilds: HashSet<PathBuf>,
|
||||
rebuild_epochs: HashMap<PathBuf, u64>,
|
||||
storage_root: PathBuf,
|
||||
}
|
||||
|
||||
impl Entity for LocalProjectIndexManager {
|
||||
type Event = LocalProjectIndexEvent;
|
||||
}
|
||||
|
||||
impl SingletonEntity for LocalProjectIndexManager {}
|
||||
|
||||
impl LocalProjectIndexManager {
|
||||
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||
let repo_metadata = repo_metadata::RepoMetadataModel::handle(ctx);
|
||||
let manager = Self::new_at(state_dir().join(INDEX_DIRECTORY_NAME), ctx);
|
||||
ctx.subscribe_to_model(&repo_metadata, |manager, _, event, ctx| {
|
||||
manager.handle_repo_metadata_event(event, ctx);
|
||||
});
|
||||
|
||||
// Re-register restored roots with repository metadata so filesystem updates continue to
|
||||
// refresh the local index after an app restart. RepositoryUpdated is intentionally ignored
|
||||
// below; the restored generation is already usable and only file-tree updates need a
|
||||
// rebuild.
|
||||
let restored_roots: Vec<PathBuf> = manager.statuses.keys().cloned().collect();
|
||||
for root_path in restored_roots {
|
||||
let Ok(standardized_root) = StandardizedPath::from_local_canonicalized(&root_path)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let _ = repo_metadata.update(ctx, |model, ctx| {
|
||||
model.index_local_directory_path(&standardized_root, ctx)
|
||||
});
|
||||
}
|
||||
manager
|
||||
}
|
||||
|
||||
/// Constructs a manager with an explicit storage root for hermetic tests.
|
||||
pub fn new_at(storage_root: PathBuf, _ctx: &mut ModelContext<Self>) -> Self {
|
||||
let mut manager = Self {
|
||||
indices: HashMap::new(),
|
||||
statuses: HashMap::new(),
|
||||
pending_rebuilds: HashSet::new(),
|
||||
rebuild_epochs: HashMap::new(),
|
||||
storage_root,
|
||||
};
|
||||
manager.restore_persisted_indices();
|
||||
manager
|
||||
}
|
||||
|
||||
/// Starts a background rebuild for an explicit local project root.
|
||||
pub fn index_directory(
|
||||
&mut self,
|
||||
root_path: PathBuf,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
let root_path = dunce::canonicalize(&root_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to canonicalize project root {}",
|
||||
root_path.display()
|
||||
)
|
||||
})?;
|
||||
if !root_path.is_dir() {
|
||||
anyhow::bail!("Project root is not a directory: {}", root_path.display());
|
||||
}
|
||||
if matches!(
|
||||
self.statuses.get(&root_path),
|
||||
Some(LocalIndexStatus::Indexing)
|
||||
) {
|
||||
self.pending_rebuilds.insert(root_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let standardized_root = StandardizedPath::from_local_canonicalized(&root_path)
|
||||
.map_err(|error| anyhow::anyhow!("Failed to standardize project root: {error}"))?;
|
||||
repo_metadata::RepoMetadataModel::handle(ctx)
|
||||
.update(ctx, |model, ctx| {
|
||||
model.index_local_directory_path(&standardized_root, ctx)
|
||||
})
|
||||
.map_err(|error| anyhow::anyhow!("Failed to register project root: {error}"))?;
|
||||
|
||||
self.start_rebuild(root_path, ctx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_rebuild(&mut self, root_path: PathBuf, ctx: &mut ModelContext<Self>) {
|
||||
let rebuild_epoch = {
|
||||
let epoch = self.rebuild_epochs.entry(root_path.clone()).or_default();
|
||||
*epoch += 1;
|
||||
*epoch
|
||||
};
|
||||
self.statuses
|
||||
.insert(root_path.clone(), LocalIndexStatus::Indexing);
|
||||
ctx.emit(LocalProjectIndexEvent::StatusChanged {
|
||||
root_path: root_path.clone(),
|
||||
status: LocalIndexStatus::Indexing,
|
||||
});
|
||||
|
||||
let storage_root = self.storage_root.clone();
|
||||
ctx.spawn(
|
||||
async move { build_persisted_index(root_path, storage_root).await },
|
||||
move |manager, result, ctx| match result {
|
||||
Ok((root_path, built_index, file_count)) => {
|
||||
if manager.rebuild_epochs.get(&root_path).copied() != Some(rebuild_epoch) {
|
||||
drop(built_index.searcher);
|
||||
let _ = fs::remove_dir_all(&built_index.generation_directory);
|
||||
return;
|
||||
}
|
||||
if let Err(error) = publish_generation(
|
||||
&manager.storage_root,
|
||||
&root_path,
|
||||
&built_index.generation,
|
||||
) {
|
||||
drop(built_index.searcher);
|
||||
manager.handle_rebuild_failure(root_path, error, ctx);
|
||||
return;
|
||||
}
|
||||
let status = LocalIndexStatus::Ready { file_count };
|
||||
manager.indices.insert(
|
||||
root_path.clone(),
|
||||
LocalProjectIndex {
|
||||
root_path: root_path.clone(),
|
||||
searcher: built_index.searcher,
|
||||
file_count,
|
||||
},
|
||||
);
|
||||
manager.statuses.insert(root_path.clone(), status.clone());
|
||||
ctx.emit(LocalProjectIndexEvent::StatusChanged {
|
||||
root_path: root_path.clone(),
|
||||
status,
|
||||
});
|
||||
let should_rebuild = manager.pending_rebuilds.remove(&root_path);
|
||||
cleanup_old_generations(&manager.storage_root, &root_path);
|
||||
if should_rebuild {
|
||||
manager.start_rebuild(root_path, ctx);
|
||||
}
|
||||
}
|
||||
Err((root_path, error)) => {
|
||||
if manager.rebuild_epochs.get(&root_path).copied() != Some(rebuild_epoch) {
|
||||
return;
|
||||
}
|
||||
manager.handle_rebuild_failure(root_path, error, ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_rebuild_failure(
|
||||
&mut self,
|
||||
root_path: PathBuf,
|
||||
error: anyhow::Error,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
log::warn!(
|
||||
"Failed to refresh local project index for {}: {error:#}",
|
||||
root_path.display()
|
||||
);
|
||||
// Keep a previously committed generation searchable. Only a root with no
|
||||
// usable generation transitions to Failed.
|
||||
if let Some(previous_index) = self.indices.get(&root_path) {
|
||||
let status = LocalIndexStatus::Ready {
|
||||
file_count: previous_index.file_count,
|
||||
};
|
||||
self.statuses.insert(root_path.clone(), status.clone());
|
||||
ctx.emit(LocalProjectIndexEvent::StatusChanged {
|
||||
root_path: root_path.clone(),
|
||||
status,
|
||||
});
|
||||
} else {
|
||||
let status = LocalIndexStatus::Failed {
|
||||
message: error.to_string(),
|
||||
};
|
||||
self.statuses.insert(root_path.clone(), status.clone());
|
||||
ctx.emit(LocalProjectIndexEvent::StatusChanged {
|
||||
root_path: root_path.clone(),
|
||||
status,
|
||||
});
|
||||
}
|
||||
let should_rebuild = self.pending_rebuilds.remove(&root_path);
|
||||
cleanup_old_generations(&self.storage_root, &root_path);
|
||||
if should_rebuild {
|
||||
self.start_rebuild(root_path, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the most specific indexed root containing `path`.
|
||||
pub fn status_for_path(&self, path: &Path) -> Option<(&Path, &LocalIndexStatus)> {
|
||||
let canonical_path = dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
|
||||
self.statuses
|
||||
.iter()
|
||||
.filter(|(root, _)| canonical_path.starts_with(root))
|
||||
.max_by_key(|(root, _)| root.components().count())
|
||||
.map(|(root, status)| (root.as_path(), status))
|
||||
}
|
||||
|
||||
pub fn is_ready_for_path(&self, path: &Path) -> bool {
|
||||
self.status_for_path(path)
|
||||
.is_some_and(|(_, status)| matches!(status, LocalIndexStatus::Ready { .. }))
|
||||
}
|
||||
|
||||
pub fn is_searchable_for_path(&self, path: &Path) -> bool {
|
||||
self.status_for_path(path).is_some_and(|(root, status)| {
|
||||
matches!(
|
||||
status,
|
||||
LocalIndexStatus::Ready { .. } | LocalIndexStatus::Indexing
|
||||
) && self.indices.contains_key(root)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn root_for_path(&self, path: &Path) -> Option<PathBuf> {
|
||||
self.status_for_path(path)
|
||||
.map(|(root, _)| root.to_path_buf())
|
||||
}
|
||||
|
||||
pub fn search(
|
||||
&self,
|
||||
path: &Path,
|
||||
query: &str,
|
||||
partial_path_segments: Option<&[String]>,
|
||||
) -> Result<Vec<LocalSearchHit>> {
|
||||
let Some((root, status)) = self.status_for_path(path) else {
|
||||
anyhow::bail!("No local project index is available for {}", path.display());
|
||||
};
|
||||
// A previous generation remains searchable while a refresh is being built.
|
||||
if !matches!(
|
||||
status,
|
||||
LocalIndexStatus::Ready { .. } | LocalIndexStatus::Indexing
|
||||
) {
|
||||
anyhow::bail!("Local project index is not ready for {}", root.display());
|
||||
}
|
||||
self.indices
|
||||
.get(root)
|
||||
.context("Local project index status has no loaded generation")?
|
||||
.search(query, partial_path_segments)
|
||||
}
|
||||
|
||||
pub fn statuses(&self) -> impl Iterator<Item = (&PathBuf, &LocalIndexStatus)> {
|
||||
self.statuses.iter()
|
||||
}
|
||||
|
||||
/// Removes a local project index and its persisted generations.
|
||||
pub fn remove_index_for_path(&mut self, root_path: PathBuf, ctx: &mut ModelContext<Self>) {
|
||||
let root_path = dunce::canonicalize(&root_path).unwrap_or(root_path);
|
||||
self.remove_index(&root_path);
|
||||
ctx.emit(LocalProjectIndexEvent::IndexRemoved { root_path });
|
||||
}
|
||||
|
||||
fn handle_repo_metadata_event(
|
||||
&mut self,
|
||||
event: &RepoMetadataEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let local_root = match event {
|
||||
RepoMetadataEvent::FileTreeEntryUpdated {
|
||||
id: RepositoryIdentifier::Local(path),
|
||||
..
|
||||
} => path.to_local_path(),
|
||||
// FileTreeUpdated is emitted before repository mutations are applied. The
|
||||
// post-application FileTreeEntryUpdated event below is the rebuild trigger; using
|
||||
// both would start duplicate full rebuilds for every watcher batch.
|
||||
RepoMetadataEvent::FileTreeUpdated { .. } => None,
|
||||
RepoMetadataEvent::RepositoryRemoved {
|
||||
id: RepositoryIdentifier::Local(path),
|
||||
} => {
|
||||
let local_path = path.to_local_path_lossy();
|
||||
let root_path = dunce::canonicalize(&local_path).unwrap_or(local_path);
|
||||
if self.statuses.contains_key(&root_path) {
|
||||
self.remove_index(&root_path);
|
||||
ctx.emit(LocalProjectIndexEvent::IndexRemoved { root_path });
|
||||
}
|
||||
return;
|
||||
}
|
||||
RepoMetadataEvent::RepositoryUpdated { .. }
|
||||
| RepoMetadataEvent::RepositoryRemoved {
|
||||
id: RepositoryIdentifier::Remote(_),
|
||||
}
|
||||
| RepoMetadataEvent::FileTreeEntryUpdated {
|
||||
id: RepositoryIdentifier::Remote(_),
|
||||
..
|
||||
}
|
||||
| RepoMetadataEvent::UpdatingRepositoryFailed { .. }
|
||||
| RepoMetadataEvent::StandingQueryResultsUpdated { .. }
|
||||
| RepoMetadataEvent::IncrementalUpdateReady { .. } => None,
|
||||
};
|
||||
|
||||
let Some(local_root) = local_root else {
|
||||
return;
|
||||
};
|
||||
let canonical_root = dunce::canonicalize(&local_root).unwrap_or(local_root);
|
||||
if self.statuses.contains_key(&canonical_root) {
|
||||
if matches!(
|
||||
self.statuses.get(&canonical_root),
|
||||
Some(LocalIndexStatus::Indexing)
|
||||
) {
|
||||
self.pending_rebuilds.insert(canonical_root);
|
||||
} else {
|
||||
self.start_rebuild(canonical_root, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_index(&mut self, root_path: &Path) {
|
||||
let root_path = dunce::canonicalize(root_path).unwrap_or_else(|_| root_path.to_path_buf());
|
||||
self.indices.remove(&root_path);
|
||||
self.statuses.remove(&root_path);
|
||||
self.pending_rebuilds.remove(&root_path);
|
||||
let epoch = self.rebuild_epochs.entry(root_path.clone()).or_default();
|
||||
*epoch += 1;
|
||||
let directory = repository_storage_directory(&self.storage_root, &root_path);
|
||||
if let Err(error) = fs::remove_dir_all(directory) {
|
||||
if error.kind() != std::io::ErrorKind::NotFound {
|
||||
log::warn!("Failed to remove local project index: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_persisted_indices(&mut self) {
|
||||
let Ok(entries) = fs::read_dir(&self.storage_root) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let directory = entry.path();
|
||||
if !directory.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let metadata = match read_metadata(&directory) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"Discarding invalid local project index at {}: {error:#}",
|
||||
directory.display()
|
||||
);
|
||||
let _ = fs::remove_dir_all(&directory);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if metadata.schema_version != INDEX_SCHEMA_VERSION {
|
||||
let _ = fs::remove_dir_all(&directory);
|
||||
continue;
|
||||
}
|
||||
|
||||
let raw_root_path = PathBuf::from(&metadata.root_path);
|
||||
let Ok(root_path) = dunce::canonicalize(&raw_root_path) else {
|
||||
log::debug!(
|
||||
"Discarding local project index for missing root {}",
|
||||
raw_root_path.display()
|
||||
);
|
||||
let _ = fs::remove_dir_all(&directory);
|
||||
continue;
|
||||
};
|
||||
if !root_path.is_dir()
|
||||
|| directory.file_name().and_then(|name| name.to_str())
|
||||
!= Some(&format_storage_directory_name(&root_path))
|
||||
{
|
||||
let _ = fs::remove_dir_all(&directory);
|
||||
continue;
|
||||
}
|
||||
|
||||
let generation_path = directory.join("generations").join(&metadata.generation);
|
||||
match SimpleFullTextSearcher::open_in_dir(
|
||||
&LOCAL_PROJECT_INDEX_SCHEMA,
|
||||
DEFAULT_MEMORY_BUDGET,
|
||||
&generation_path,
|
||||
) {
|
||||
Ok(searcher) => {
|
||||
self.indices.insert(
|
||||
root_path.clone(),
|
||||
LocalProjectIndex {
|
||||
root_path: root_path.clone(),
|
||||
searcher,
|
||||
file_count: metadata.file_count,
|
||||
},
|
||||
);
|
||||
self.statuses.insert(
|
||||
root_path,
|
||||
LocalIndexStatus::Ready {
|
||||
file_count: metadata.file_count,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"Discarding unreadable local project index at {}: {error:#}",
|
||||
generation_path.display()
|
||||
);
|
||||
let _ = fs::remove_dir_all(&directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BuiltLocalProjectIndex {
|
||||
searcher: SimpleFullTextSearcher<LocalProjectIndexSchema>,
|
||||
generation: String,
|
||||
generation_directory: PathBuf,
|
||||
}
|
||||
|
||||
async fn build_persisted_index(
|
||||
root_path: PathBuf,
|
||||
storage_root: PathBuf,
|
||||
) -> std::result::Result<(PathBuf, BuiltLocalProjectIndex, usize), (PathBuf, anyhow::Error)> {
|
||||
let error_root_path = root_path.clone();
|
||||
let result = async {
|
||||
let documents = build_documents(&root_path).await?;
|
||||
let index_directory = repository_storage_directory(&storage_root, &root_path);
|
||||
fs::create_dir_all(index_directory.join("generations"))?;
|
||||
let generation = format!("generation-{}", uuid::Uuid::new_v4());
|
||||
let temporary_directory = index_directory
|
||||
.join("generations")
|
||||
.join(format!(".tmp-{generation}"));
|
||||
let generation_directory = index_directory.join("generations").join(&generation);
|
||||
let _ = fs::remove_dir_all(&temporary_directory);
|
||||
fs::create_dir_all(&temporary_directory)?;
|
||||
|
||||
let searcher = match SimpleFullTextSearcher::create_in_dir(
|
||||
&LOCAL_PROJECT_INDEX_SCHEMA,
|
||||
DEFAULT_MEMORY_BUDGET,
|
||||
&temporary_directory,
|
||||
) {
|
||||
Ok(searcher) => searcher,
|
||||
Err(error) => {
|
||||
let _ = fs::remove_dir_all(&temporary_directory);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
if let Err(error) = searcher.build_index(build_search_documents(&documents)) {
|
||||
let _ = fs::remove_dir_all(&temporary_directory);
|
||||
return Err(error);
|
||||
}
|
||||
// Close the writer before moving the directory so the committed generation can be
|
||||
// reopened consistently on all supported platforms.
|
||||
drop(searcher);
|
||||
// The generation is complete and validated before it becomes visible through CURRENT.
|
||||
fs::rename(&temporary_directory, &generation_directory)?;
|
||||
let searcher = SimpleFullTextSearcher::open_in_dir(
|
||||
&LOCAL_PROJECT_INDEX_SCHEMA,
|
||||
DEFAULT_MEMORY_BUDGET,
|
||||
&generation_directory,
|
||||
)?;
|
||||
|
||||
let metadata = LocalIndexMetadata {
|
||||
root_path: root_path.to_string_lossy().into_owned(),
|
||||
schema_version: INDEX_SCHEMA_VERSION,
|
||||
generation: generation.clone(),
|
||||
file_count: documents.len(),
|
||||
};
|
||||
let metadata_json = serde_json::to_vec_pretty(&metadata)?;
|
||||
// Store metadata inside the generation so an interrupted refresh cannot replace the
|
||||
// metadata belonging to the generation still referenced by CURRENT.
|
||||
fs::write(generation_directory.join(METADATA_FILE_NAME), metadata_json)?;
|
||||
|
||||
Ok((
|
||||
root_path,
|
||||
BuiltLocalProjectIndex {
|
||||
searcher,
|
||||
generation,
|
||||
generation_directory,
|
||||
},
|
||||
documents.len(),
|
||||
))
|
||||
}
|
||||
.await;
|
||||
result.map_err(|error| (error_root_path, error))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalProjectDocument {
|
||||
file_path: String,
|
||||
symbol: String,
|
||||
metadata: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
fn build_search_documents(
|
||||
documents: &[LocalProjectDocument],
|
||||
) -> impl Iterator<Item = LocalProjectSearchDocument> + '_ {
|
||||
documents
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|document| LocalProjectSearchDocument {
|
||||
symbol: document.symbol,
|
||||
path: document.file_path.clone(),
|
||||
metadata: document.metadata,
|
||||
body: document.body,
|
||||
file_path: document.file_path,
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_documents(root_path: &Path) -> Result<Vec<LocalProjectDocument>> {
|
||||
let root_path = dunce::canonicalize(root_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to canonicalize project root {}",
|
||||
root_path.display()
|
||||
)
|
||||
})?;
|
||||
let outline = build_outline(&root_path, Some(MAX_INDEX_FILES)).await?;
|
||||
let mut documents = Vec::new();
|
||||
for file in outline.to_file_symbols(None) {
|
||||
let relative_path = PathBuf::from(&file.path);
|
||||
let absolute_path = root_path.join(&relative_path);
|
||||
let Ok(bytes) = fs::read(&absolute_path) else {
|
||||
continue;
|
||||
};
|
||||
if bytes.is_empty() || bytes.len() > MAX_INDEXED_FILE_BYTES {
|
||||
continue;
|
||||
}
|
||||
let Ok(content) = String::from_utf8(bytes) else {
|
||||
continue;
|
||||
};
|
||||
let body = content.chars().take(MAX_INDEXED_BODY_BYTES).collect();
|
||||
let extension = absolute_path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.unwrap_or_default();
|
||||
let metadata = format!("{extension} {}", file.symbols);
|
||||
documents.push(LocalProjectDocument {
|
||||
file_path: absolute_path.to_string_lossy().into_owned(),
|
||||
symbol: file.symbols,
|
||||
metadata,
|
||||
body,
|
||||
});
|
||||
}
|
||||
Ok(documents)
|
||||
}
|
||||
|
||||
fn format_storage_directory_name(root_path: &Path) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(root_path.to_string_lossy().as_bytes());
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
fn repository_storage_directory(storage_root: &Path, root_path: &Path) -> PathBuf {
|
||||
storage_root.join(format_storage_directory_name(root_path))
|
||||
}
|
||||
|
||||
fn publish_generation(storage_root: &Path, root_path: &Path, generation: &str) -> Result<()> {
|
||||
let index_directory = repository_storage_directory(storage_root, root_path);
|
||||
let temporary_current = index_directory.join(format!(".{CURRENT_FILE_NAME}.tmp"));
|
||||
fs::write(&temporary_current, generation.as_bytes())?;
|
||||
fs::rename(&temporary_current, index_directory.join(CURRENT_FILE_NAME))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup_old_generations(storage_root: &Path, root_path: &Path) {
|
||||
let index_directory = repository_storage_directory(storage_root, root_path);
|
||||
let Ok(current) = fs::read_to_string(index_directory.join(CURRENT_FILE_NAME)) else {
|
||||
return;
|
||||
};
|
||||
let current = current.trim();
|
||||
let generations_directory = index_directory.join("generations");
|
||||
let Ok(entries) = fs::read_dir(&generations_directory) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let generation = entry.file_name();
|
||||
if generation.to_string_lossy() != current {
|
||||
let _ = fs::remove_dir_all(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_metadata(directory: &Path) -> Result<LocalIndexMetadata> {
|
||||
let current = fs::read_to_string(directory.join(CURRENT_FILE_NAME))?;
|
||||
let current = current.trim();
|
||||
let mut components = Path::new(current).components();
|
||||
if current.is_empty()
|
||||
|| !matches!(components.next(), Some(std::path::Component::Normal(_)))
|
||||
|| components.next().is_some()
|
||||
{
|
||||
anyhow::bail!("Invalid local index generation name");
|
||||
}
|
||||
let metadata_path = directory
|
||||
.join("generations")
|
||||
.join(current)
|
||||
.join(METADATA_FILE_NAME);
|
||||
let metadata: LocalIndexMetadata = serde_json::from_slice(&fs::read(metadata_path)?)?;
|
||||
if metadata.generation != current {
|
||||
anyhow::bail!("Local index generation pointer does not match metadata");
|
||||
}
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::fs;
|
||||
|
||||
use tempfile::TempDir;
|
||||
use warp_search_core::searcher::SimpleFullTextSearcher;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn local_index_documents_include_symbols_paths_and_body_text() {
|
||||
let root = TempDir::new().unwrap();
|
||||
fs::create_dir(root.path().join("src")).unwrap();
|
||||
fs::write(
|
||||
root.path().join("src/lib.rs"),
|
||||
"/// Authenticate a request\npub fn authenticate_request() {}\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let documents = futures::executor::block_on(build_documents(root.path())).unwrap();
|
||||
assert_eq!(documents.len(), 1);
|
||||
assert!(documents[0].file_path.ends_with("src/lib.rs"));
|
||||
assert!(documents[0].symbol.contains("authenticate_request"));
|
||||
assert!(documents[0].body.contains("Authenticate a request"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistent_index_can_be_reopened_without_network_access() {
|
||||
let root = TempDir::new().unwrap();
|
||||
let storage = TempDir::new().unwrap();
|
||||
fs::write(
|
||||
root.path().join("main.rs"),
|
||||
"fn local_authentication() {}\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let canonical_root = dunce::canonicalize(root.path()).unwrap();
|
||||
let (root_path, built, _) = futures::executor::block_on(build_persisted_index(
|
||||
canonical_root,
|
||||
storage.path().to_path_buf(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
built
|
||||
.searcher
|
||||
.search_id("local_authentication")
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
let directory = repository_storage_directory(storage.path(), &root_path);
|
||||
publish_generation(storage.path(), &root_path, &built.generation).unwrap();
|
||||
let metadata = read_metadata(&directory).unwrap();
|
||||
let reopened = SimpleFullTextSearcher::open_in_dir(
|
||||
&LOCAL_PROJECT_INDEX_SCHEMA,
|
||||
DEFAULT_MEMORY_BUDGET,
|
||||
&directory.join("generations").join(metadata.generation),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(reopened.search_id("local_authentication").unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_paths_filter_local_search_results() {
|
||||
let root = TempDir::new().unwrap();
|
||||
fs::create_dir(root.path().join("src")).unwrap();
|
||||
fs::create_dir(root.path().join("tests")).unwrap();
|
||||
fs::write(root.path().join("src/lib.rs"), "fn authentication() {}\n").unwrap();
|
||||
fs::write(root.path().join("tests/lib.rs"), "fn authentication() {}\n").unwrap();
|
||||
|
||||
let storage = TempDir::new().unwrap();
|
||||
let canonical_root = dunce::canonicalize(root.path()).unwrap();
|
||||
let (_, built, file_count) = futures::executor::block_on(build_persisted_index(
|
||||
canonical_root.clone(),
|
||||
storage.path().to_path_buf(),
|
||||
))
|
||||
.unwrap();
|
||||
let manager = LocalProjectIndex {
|
||||
root_path: canonical_root,
|
||||
searcher: built.searcher,
|
||||
file_count,
|
||||
};
|
||||
|
||||
let matches = manager
|
||||
.search("authentication", Some(&["src".to_string()]))
|
||||
.unwrap();
|
||||
assert_eq!(matches.len(), 1);
|
||||
assert!(matches[0].path.ends_with("src/lib.rs"));
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
mod file_outline;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
pub mod local_project_index;
|
||||
pub mod locations;
|
||||
pub const DEFAULT_SYNC_REQUESTS_PER_MIN: u32 = 600;
|
||||
|
||||
|
||||
@@ -35,13 +35,33 @@ impl ExecutionMode {
|
||||
pub struct AppExecutionMode {
|
||||
mode: ExecutionMode,
|
||||
is_sandboxed: bool,
|
||||
local_project_indexing_enabled: bool,
|
||||
}
|
||||
|
||||
impl AppExecutionMode {
|
||||
/// Create an `AppExecutionMode` model with the execution mode set.
|
||||
pub fn new(mode: ExecutionMode, is_sandboxed: bool, _ctx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn new(mode: ExecutionMode, is_sandboxed: bool, ctx: &mut ModelContext<Self>) -> Self {
|
||||
Self::new_with_local_project_indexing(
|
||||
mode,
|
||||
is_sandboxed,
|
||||
matches!(mode, ExecutionMode::App | ExecutionMode::Sdk),
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create an execution-mode model with an explicit local project-index capability.
|
||||
pub fn new_with_local_project_indexing(
|
||||
mode: ExecutionMode,
|
||||
is_sandboxed: bool,
|
||||
local_project_indexing_enabled: bool,
|
||||
_ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _ = GLOBAL_EXECUTION_MODE.set(mode);
|
||||
Self { mode, is_sandboxed }
|
||||
Self {
|
||||
mode,
|
||||
is_sandboxed,
|
||||
local_project_indexing_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if running as the full desktop app.
|
||||
@@ -121,6 +141,18 @@ impl AppExecutionMode {
|
||||
pub fn is_sandboxed(&self) -> bool {
|
||||
self.is_sandboxed
|
||||
}
|
||||
|
||||
/// Returns whether this process is the remote-server daemon. Native local app and SDK
|
||||
/// sessions use the local project index, while the daemon must retain its remote indexing
|
||||
/// pipeline.
|
||||
pub fn is_remote_server_daemon(&self) -> bool {
|
||||
matches!(self.mode, ExecutionMode::RemoteServerDaemon)
|
||||
}
|
||||
|
||||
/// Returns whether local project indexing is registered and may be used in this process.
|
||||
pub fn local_project_indexing_enabled(&self) -> bool {
|
||||
self.local_project_indexing_enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for AppExecutionMode {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::iter::Peekable;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::thread::available_parallelism;
|
||||
use std::time::Duration;
|
||||
@@ -474,7 +475,7 @@ impl SearcherWriterWrapper {
|
||||
MAX_THREADS_PER_INDEX_WRITER,
|
||||
);
|
||||
let writer = search_index
|
||||
.writer_with_num_threads(memory_budget, num_threads)
|
||||
.writer_with_num_threads(num_threads, memory_budget)
|
||||
.ok();
|
||||
|
||||
SearcherWriterWrapper {
|
||||
@@ -653,40 +654,99 @@ pub struct SimpleFullTextSearcher<C: SearchSchemaConfig> {
|
||||
|
||||
impl<C: SearchSchemaConfig> SimpleFullTextSearcher<C> {
|
||||
pub fn new(schema: &FullTextSearchSchema<C>, memory_budget: usize) -> Self {
|
||||
let mut schema_builder = Schema::builder();
|
||||
|
||||
// Add composite key field for efficient term querying
|
||||
let composite_key_field = schema_builder.add_bytes_field(
|
||||
COMPOSITE_KEY_FIELD,
|
||||
BytesOptions::default().set_indexed().set_stored(),
|
||||
);
|
||||
|
||||
let mut weighted_search_fields = HashMap::new();
|
||||
let mut normalizing_factor = 0.0;
|
||||
for (field_name, weight) in schema.weighted_search_fields.iter() {
|
||||
let text_indexing = TEXT
|
||||
.get_indexing_options()
|
||||
.cloned()
|
||||
.unwrap_or(
|
||||
TextFieldIndexing::default()
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||
let (
|
||||
tantivy_schema,
|
||||
composite_key_field,
|
||||
weighted_search_fields,
|
||||
id_fields,
|
||||
normalizing_factor,
|
||||
) = build_tantivy_schema(schema);
|
||||
Self::new_with_index(
|
||||
Arc::new(Index::create_in_ram(tantivy_schema)),
|
||||
composite_key_field,
|
||||
weighted_search_fields,
|
||||
id_fields,
|
||||
normalizing_factor,
|
||||
memory_budget,
|
||||
)
|
||||
.set_tokenizer("custom");
|
||||
let text_option = TEXT.clone().set_indexing_options(text_indexing) | STORED;
|
||||
|
||||
let field = schema_builder.add_text_field(field_name, text_option);
|
||||
weighted_search_fields.insert(field_name.clone(), (field, *weight));
|
||||
normalizing_factor += weight;
|
||||
}
|
||||
|
||||
let mut id_fields = HashMap::new();
|
||||
for (field_name, field_type) in schema.id_fields.iter() {
|
||||
let field =
|
||||
schema_builder.add_field(field_type.field_entry_from_name(field_name.clone()));
|
||||
id_fields.insert(field_name.clone(), (field, *field_type));
|
||||
/// Opens an existing Tantivy index in `directory` and validates its schema.
|
||||
pub fn open_in_dir(
|
||||
schema: &FullTextSearchSchema<C>,
|
||||
memory_budget: usize,
|
||||
directory: &Path,
|
||||
) -> anyhow::Result<Self> {
|
||||
let (
|
||||
expected_schema,
|
||||
composite_key_field,
|
||||
weighted_search_fields,
|
||||
id_fields,
|
||||
normalizing_factor,
|
||||
) = build_tantivy_schema(schema);
|
||||
let search_index = Index::open_in_dir(directory)?;
|
||||
if search_index.schema() != expected_schema {
|
||||
anyhow::bail!("Persistent Tantivy index schema does not match the requested schema");
|
||||
}
|
||||
Ok(Self::new_with_index(
|
||||
Arc::new(search_index),
|
||||
composite_key_field,
|
||||
weighted_search_fields,
|
||||
id_fields,
|
||||
normalizing_factor,
|
||||
memory_budget,
|
||||
))
|
||||
}
|
||||
|
||||
let search_index = Arc::new(Index::create_in_ram(schema_builder.build()));
|
||||
/// Creates a new Tantivy index in `directory` and validates its schema.
|
||||
pub fn create_in_dir(
|
||||
schema: &FullTextSearchSchema<C>,
|
||||
memory_budget: usize,
|
||||
directory: &Path,
|
||||
) -> anyhow::Result<Self> {
|
||||
std::fs::create_dir_all(directory)?;
|
||||
let (
|
||||
tantivy_schema,
|
||||
composite_key_field,
|
||||
weighted_search_fields,
|
||||
id_fields,
|
||||
normalizing_factor,
|
||||
) = build_tantivy_schema(schema);
|
||||
let search_index = Index::create_in_dir(directory, tantivy_schema)?;
|
||||
Ok(Self::new_with_index(
|
||||
Arc::new(search_index),
|
||||
composite_key_field,
|
||||
weighted_search_fields,
|
||||
id_fields,
|
||||
normalizing_factor,
|
||||
memory_budget,
|
||||
))
|
||||
}
|
||||
|
||||
/// Opens or creates a Tantivy index in `directory`.
|
||||
///
|
||||
/// This compatibility constructor is intended for callers that explicitly accept the
|
||||
/// open-or-create behavior. Persistent committed generations should use [`Self::open_in_dir`]
|
||||
/// and new temporary generations should use [`Self::create_in_dir`].
|
||||
pub fn new_in_dir(
|
||||
schema: &FullTextSearchSchema<C>,
|
||||
memory_budget: usize,
|
||||
directory: &Path,
|
||||
) -> anyhow::Result<Self> {
|
||||
match Self::open_in_dir(schema, memory_budget, directory) {
|
||||
Ok(searcher) => Ok(searcher),
|
||||
Err(_) => Self::create_in_dir(schema, memory_budget, directory),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_with_index(
|
||||
search_index: Arc<Index>,
|
||||
composite_key_field: Field,
|
||||
weighted_search_fields: HashMap<String, (Field, f32)>,
|
||||
id_fields: HashMap<String, (Field, FullTextSearchFieldTypes)>,
|
||||
normalizing_factor: f32,
|
||||
memory_budget: usize,
|
||||
) -> Self {
|
||||
search_index
|
||||
.tokenizers()
|
||||
.register("custom", CustomTokenizer::default());
|
||||
@@ -695,10 +755,8 @@ impl<C: SearchSchemaConfig> SimpleFullTextSearcher<C> {
|
||||
search_index,
|
||||
weighted_search_fields,
|
||||
id_fields,
|
||||
// The normalization is done via division, so in order to boost the score, we divide by the boost factor.
|
||||
normalizing_factor / schema.boost_factor,
|
||||
normalizing_factor,
|
||||
)));
|
||||
|
||||
let writer = Arc::new(Mutex::new(SearcherWriterWrapper::new(
|
||||
reader.clone(),
|
||||
composite_key_field,
|
||||
@@ -841,6 +899,59 @@ impl<C: SearchSchemaConfig> SimpleFullTextSearcher<C> {
|
||||
}
|
||||
}
|
||||
|
||||
type TantivySchemaParts = (
|
||||
Schema,
|
||||
Field,
|
||||
HashMap<String, (Field, f32)>,
|
||||
HashMap<String, (Field, FullTextSearchFieldTypes)>,
|
||||
f32,
|
||||
);
|
||||
|
||||
fn build_tantivy_schema<C: SearchSchemaConfig>(
|
||||
schema: &FullTextSearchSchema<C>,
|
||||
) -> TantivySchemaParts {
|
||||
let mut schema_builder = Schema::builder();
|
||||
let composite_key_field = schema_builder.add_bytes_field(
|
||||
COMPOSITE_KEY_FIELD,
|
||||
BytesOptions::default().set_indexed().set_stored(),
|
||||
);
|
||||
|
||||
let mut weighted_search_fields = HashMap::new();
|
||||
let mut normalizing_factor = 0.0;
|
||||
let mut weighted_fields = schema.weighted_search_fields.iter().collect_vec();
|
||||
weighted_fields.sort_by_key(|(left, _)| *left);
|
||||
for (field_name, weight) in weighted_fields {
|
||||
let text_indexing = TEXT
|
||||
.get_indexing_options()
|
||||
.cloned()
|
||||
.unwrap_or(
|
||||
TextFieldIndexing::default()
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||
)
|
||||
.set_tokenizer("custom");
|
||||
let text_option = TEXT.clone().set_indexing_options(text_indexing) | STORED;
|
||||
let field = schema_builder.add_text_field(field_name, text_option);
|
||||
weighted_search_fields.insert(field_name.clone(), (field, *weight));
|
||||
normalizing_factor += weight;
|
||||
}
|
||||
|
||||
let mut id_fields = HashMap::new();
|
||||
let mut id_field_entries = schema.id_fields.iter().collect_vec();
|
||||
id_field_entries.sort_by_key(|(left, _)| *left);
|
||||
for (field_name, field_type) in id_field_entries {
|
||||
let field = schema_builder.add_field(field_type.field_entry_from_name(field_name.clone()));
|
||||
id_fields.insert(field_name.clone(), (field, *field_type));
|
||||
}
|
||||
|
||||
(
|
||||
schema_builder.build(),
|
||||
composite_key_field,
|
||||
weighted_search_fields,
|
||||
id_fields,
|
||||
normalizing_factor / schema.boost_factor,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_term_query(term: Term) -> Box<BooleanQuery> {
|
||||
let term_query = Box::new(TermQuery::new(
|
||||
term.clone(),
|
||||
|
||||
Reference in New Issue
Block a user