first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+12 -19
View File
@@ -1,18 +1,17 @@
use fuzzy_match::{match_indices_case_insensitive, FuzzyMatchResult};
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle};
use std::collections::HashMap;
use std::sync::Arc;
use fuzzy_match::{match_indices_case_insensitive, FuzzyMatchResult};
use galaxyui::keymap::{BindingId, DescriptionContext};
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle};
use crate::search::action::search_item::MatchedBinding;
use crate::search::binding_source::BindingSource;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::data_source::{DataSourceSearchError, Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::util::bindings::CommandBinding;
use crate::search::binding_source::BindingSource;
use galaxyui::keymap::{BindingId, DescriptionContext};
/// Data source for [`CommandBinding`]s. Produces a list of in-app actions a user can currently
/// perform.
pub struct CommandBindingDataSource {
@@ -104,9 +103,7 @@ impl SyncDataSource for CommandBindingDataSource {
self.searcher
.search(&query.text.trim().to_lowercase())
.map_err(|err| {
let search_error = DataSourceSearchError {
message: err.to_string(),
};
let search_error = DataSourceSearchError::new(err.to_string());
Box::new(search_error) as DataSourceRunErrorWrapper
})
}
@@ -183,20 +180,16 @@ impl ActionSearcher for FuzzyActionSearcher {
#[cfg(not(target_family = "wasm"))]
mod full_text_searcher {
use crate::define_search_schema;
use crate::search::action::{
data_source::{is_excluded_binding, ActionSearcher, SearcherAction},
search_item::MatchedBinding,
};
use fuzzy_match::FuzzyMatchResult;
use warp_search_core::define_search_schema;
use warpui::keymap::{BindingId, DescriptionContext};
use crate::search::action::data_source::{is_excluded_binding, ActionSearcher, SearcherAction};
use crate::search::data_source::QueryResult;
use crate::search::searcher::{
SimpleFullTextSearcher, DEFAULT_MEMORY_BUDGET, SCORE_CONVERSION_FACTOR,
};
use crate::util::bindings::CommandBinding;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::keymap::{BindingId, DescriptionContext};
use std::collections::HashMap;
use std::sync::Arc;
define_search_schema!(
schema_name: ACTION_SEARCH_SCHEMA,
+13 -11
View File
@@ -1,3 +1,16 @@
use std::sync::Arc;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use pathfinder_color::ColorU;
use warpui::elements::{
Align, ConstrainedBox, Container, Flex, Highlight, ParentElement, Shrinkable, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::keymap::{DescriptionContext, Keystroke};
use warpui::ui_components::components::UiComponent;
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::drive::cloud_object_styling::warp_drive_icon_color;
use crate::drive::DriveObjectType;
@@ -10,17 +23,6 @@ use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use crate::ui_components::icons::Icon;
use crate::util::bindings::{BindingGroup, CommandBinding};
use fuzzy_match::FuzzyMatchResult;
use galaxyui::elements::{
Align, ConstrainedBox, Container, Flex, Highlight, ParentElement, Shrinkable, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::keymap::{DescriptionContext, Keystroke};
use galaxyui::ui_components::components::UiComponent;
use galaxyui::{AppContext, Element, SingletonEntity};
use ordered_float::OrderedFloat;
use pathfinder_color::ColorU;
use std::sync::Arc;
/// A matched binding from a search query.
#[derive(Debug)]
@@ -1,3 +1,7 @@
use fuzzy_match::FuzzyMatchResult;
use itertools::Itertools;
use warpui::{AppContext, Entity, SingletonEntity};
use super::search_item::BlockSearchItem;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
@@ -6,9 +10,6 @@ use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::terminal::model::block::Block;
use crate::terminal::TerminalView;
use crate::workspace::ActiveSession;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{AppContext, Entity, SingletonEntity};
use itertools::Itertools;
const MAX_RESULTS: usize = 20;
const ZERO_STATE_BASE_SCORE: i64 = 1000;
@@ -1,4 +1,7 @@
use chrono::{Duration, Local};
use fuzzy_match::FuzzyMatchResult;
use galaxy_core::command::ExitCode;
use warpui::{App, SingletonEntity};
use crate::search::ai_context_menu::blocks::data_source::BlockDataSource;
use crate::search::ai_context_menu::blocks::search_item::BlockSearchItem;
@@ -11,10 +14,6 @@ use crate::test_util::terminal::{
};
use crate::workspace::ActiveSession;
use fuzzy_match::FuzzyMatchResult;
use galaxy_core::command::ExitCode;
use galaxyui::{App, SingletonEntity};
/// Helper to create a `BlockSearchItem` with the given parameters.
fn make_block_search_item(
command: &str,
@@ -1,3 +1,13 @@
use chrono::{DateTime, Local};
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use galaxy_core::command::ExitCode;
use warpui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::styles;
@@ -5,17 +15,6 @@ use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use crate::terminal::model::block::BlockId;
use crate::util::truncation::truncate_from_end;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::elements::Highlight;
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{
elements::{ConstrainedBox, Container, CrossAxisAlignment, Flex, Icon, ParentElement, Text},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
use chrono::{DateTime, Local};
use galaxy_core::command::ExitCode;
/// Calculate how long ago a timestamp was
fn time_ago_string(timestamp: Option<&DateTime<Local>>) -> String {
@@ -1,7 +1,33 @@
#![cfg_attr(not(feature = "local_fs"), allow(dead_code))]
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
#[cfg(not(target_family = "wasm"))]
use std::path::Path;
use std::path::PathBuf;
#[cfg(not(target_family = "wasm"))]
use std::time::Duration;
use ai::index::Symbol;
use fuzzy_match::FuzzyMatchResult;
#[cfg(not(target_family = "wasm"))]
use instant::Instant;
#[cfg(not(target_family = "wasm"))]
use itertools::Itertools;
#[cfg(not(target_family = "wasm"))]
use repo_metadata::repositories::DetectedRepositories;
#[cfg(not(target_family = "wasm"))]
use warp_util::local_or_remote_path::LocalOrRemotePath;
use warpui::AppContext;
#[cfg(not(target_family = "wasm"))]
use warpui::ModelSpawner;
#[cfg(not(target_family = "wasm"))]
use warpui::SingletonEntity;
#[cfg(not(target_family = "wasm"))]
use super::search_item::CodeSearchItem;
#[cfg(not(target_family = "wasm"))]
use crate::ai::outline::{OutlineStatus, RepoOutlines, RepoOutlinesEvent};
#[cfg(not(target_family = "wasm"))]
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
#[cfg(not(target_family = "wasm"))]
use crate::search::data_source::{Query, QueryResult};
@@ -11,32 +37,8 @@ use crate::search::files::model::FileSearchModel;
use crate::search::mixer::{
AsyncDataSource, BoxFuture, DataSourceRunError, DataSourceRunErrorWrapper,
};
use ai::index::Symbol;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::AppContext;
#[cfg(not(target_family = "wasm"))]
use galaxyui::ModelSpawner;
#[cfg(not(target_family = "wasm"))]
use instant::Instant;
#[cfg(not(target_family = "wasm"))]
use itertools::Itertools;
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
#[cfg(not(target_family = "wasm"))]
use std::time::Duration;
#[cfg(not(target_family = "wasm"))]
use crate::ai::outline::{OutlineStatus, RepoOutlines, RepoOutlinesEvent};
#[cfg(not(target_family = "wasm"))]
use crate::workspace::ActiveSession;
#[cfg(not(target_family = "wasm"))]
use galaxyui::SingletonEntity;
#[cfg(not(target_family = "wasm"))]
use repo_metadata::repositories::DetectedRepositories;
#[cfg(not(target_family = "wasm"))]
use std::path::Path;
const MAX_RESULTS: usize = 200;
@@ -84,12 +86,15 @@ impl CodeSymbolCache {
spawner,
};
ctx.subscribe_to_model(&RepoOutlines::handle(ctx), |me, event, ctx| match event {
RepoOutlinesEvent::OutlinesUpdated(repo_path) => {
me.symbol_cache.get_mut().remove(repo_path);
ctx.emit(());
}
});
ctx.subscribe_to_model(
&RepoOutlines::handle(ctx),
|me, _, event, ctx| match event {
RepoOutlinesEvent::OutlinesUpdated(repo_path) => {
me.symbol_cache.get_mut().remove(repo_path);
ctx.emit(());
}
},
);
cache
}
@@ -118,7 +123,11 @@ impl CodeSymbolCache {
.active_window
.and_then(|window_id| ActiveSession::as_ref(app).path_if_local(window_id))
.and_then(|current_dir| {
DetectedRepositories::as_ref(app).get_root_for_path(Path::new(current_dir))
DetectedRepositories::as_ref(app)
.get_root_for_path(&LocalOrRemotePath::Local(
Path::new(current_dir).to_path_buf(),
))
.and_then(|r| PathBuf::try_from(r).ok())
})?;
let (outline_status, _) = RepoOutlines::as_ref(app).get_outline(&git_repo_path)?;
@@ -198,7 +207,11 @@ impl CodeSymbolCache {
.active_window
.and_then(|window_id| ActiveSession::as_ref(app).path_if_local(window_id))
.and_then(|current_dir| {
DetectedRepositories::as_ref(app).get_root_for_path(Path::new(current_dir))
DetectedRepositories::as_ref(app)
.get_root_for_path(&LocalOrRemotePath::Local(
Path::new(current_dir).to_path_buf(),
))
.and_then(|r| PathBuf::try_from(r).ok())
})
else {
return HashSet::new();
@@ -1,9 +1,11 @@
#[cfg(test)]
use super::*;
use ai::index::Symbol;
use std::collections::HashSet;
use std::path::PathBuf;
use ai::index::Symbol;
#[cfg(test)]
use super::*;
fn create_test_symbol(name: &str, type_prefix: Option<&str>) -> CodeSymbol {
CodeSymbol {
file_path: PathBuf::from("test.rs"),
+6 -4
View File
@@ -3,14 +3,16 @@ pub mod data_source;
pub mod search_item;
#[cfg(not(target_family = "wasm"))]
use crate::ai::outline::{OutlineStatus, RepoOutlines};
#[cfg(not(target_family = "wasm"))]
use crate::workspace::ActiveSession;
use std::path::Path;
use galaxyui::AppContext;
#[cfg(not(target_family = "wasm"))]
use galaxyui::SingletonEntity;
#[cfg(not(target_family = "wasm"))]
use std::path::Path;
use crate::ai::outline::{OutlineStatus, RepoOutlines};
#[cfg(not(target_family = "wasm"))]
use crate::workspace::ActiveSession;
/// Checks if the code symbols (outline) are currently being indexed for the active directory.
/// Returns true if the outline is in a pending state, false otherwise.
@@ -1,8 +1,3 @@
use crate::appearance::Appearance;
use crate::search::ai_context_menu::styles;
use crate::search::ai_context_menu::{mixer::AIContextMenuSearchableAction, safe_truncate};
use crate::search::item::{IconLocation, SearchItem};
use crate::search::result_renderer::ItemHighlightState;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text,
@@ -13,6 +8,11 @@ use ordered_float::OrderedFloat;
// Import CodeSymbol from the data_source module
use super::data_source::CodeSymbol;
use crate::appearance::Appearance;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::{safe_truncate, styles};
use crate::search::item::{IconLocation, SearchItem};
use crate::search::result_renderer::ItemHighlightState;
const MAX_COMBINED_LENGTH: usize = 55;
@@ -1,11 +1,13 @@
use std::collections::HashSet;
use fuzzy_match::FuzzyMatchResult;
use warpui::{AppContext, SingletonEntity};
use super::search_item::CommandSearchItem;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::terminal::History;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{AppContext, SingletonEntity};
use std::collections::HashSet;
const MAX_RESULTS: usize = 50;
@@ -1,13 +1,12 @@
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use warpui::elements::{ConstrainedBox, Container, Icon, Text};
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{
elements::{ConstrainedBox, Container, Icon, Text},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
#[derive(Clone, Debug)]
pub struct CommandSearchItem {
@@ -1,3 +1,8 @@
use std::collections::HashSet;
use fuzzy_match::FuzzyMatchResult;
use warpui::{AppContext, Entity, SingletonEntity};
use super::search_item::ConversationSearchItem;
use super::ConversationContextItem;
use crate::ai::agent_conversations_model::AgentConversationsModel;
@@ -5,9 +10,6 @@ use crate::ai::conversation_navigation::ConversationNavigationData;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{AppContext, Entity, SingletonEntity};
use std::collections::HashSet;
const MAX_RESULTS: usize = 50;
/// Minimum fuzzy match score to include a conversation in filtered results.
@@ -1,5 +1,10 @@
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use warpui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::{AppContext, Element, SingletonEntity};
use super::ConversationContextItem;
use crate::appearance::Appearance;
@@ -9,11 +14,6 @@ use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use crate::util::time_format::format_approx_duration_from_now_utc;
use crate::util::truncation::truncate_from_end;
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
const MAX_TITLE_LENGTH: usize = 45;
@@ -1,10 +1,10 @@
use warpui::AppContext;
use super::search_item::DiffSetSearchItem;
use crate::code_review::diff_state::DiffMode;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use galaxyui::AppContext;
const UNCOMMITTED_CHANGES_NAME: &str = "uncommitted changes";
const MAIN_BRANCH_CHANGES_NAME: &str = "changes vs. main branch";
@@ -1,9 +1,3 @@
use crate::appearance::Appearance;
use crate::code_review::diff_state::DiffMode;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::styles;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Icon, ParentElement, Text,
@@ -11,6 +5,13 @@ use galaxyui::elements::{
use galaxyui::{AppContext, Element, SingletonEntity};
use ordered_float::OrderedFloat;
use crate::appearance::Appearance;
use crate::code_review::diff_state::DiffMode;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::styles;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
#[derive(Debug, Clone)]
pub struct DiffSetSearchItem {
pub diff_mode: DiffMode,
@@ -1,5 +1,17 @@
#![cfg_attr(not(feature = "local_fs"), allow(dead_code))]
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use futures_lite::future::yield_now;
use fuzzy_match::FuzzyMatchResult;
use itertools::Itertools;
#[cfg(feature = "local_fs")]
use repo_metadata::repositories::DetectedRepositories;
use warpui::{AppContext, SingletonEntity};
use super::search_item::FileSearchItem;
#[cfg(feature = "local_fs")]
use crate::code::opened_files::OpenedFilesModel;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::async_snapshot_data_source::AsyncSnapshotDataSource;
@@ -7,16 +19,8 @@ use crate::search::data_source::{Query, QueryResult};
use crate::search::files::model::FileSearchModel;
use crate::search::files::search_item::FileSearchResult;
use crate::search::mixer::{BoxFuture, DataSourceRunErrorWrapper};
#[cfg(feature = "local_fs")]
use crate::workspace::ActiveSession;
use futures_lite::future::yield_now;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{AppContext, SingletonEntity};
use itertools::Itertools;
use repo_metadata::repositories::DetectedRepositories;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
const MAX_RESULTS: usize = 200;
@@ -58,7 +62,7 @@ pub fn file_data_source_for_current_repo(
last_opened,
}
} else {
let contents = file_search_model.get_repo_contents(app);
let contents = file_search_model.get_repo_contents(&query.text, app);
FileSnapshot {
contents,
git_changed_files: HashSet::new(),
@@ -104,33 +108,36 @@ pub fn file_data_source_for_pwd(
/// Captures last-opened timestamps from `OpenedFilesModel` for the active
/// repo at snapshot time. Returns an empty map when no repo is active.
#[cfg(feature = "local_fs")]
fn snapshot_last_opened(app: &AppContext) -> HashMap<String, instant::Instant> {
let git_repo_path = app
let repo_root = app
.windows()
.state()
.active_window
.and_then(|window_id| ActiveSession::as_ref(app).path_if_local(window_id))
.and_then(|current_dir| {
DetectedRepositories::as_ref(app).get_root_for_path(Path::new(current_dir))
});
.and_then(|window_id| ActiveSession::as_ref(app).working_directory(window_id))
.and_then(|working_dir| DetectedRepositories::as_ref(app).get_root_for_path(working_dir));
let Some(repo_path) = git_repo_path else {
let Some(repo_root) = repo_root else {
return HashMap::new();
};
let opened_files_model = OpenedFilesModel::as_ref(app);
let Some(opened_in_repo) = opened_files_model.opened_files_for_repo(&repo_path) else {
let Some(opened_in_repo) = opened_files_model.opened_files_for_repo(&repo_root) else {
return HashMap::new();
};
// Convert PathBuf keys to String keys matching FileSearchResult.path
// (relative paths from repo root).
opened_in_repo
.iter()
.map(|(path, ts)| (path.to_string_lossy().to_string(), *ts))
.map(|(path, ts)| (path.clone(), *ts))
.collect()
}
/// File-open recency is unavailable without a local filesystem.
#[cfg(not(feature = "local_fs"))]
fn snapshot_last_opened(_app: &AppContext) -> HashMap<String, instant::Instant> {
HashMap::new()
}
/// Routes file matching to zero-state ranking or query-based fuzzy scoring.
pub(crate) fn fuzzy_match_files(
snapshot: FileSnapshot,
@@ -272,3 +279,7 @@ async fn fuzzy_match_files_query(
.k_largest_relaxed_by_key(MAX_RESULTS, |item| item.score())
.collect()
}
#[cfg(test)]
#[path = "data_source_tests.rs"]
mod tests;
@@ -1,27 +1,28 @@
use crate::search::{
ai_context_menu::{
files::data_source::{file_data_source_for_pwd, fuzzy_match_files, FileSnapshot},
mixer::AIContextMenuSearchableAction,
},
data_source::Query,
files::{model::FileSearchModel, search_item::FileSearchResult},
item::SearchItem,
mixer::AsyncDataSource,
};
use crate::{terminal::model::session::Session, workspace::ActiveSession};
use galaxyui::platform::WindowStyle;
use galaxyui::r#async::block_on;
use galaxyui::windowing::WindowManager;
use galaxyui::SingletonEntity;
use galaxyui::{elements::Empty, App, AppContext, Element, Entity, TypedActionView, View};
use repo_metadata::repositories::DetectedRepositories;
use repo_metadata::RepoMetadataModel;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use repo_metadata::repositories::DetectedRepositories;
use repo_metadata::RepoMetadataModel;
use tempfile::tempdir;
use galaxyui::elements::Empty;
use galaxyui::platform::WindowStyle;
use galaxyui::r#async::block_on;
use galaxyui::windowing::WindowManager;
use galaxyui::{App, AppContext, Element, Entity, SingletonEntity, TypedActionView, View};
use crate::search::ai_context_menu::files::data_source::{
file_data_source_for_pwd, fuzzy_match_files, FileSnapshot,
};
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::Query;
use crate::search::files::model::FileSearchModel;
use crate::search::files::search_item::FileSearchResult;
use crate::search::item::SearchItem;
use crate::search::mixer::AsyncDataSource;
use crate::terminal::model::session::Session;
use crate::workspace::ActiveSession;
struct TestView;
impl Entity for TestView {
@@ -406,9 +407,10 @@ fn test_path_proximity_ranking() {
#[test]
fn test_directory_search_support() {
use crate::search::ai_context_menu::files::search_item::FileSearchItem;
use fuzzy_match::FuzzyMatchResult;
use crate::search::ai_context_menu::files::search_item::FileSearchItem;
// Test that directories can be created with is_directory flag
let directory_item = FileSearchItem {
path: PathBuf::from("src/components/"),
@@ -434,10 +436,7 @@ fn test_directory_search_support() {
#[test]
fn test_directory_action_type() {
use crate::search::ai_context_menu::files::search_item::FileSearchItem;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::item::SearchItem;
use fuzzy_match::FuzzyMatchResult;
let directory_item = FileSearchItem {
path: PathBuf::from("src/components/"),
@@ -520,7 +519,7 @@ fn test_mixed_file_directory_search() {
("src/components/ui", true),
("src/components/ui/modal.rs", false),
("tests/components", true),
("tests/components/button_test.rs", false),
("tests/components/button_tests.rs", false),
];
let query = "components";
@@ -1,5 +1,2 @@
pub mod data_source;
pub mod search_item;
#[cfg(test)]
mod data_source_tests;
@@ -1,17 +1,17 @@
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use std::fmt::Debug;
use std::path::PathBuf;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use warpui::elements::{ConstrainedBox, Container, Icon};
use warpui::{AppContext, Element};
use crate::appearance::Appearance;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::styles;
use crate::search::files::icon::icon_from_file_path;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use galaxyui::elements::{ConstrainedBox, Container, Icon};
use galaxyui::{AppContext, Element};
use crate::search::files::icon::icon_from_file_path;
use crate::ui_components::render_file_search_row::{render_file_search_row, FileSearchRowOptions};
#[derive(Debug)]
@@ -1,3 +1,6 @@
use fuzzy_match::FuzzyMatchResult;
use warpui::{AppContext, SingletonEntity};
use super::search_item::NotebookSearchItem;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::CloudModelType;
@@ -6,8 +9,6 @@ use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::workspaces::user_workspaces::UserWorkspaces;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{AppContext, SingletonEntity};
const MAX_RESULTS: usize = 50;
/// Base score for zero-state results. Each item gets an additional bonus based on
@@ -155,3 +156,7 @@ impl SyncDataSource for NotebookDataSource {
impl galaxyui::Entity for NotebookDataSource {
type Event = ();
}
#[cfg(test)]
#[path = "data_source_tests.rs"]
mod tests;
@@ -0,0 +1,250 @@
use std::sync::Arc;
use chrono::{Duration, Utc};
use cloud_object_client::MockObjectClient;
use settings::manager::SettingsManager;
use warpui::{App, SingletonEntity};
use crate::auth::AuthStateProvider;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::model::view::CloudViewModel;
use crate::cloud_object::{Owner, Revision, ServerMetadata, ServerNotebook, ServerPermissions};
use crate::notebooks::manager::NotebookManager;
use crate::notebooks::CloudNotebookModel;
use crate::search::ai_context_menu::notebooks::data_source::NotebookDataSource;
use crate::search::data_source::Query;
use crate::search::mixer::SyncDataSource;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::ids::{ServerId, SyncId};
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::server::server_api::ServerApiProvider;
use crate::server::sync_queue::SyncQueue;
use crate::settings::AISettings;
use crate::system::SystemStats;
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::user_profiles::UserProfiles;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::NetworkStatus;
fn mock_server_notebook_with_revision(id: i64, title: &str, revision: Revision) -> ServerNotebook {
ServerNotebook::new(
SyncId::ServerId(id.into()),
CloudNotebookModel {
title: title.to_string(),
data: format!("{title} content"),
ai_document_id: None,
conversation_id: None,
},
ServerMetadata {
uid: ServerId::default(),
revision,
metadata_last_updated_ts: Utc::now().into(),
trashed_ts: None,
folder_id: None,
is_welcome_object: false,
creator_uid: None,
last_editor_uid: None,
current_editor_uid: None,
},
ServerPermissions {
space: Owner::mock_current_user(),
guests: Vec::new(),
anyone_link_sharing: None,
permissions_last_updated_ts: Utc::now().into(),
},
)
}
fn initialize_app(app: &mut App) {
app.add_singleton_model(|_| NetworkStatus::new());
app.add_singleton_model(|_| SystemStats::new());
let mock_team_client = Arc::new(MockTeamClient::new());
let mock_workspace_client = Arc::new(MockWorkspaceClient::new());
app.add_singleton_model(|ctx| {
UserWorkspaces::mock(
mock_team_client.clone(),
mock_workspace_client.clone(),
vec![],
ctx,
)
});
app.add_singleton_model(TeamTesterStatus::new);
app.add_singleton_model(SyncQueue::mock);
app.add_singleton_model(CloudModel::mock);
app.add_singleton_model(|ctx| UpdateManager::new(None, Arc::new(MockObjectClient::new()), ctx));
app.add_singleton_model(|_| UserProfiles::new(Vec::new()));
app.add_singleton_model(CloudViewModel::new);
app.add_singleton_model(NotebookManager::mock);
app.add_singleton_model(|_| ServerApiProvider::new_for_test());
app.add_singleton_model(|_| SettingsManager::default());
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.update(crate::settings::init_and_register_user_preferences);
app.update(AISettings::register_and_subscribe_to_events);
}
#[test]
fn zero_state_scores_reflect_recency() {
App::test((), |mut app| async move {
initialize_app(&mut app);
let now = Utc::now();
CloudModel::handle(&app).update(&mut app, |model, ctx| {
model.upsert_from_server_notebook(
mock_server_notebook_with_revision(
1,
"oldest",
(now - Duration::minutes(3)).into(),
),
ctx,
);
model.upsert_from_server_notebook(
mock_server_notebook_with_revision(
2,
"middle",
(now - Duration::minutes(2)).into(),
),
ctx,
);
model.upsert_from_server_notebook(
mock_server_notebook_with_revision(
3,
"newest",
(now - Duration::minutes(1)).into(),
),
ctx,
);
});
let data_source = NotebookDataSource::new(false);
let results = app.read(|app| data_source.run_query(&Query::from(""), app).unwrap());
assert_eq!(results.len(), 3);
// run_query sorts descending by score, so first result should be newest
let scores: Vec<_> = results.iter().map(|r| r.score()).collect();
assert!(
scores[0] > scores[1] && scores[1] > scores[2],
"Expected scores in strictly descending order (newest first), got {scores:?}"
);
})
}
#[test]
fn filtered_state_adds_recency_bonus_to_equal_matches() {
App::test((), |mut app| async move {
initialize_app(&mut app);
let now = Utc::now();
// All titles contain "plan" so fuzzy scores should be similar
CloudModel::handle(&app).update(&mut app, |model, ctx| {
model.upsert_from_server_notebook(
mock_server_notebook_with_revision(
1,
"my first plan",
(now - Duration::minutes(3)).into(),
),
ctx,
);
model.upsert_from_server_notebook(
mock_server_notebook_with_revision(
2,
"my second plan",
(now - Duration::minutes(2)).into(),
),
ctx,
);
model.upsert_from_server_notebook(
mock_server_notebook_with_revision(
3,
"my third plan",
(now - Duration::minutes(1)).into(),
),
ctx,
);
});
let data_source = NotebookDataSource::new(false);
let results = app.read(|app| data_source.run_query(&Query::from("plan"), app).unwrap());
assert_eq!(results.len(), 3);
// All match "plan" similarly; recency bonus should make newer items score higher
let scores: Vec<_> = results.iter().map(|r| r.score()).collect();
assert!(
scores[0] > scores[1] && scores[1] > scores[2],
"Expected scores in strictly descending order (newest first), got {scores:?}"
);
})
}
#[test]
fn test_multibyte_character_truncation() {
// Test string with multibyte characters (emojis, accented chars)
let test_content = "This is a test with emojis 🚀 and accented chars like café and naïve that should be truncated properly without panicking. This string is intentionally long to test the 200 character limit and ensure we don't slice in the middle of multibyte characters like 你好世界";
let truncated = if test_content.len() > 200 {
let result = test_content
.char_indices()
.take_while(|(i, _)| *i <= 197)
.last()
.map(|(i, c)| &test_content[..i + c.len_utf8()])
.unwrap_or("");
format!("{result}...")
} else {
test_content.to_string()
};
// Should not panic and should produce a valid string
assert!(!truncated.is_empty());
assert!(truncated.ends_with("..."));
// The truncated string should be valid UTF-8
assert!(std::str::from_utf8(truncated.as_bytes()).is_ok());
}
#[test]
fn test_truncation_with_boundary_at_multibyte_char() {
// Create a string where byte 197 falls exactly in the middle of a multibyte character
let mut test_content = "a".repeat(195); // 195 single-byte chars
test_content.push('🚀'); // 4-byte emoji at positions 195-198
test_content.push_str("more text after emoji");
// This should not panic even though byte 197 is in the middle of the emoji
let truncated = if test_content.len() > 200 {
let result = test_content
.char_indices()
.take_while(|(i, _)| *i <= 197)
.last()
.map(|(i, c)| &test_content[..i + c.len_utf8()])
.unwrap_or("");
format!("{result}...")
} else {
test_content.to_string()
};
// Should not panic and should produce a valid string
assert!(!truncated.is_empty());
// The truncated string should be valid UTF-8
assert!(std::str::from_utf8(truncated.as_bytes()).is_ok());
// Should either include the full emoji or stop before it
assert!(!truncated.contains("🚀") || truncated.contains("🚀..."));
}
#[test]
fn test_short_content_not_truncated() {
let short_content = "This is a short string with emoji 🚀";
let result = if short_content.len() > 200 {
let truncated = short_content
.char_indices()
.take_while(|(i, _)| *i <= 197)
.last()
.map(|(i, c)| &short_content[..i + c.len_utf8()])
.unwrap_or("");
format!("{truncated}...")
} else {
short_content.to_string()
};
// Short content should not be truncated
assert_eq!(result, short_content);
assert!(!result.ends_with("..."));
}
@@ -1,5 +1,2 @@
pub mod data_source;
pub mod search_item;
#[cfg(test)]
mod data_source_test;
@@ -1,19 +1,20 @@
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use std::fmt::Debug;
use crate::appearance::Appearance;
use crate::cloud_object::ObjectType;
use crate::search::ai_context_menu::styles;
use crate::search::ai_context_menu::{mixer::AIContextMenuSearchableAction, safe_truncate};
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::cloud_object::ObjectType;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::{safe_truncate, styles};
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
const MAX_COMBINED_LENGTH: usize = 55;
#[derive(Debug)]
@@ -1,3 +1,6 @@
use fuzzy_match::FuzzyMatchResult;
use warpui::{AppContext, Entity, SingletonEntity};
use super::search_item::RuleSearchItem;
use crate::ai::facts::{AIFact, CloudAIFactModel};
use crate::cloud_object::model::generic_string_model::GenericStringObjectId;
@@ -6,8 +9,6 @@ use crate::cloud_object::CloudObject;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{AppContext, Entity, SingletonEntity};
const MAX_RESULTS: usize = 50;
const ZERO_STATE_BASE_SCORE: i64 = 1000;
@@ -1,7 +1,7 @@
use std::sync::Arc;
use chrono::{Duration, Utc};
use galaxyui::{App, SingletonEntity};
use cloud_object_client::MockObjectClient;
use settings::manager::SettingsManager;
use crate::ai::facts::{AIFact, AIMemory, CloudAIFactModel};
@@ -18,6 +18,8 @@ use crate::search::data_source::Query;
use crate::search::mixer::SyncDataSource;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::ids::{ServerId, SyncId};
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::server::server_api::ServerApiProvider;
use crate::server::sync_queue::SyncQueue;
use crate::settings::AISettings;
@@ -27,19 +29,21 @@ use crate::workspaces::user_profiles::UserProfiles;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::NetworkStatus;
use crate::server::server_api::object::MockObjectClient;
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
type ServerAIFact = GenericServerObject<
crate::cloud_object::model::generic_string_model::GenericStringObjectId,
CloudAIFactModel,
>;
fn mock_server_ai_fact(id: i64, name: &str, content: &str, revision: Revision) -> ServerAIFact {
GenericServerObject {
id: SyncId::ServerId(id.into()),
metadata: ServerMetadata {
GenericServerObject::new(
SyncId::ServerId(id.into()),
GenericStringModel::new(AIFact::Memory(AIMemory {
name: Some(name.to_string()),
content: content.to_string(),
is_autogenerated: false,
suggested_logging_id: None,
})),
ServerMetadata {
uid: ServerId::default(),
revision,
metadata_last_updated_ts: Utc::now().into(),
@@ -50,21 +54,13 @@ fn mock_server_ai_fact(id: i64, name: &str, content: &str, revision: Revision) -
last_editor_uid: None,
current_editor_uid: None,
},
permissions: ServerPermissions {
ServerPermissions {
space: Owner::mock_current_user(),
guests: Vec::new(),
anyone_link_sharing: None,
permissions_last_updated_ts: Utc::now().into(),
},
model: GenericStringModel {
string_model: AIFact::Memory(AIMemory {
name: Some(name.to_string()),
content: content.to_string(),
is_autogenerated: false,
suggested_logging_id: None,
}),
},
}
)
}
fn initialize_app(app: &mut App) {
@@ -1,19 +1,20 @@
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use std::fmt::Debug;
use crate::appearance::Appearance;
use crate::cloud_object::{GenericStringObjectFormat, JsonObjectType, ObjectType};
use crate::search::ai_context_menu::styles;
use crate::search::ai_context_menu::{mixer::AIContextMenuSearchableAction, safe_truncate};
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::cloud_object::{GenericStringObjectFormat, JsonObjectType, ObjectType};
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::{safe_truncate, styles};
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
const MAX_COMBINED_LENGTH: usize = 55;
#[derive(Debug)]
@@ -1,12 +1,12 @@
use fuzzy_match::FuzzyMatchResult;
use warp_util::local_or_remote_path::LocalOrRemotePath;
use warpui::{AppContext, Entity, SingletonEntity};
use super::search_item::SkillSearchItem;
use crate::ai::skills::SkillManager;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{AppContext, Entity, SingletonEntity};
use std::path::PathBuf;
#[cfg(not(target_family = "wasm"))]
use crate::workspace::ActiveSession;
@@ -31,23 +31,19 @@ impl SyncDataSource for SkillsDataSource {
let query_text = &query.text;
// Resolve the current working directory from the active window's session.
let cwd: Option<PathBuf> = {
#[cfg(not(target_family = "wasm"))]
{
app.windows()
.state()
.active_window
.and_then(|window_id| ActiveSession::as_ref(app).path_if_local(window_id))
.map(PathBuf::from)
}
#[cfg(target_family = "wasm")]
{
None
}
};
let skills =
SkillManager::as_ref(app).get_skills_for_working_directory(cwd.as_deref(), app);
#[cfg(not(target_family = "wasm"))]
let cwd: Option<LocalOrRemotePath> = app
.windows()
.state()
.active_window
.map(|window_id| {
let active_session = ActiveSession::as_ref(app);
active_session.working_directory(window_id).cloned()
})
.unwrap_or(None);
#[cfg(target_family = "wasm")]
let cwd: Option<LocalOrRemotePath> = None;
let skills = SkillManager::as_ref(app).get_skills_for_working_directory(cwd.as_ref(), app);
let mut results: Vec<QueryResult<Self::Action>> = if query_text.is_empty() {
// Zero state: show all skills with a uniform high score.
@@ -1,12 +1,6 @@
use ai::skills::SkillProvider;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use crate::appearance::Appearance;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::styles;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use galaxy_core::ui::icons::Icon;
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, ParentElement, Shrinkable, Text,
@@ -14,6 +8,12 @@ use galaxyui::elements::{
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::styles;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
const MAX_DESCRIPTION_LEN: usize = 60;
#[derive(Debug)]
+35 -52
View File
@@ -1,5 +1,30 @@
use std::collections::HashSet;
use std::ops::Range;
use std::time::Duration;
use async_channel::Sender;
use itertools::Itertools;
#[cfg(not(target_family = "wasm"))]
use repo_metadata::repositories::DetectedRepositories;
use settings::Setting as _;
use galaxy_core::features::FeatureFlag;
use warpui::elements::{
AnchorPair, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Dismiss, Empty, Fill, Flex, Hoverable, Icon, MouseStateHandle, OffsetPositioning, OffsetType,
ParentElement, PositionedElementOffsetBounds, PositioningAxis, Radius, SavePosition,
ScrollStateHandle, Scrollable, ScrollableElement, ScrollbarWidth, Shrinkable, Stack, Text,
UniformList, UniformListState, XAxisAnchor, YAxisAnchor,
};
use warpui::platform::Cursor;
use warpui::windowing::WindowManager;
use warpui::{
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use super::styles;
use crate::appearance::Appearance;
use crate::debounce::debounce;
use crate::debounce;
use crate::drive::settings::WarpDriveSettings;
#[cfg(not(target_family = "wasm"))]
use crate::search::ai_context_menu::blocks::data_source::BlockDataSource;
@@ -16,8 +41,7 @@ use crate::search::ai_context_menu::diffset::data_source::DiffSetDataSource;
use crate::search::ai_context_menu::files::data_source::{
file_data_source_for_current_repo, file_data_source_for_pwd,
};
use crate::search::ai_context_menu::mixer::AIContextMenuMixer;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::mixer::{AIContextMenuMixer, AIContextMenuSearchableAction};
#[cfg(not(target_family = "wasm"))]
use crate::search::ai_context_menu::notebooks::data_source::NotebookDataSource;
#[cfg(not(target_family = "wasm"))]
@@ -26,55 +50,14 @@ use crate::search::ai_context_menu::rules::data_source::RulesDataSource;
use crate::search::ai_context_menu::skills::data_source::SkillsDataSource;
#[cfg(not(target_family = "wasm"))]
use crate::search::ai_context_menu::workflows::data_source::WorkflowDataSource;
use crate::search::data_source::QueryResult;
use crate::search::data_source::{Query, QueryFilter};
use crate::search::data_source::{Query, QueryFilter, QueryResult};
#[cfg(not(target_family = "wasm"))]
use crate::search::mixer::AddAsyncSourceOptions;
use crate::search::result_renderer::{QueryResultRenderer, QueryResultRendererStyles};
use crate::search::search_bar::{SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering};
use crate::settings::InputSettings;
use async_channel::Sender;
use galaxy_core::features::FeatureFlag;
use galaxyui::elements::ConstrainedBox;
use galaxyui::elements::CrossAxisAlignment;
use galaxyui::elements::Empty;
use galaxyui::elements::Fill;
use galaxyui::elements::Hoverable;
use galaxyui::elements::MouseStateHandle;
use galaxyui::elements::ScrollStateHandle;
use galaxyui::elements::Scrollable;
use galaxyui::elements::ScrollableElement;
use galaxyui::elements::ScrollbarWidth;
use galaxyui::elements::UniformList;
use galaxyui::elements::UniformListState;
use galaxyui::elements::{
AnchorPair, Border, ChildView, Container, CornerRadius, Dismiss, Flex, Icon, OffsetPositioning,
OffsetType, ParentElement, PositionedElementOffsetBounds, PositioningAxis, Radius,
SavePosition, Shrinkable, Stack, Text, XAxisAnchor, YAxisAnchor,
};
use itertools::Itertools;
use settings::Setting as _;
use std::collections::HashSet;
use std::ops::Range;
use std::time::Duration;
use galaxyui::platform::Cursor;
use galaxyui::windowing::WindowManager;
use galaxyui::SingletonEntity;
use galaxyui::View;
use galaxyui::{
AppContext, Element, Entity, ModelHandle, TypedActionView, ViewContext, ViewHandle,
WeakViewHandle,
};
#[cfg(not(target_family = "wasm"))]
use crate::workspace::ActiveSession;
#[cfg(not(target_family = "wasm"))]
use repo_metadata::repositories::DetectedRepositories;
#[cfg(not(target_family = "wasm"))]
use std::path::Path;
use super::styles;
const CORNER_RADIUS: f32 = 8.0;
const DEFAULT_PALETTE_WIDTH: f32 = 320.0;
@@ -402,13 +385,13 @@ impl AIContextMenu {
#[cfg(not(target_family = "wasm"))]
{
let active_window_id = app.windows().state().active_window;
let active_dir = active_window_id
.and_then(|window_id| ActiveSession::as_ref(app).path_if_local(window_id));
active_dir.is_some_and(|dir| {
DetectedRepositories::as_ref(app)
.get_root_for_path(Path::new(dir))
.is_some()
})
active_window_id
.and_then(|window_id| ActiveSession::as_ref(app).working_directory(window_id))
.is_some_and(|dir| {
DetectedRepositories::as_ref(app)
.get_root_for_canonical_path(dir)
.is_some()
})
}
};
@@ -1,3 +1,6 @@
use fuzzy_match::FuzzyMatchResult;
use warpui::{AppContext, SingletonEntity};
use super::search_item::WorkflowSearchItem;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::CloudModelType;
@@ -5,8 +8,6 @@ use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::workspaces::user_workspaces::UserWorkspaces;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{AppContext, SingletonEntity};
const MAX_RESULTS: usize = 50;
/// Base score for zero-state results. Each item gets an additional bonus based on
@@ -1,19 +1,20 @@
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use std::fmt::Debug;
use crate::appearance::Appearance;
use crate::cloud_object::ObjectType;
use crate::search::ai_context_menu::styles;
use crate::search::ai_context_menu::{mixer::AIContextMenuSearchableAction, safe_truncate};
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::cloud_object::ObjectType;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::ai_context_menu::{safe_truncate, styles};
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
const MAX_COMBINED_LENGTH: usize = 55;
#[derive(Debug)]
@@ -1,3 +1,8 @@
use std::collections::HashMap;
use itertools::Itertools;
use warpui::{AppContext, Entity};
use crate::ai::agent::conversation::{AIConversation, AIConversationId};
use crate::ai::conversation_navigation::ConversationNavigationData;
use crate::search::command_palette::conversations::search::{
@@ -12,9 +17,6 @@ use crate::search::data_source::{DataSourceSearchError, Query, QueryResult};
use crate::search::mixer::DataSourceRunErrorWrapper;
use crate::search::SyncDataSource;
use crate::workspace::Workspace;
use galaxyui::{AppContext, Entity};
use itertools::Itertools;
use std::collections::HashMap;
/// Sections for grouping conversations in the command palette.
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -75,13 +77,6 @@ impl DataSource {
}
}
pub fn historical() -> Self {
Self {
searcher: FuzzyConversationSearcher::historical(),
add_conversation_actions: false,
}
}
/// Returns a [`QueryResult`] for a conversation identified by `conversation_id`. `None` if no result was
/// found with the given ID.
pub fn query_result(
@@ -200,9 +195,7 @@ impl SyncDataSource for DataSource {
self.searcher
.search(&query.text.trim().to_lowercase(), app)
.map_err(|err| {
let search_error = DataSourceSearchError {
message: err.to_string(),
};
let search_error = DataSourceSearchError::new(err.to_string());
Box::new(search_error) as DataSourceRunErrorWrapper
})
};
@@ -2,8 +2,6 @@ mod data_source;
mod search;
mod search_item;
#[cfg(test)]
mod search_test;
pub use data_source::DataSource;
pub use crate::ai::conversation_navigation::ConversationNavigationData;
pub use data_source::DataSource;
@@ -1,11 +1,13 @@
use fuzzy_match::match_indices_case_insensitive;
use warpui::AppContext;
use crate::ai::conversation_navigation::ConversationNavigationData;
use crate::search::command_palette::conversations::search_item::ConversationAction;
use crate::search::command_palette::conversations::search_item::ConversationSearchItem;
use crate::search::command_palette::conversations::search_item::{
ConversationAction, ConversationSearchItem,
};
use crate::search::command_palette::conversations::DataSource;
use crate::search::data_source::QueryResult;
use crate::search::SyncDataSource;
use fuzzy_match::match_indices_case_insensitive;
use galaxyui::AppContext;
/// A conversation that was fuzzy matched against a search term.
#[derive(Debug)]
@@ -178,36 +180,15 @@ pub trait ConversationSearcher {
) -> anyhow::Result<Vec<QueryResult<SearcherAction>>>;
}
#[derive(PartialEq)]
pub enum ConversationType {
All,
Historical,
}
pub struct FuzzyConversationSearcher {
filter: ConversationType,
}
pub struct FuzzyConversationSearcher;
impl FuzzyConversationSearcher {
pub fn new() -> Self {
Self {
filter: ConversationType::All,
}
}
pub fn historical() -> Self {
Self {
filter: ConversationType::Historical,
}
Self
}
pub fn searchable_conversations(&self, app: &AppContext) -> Vec<ConversationNavigationData> {
match self.filter {
ConversationType::Historical => {
ConversationNavigationData::historical_conversations(app)
}
ConversationType::All => ConversationNavigationData::all_conversations(app),
}
ConversationNavigationData::all_conversations(app)
}
}
@@ -228,3 +209,7 @@ impl ConversationSearcher for FuzzyConversationSearcher {
.collect())
}
}
#[cfg(test)]
#[path = "search_tests.rs"]
mod tests;
@@ -1,3 +1,20 @@
use ordered_float::OrderedFloat;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use galaxy_core::ui::color::blend::Blend;
use galaxy_core::ui::color::coloru_with_opacity;
use galaxy_core::ui::icons::Icon;
use galaxy_core::ui::theme::color::internal_colors;
use warpui::elements::{
AnchorPair, Container, CrossAxisAlignment, Expanded, Fill, Flex, Highlight, MainAxisSize,
MouseStateHandle, OffsetPositioning, OffsetType, ParentElement, ParentOffsetBounds,
PositioningAxis, Stack, Text, XAxisAnchor, YAxisAnchor,
};
use warpui::fonts::{Properties, Weight};
use warpui::ui_components::button::ButtonTooltipPosition;
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
use warpui::{AppContext, Element, Gradient, SingletonEntity};
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::blocklist::history_model::BlocklistAIHistoryModel;
use crate::appearance::Appearance;
@@ -10,21 +27,6 @@ use crate::search::result_renderer::ItemHighlightState;
use crate::search::SearchItem;
use crate::ui_components::buttons::icon_button;
use crate::util::time_format::format_approx_duration_from_now;
use galaxy_core::ui::color::{blend::Blend, coloru_with_opacity};
use galaxy_core::ui::icons::Icon;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::elements::{
AnchorPair, Container, CrossAxisAlignment, Expanded, Fill, Flex, Highlight, MainAxisSize,
MouseStateHandle, OffsetPositioning, OffsetType, ParentElement, ParentOffsetBounds,
PositioningAxis, Stack, Text, XAxisAnchor, YAxisAnchor,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::ui_components::button::ButtonTooltipPosition;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{AppContext, Element, Gradient, SingletonEntity};
use ordered_float::OrderedFloat;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
/// Information about which action to take once the conversation item is accepted.
#[derive(Debug)]
@@ -202,7 +204,8 @@ impl ConversationSearchItem {
.finish();
// We only want to show the fork button if the conversation is completed
// (i.e. the agent has finished responding and there are no blocked commands).
// (i.e. the agent has finished responding and there are no blocked commands
// and is not yielded waiting for events).
let conversation_is_done = BlocklistAIHistoryModel::as_ref(app)
.conversation(&conversation.id())
.map(|c| c.status().is_done())
@@ -1,8 +1,8 @@
use crate::ai::{
agent::conversation::AIConversationId, conversation_navigation::ConversationNavigationData,
};
use galaxyui::{EntityId, WindowId};
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::conversation_navigation::ConversationNavigationData;
#[test]
fn test_conversation_navigation_data_ordering() {
// Create test data with different active states and timestamps
+58 -26
View File
@@ -1,28 +1,25 @@
use std::collections::HashSet;
use std::path::PathBuf;
use galaxy_core::context_flag::ContextFlag;
use galaxy_core::features::FeatureFlag;
use warpui::keymap::BindingId;
use warpui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
use super::{conversations, warp_drive};
use crate::drive::settings::WarpDriveSettings;
use crate::search::action::CommandBindingDataSource;
use crate::search::binding_source::BindingSource;
use crate::search::command_palette::files;
use crate::search::command_palette::launch_config;
use crate::search::command_palette::mixer::{CommandPaletteItemAction, ItemSummary};
use crate::search::command_palette::new_session::NewSessionDataSource;
use crate::search::command_palette::repos::RepoDataSource;
use crate::search::command_palette::{navigation, CommandPaletteMixer};
use crate::search::command_palette::{files, launch_config, navigation, tabs, CommandPaletteMixer};
use crate::search::data_source::QueryResult;
use crate::search::files::model::FileSearchModel;
use crate::search::mixer::AddAsyncSourceOptions;
use crate::search::QueryFilter;
use crate::session_management::SessionSource;
use crate::settings::AISettings;
use galaxy_core::context_flag::ContextFlag;
use galaxy_core::features::FeatureFlag;
use galaxyui::keymap::BindingId;
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
use super::conversations;
use super::warp_drive;
/// Store of all of the [`crate::search::DataSource`]s for the command palette.
pub struct DataSourceStore {
@@ -31,9 +28,9 @@ pub struct DataSourceStore {
warp_drive_data_source: ModelHandle<warp_drive::DataSource>,
launch_config_data_source: ModelHandle<launch_config::DataSource>,
new_session_data_source: Option<ModelHandle<NewSessionDataSource>>,
historical_conversation_data_source: ModelHandle<conversations::DataSource>,
all_conversation_data_source: ModelHandle<conversations::DataSource>,
repo_data_source: ModelHandle<RepoDataSource>,
tabs_data_source: Option<ModelHandle<tabs::DataSource>>,
}
impl DataSourceStore {
@@ -56,9 +53,6 @@ impl DataSourceStore {
&& cfg!(feature = "local_tty"))
.then_some(ctx.add_model(|ctx| NewSessionDataSource::new(binding_source, ctx)));
let historical_conversation_data_source: ModelHandle<conversations::DataSource> =
ctx.add_model(|_| conversations::DataSource::historical());
let all_conversation_data_source: ModelHandle<conversations::DataSource> =
ctx.add_model(|_| conversations::DataSource::new());
@@ -70,9 +64,9 @@ impl DataSourceStore {
warp_drive_data_source,
launch_config_data_source,
new_session_data_source,
historical_conversation_data_source,
all_conversation_data_source,
repo_data_source,
tabs_data_source: None,
}
}
@@ -128,8 +122,7 @@ impl DataSourceStore {
if FeatureFlag::CommandPaletteFileSearch.is_enabled() && !is_shared_session_viewer {
let file_search_model = FileSearchModel::as_ref(ctx);
let repo_root = file_search_model.repo_root(ctx);
let is_in_git_repo = repo_root.is_some();
let is_in_git_repo = file_search_model.repo_root_location(ctx).is_some();
let files_data_source = if is_in_git_repo {
ctx.add_model(|_| files::data_source::FileDataSource::new())
@@ -154,11 +147,6 @@ impl DataSourceStore {
self.all_conversation_data_source.clone(),
HashSet::from([QueryFilter::Conversations]),
);
mixer.add_sync_source(
self.historical_conversation_data_source.clone(),
HashSet::from([QueryFilter::HistoricalConversations]),
);
}
mixer.add_sync_source(
@@ -170,6 +158,45 @@ impl DataSourceStore {
});
}
/// Resets the [`CommandPaletteMixer`] to the set of data sources relevant for the Ctrl+Tab
/// palette, which shows tabs sorted by MRU order.
pub fn reset_ctrl_tab_mixer(
&mut self,
mixer: ModelHandle<CommandPaletteMixer>,
tabs: Vec<crate::session_management::TabNavigationData>,
ctx: &mut ModelContext<Self>,
) {
if self.tabs_data_source.is_none() {
self.tabs_data_source = Some(ctx.add_model(|_| tabs::DataSource::new()));
}
if let Some(tabs_data_source) = &self.tabs_data_source {
tabs_data_source.update(ctx, |ds, _| ds.set_tabs(tabs));
mixer.update(ctx, |mixer, ctx| {
mixer.reset(ctx);
mixer.add_sync_source(tabs_data_source.clone(), HashSet::from([QueryFilter::Tabs]));
ctx.notify();
});
}
}
/// Restores the [`CommandPaletteMixer`] to the sessions-only source for Ctrl+Tab,
/// undoing any previous `reset_ctrl_tab_mixer` call.
pub fn restore_ctrl_tab_session_mixer(
&self,
mixer: ModelHandle<CommandPaletteMixer>,
ctx: &mut ModelContext<Self>,
) {
mixer.update(ctx, |mixer, ctx| {
mixer.reset(ctx);
mixer.add_sync_source(
self.sessions_data_source.clone(),
HashSet::from([QueryFilter::Sessions]),
);
ctx.notify();
});
}
/// Returns a [`QueryResult`] from the data sources identified by the `summary`. `None` if none
/// of the data sources contained an item with given summary.
pub fn query_result_from_summary(
@@ -219,9 +246,10 @@ impl DataSourceStore {
line_and_column_arg,
} => {
// Create a file search item from the summary
use crate::search::command_palette::files::search_item::FileSearchItem;
use fuzzy_match::FuzzyMatchResult;
use crate::search::command_palette::files::search_item::FileSearchItem;
let search_item = FileSearchItem {
path: PathBuf::from(path),
project_directory: project_directory.clone(),
@@ -236,8 +264,7 @@ impl DataSourceStore {
project_directory,
} => {
// Create a directory search item from the summary
use crate::search::command_palette::files::search_item::FileSearchItem;
use fuzzy_match::FuzzyMatchResult;
let search_item = FileSearchItem {
path: PathBuf::from(path),
@@ -272,6 +299,11 @@ impl DataSourceStore {
// No-op action (used for non-interactable separator items that don't do anything on click).
None
}
ItemSummary::Tab { .. } => {
// Tabs are only shown in the ctrl_tab palette, not in recent commands.
None
}
}
}
@@ -291,5 +323,5 @@ impl Entity for DataSourceStore {
}
#[cfg(test)]
#[path = "data_sources_test.rs"]
#[path = "data_sources_tests.rs"]
mod tests;
@@ -1,44 +1,34 @@
use std::sync::Arc;
use chrono::Utc;
use galaxyui::{App, SingletonEntity};
use cloud_object_client::MockObjectClient;
use settings::manager::SettingsManager;
use super::*;
use crate::auth::AuthStateProvider;
use crate::cloud_object::Owner;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::model::view::CloudViewModel;
use crate::cloud_object::{
Owner, Revision, ServerMetadata, ServerNotebook, ServerPermissions, ServerWorkflow,
};
use crate::network::NetworkStatus;
use crate::notebooks::manager::NotebookManager;
use crate::notebooks::CloudNotebookModel;
use crate::notebooks::{CloudNotebookModel, NotebookId};
use crate::search::data_source::Query;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::ids::ServerId;
use crate::server::ids::SyncId::{self};
use crate::settings::AISettings;
use crate::workflows::workflow::Workflow;
use crate::workflows::CloudWorkflowModel;
use crate::{
cloud_object::{
model::{persistence::CloudModel, view::CloudViewModel},
Revision, ServerMetadata, ServerNotebook, ServerPermissions, ServerWorkflow,
},
network::NetworkStatus,
notebooks::NotebookId,
search::data_source::Query,
server::{
cloud_objects::update_manager::UpdateManager, server_api::ServerApiProvider,
sync_queue::SyncQueue,
},
system::SystemStats,
workflows::WorkflowId,
workspaces::{
team_tester::TeamTesterStatus, user_profiles::UserProfiles, user_workspaces::UserWorkspaces,
},
};
#[cfg(test)]
use crate::server::server_api::object::MockObjectClient;
#[cfg(test)]
use crate::server::server_api::team::MockTeamClient;
#[cfg(test)]
use crate::server::server_api::workspace::MockWorkspaceClient;
use crate::server::server_api::ServerApiProvider;
use crate::server::sync_queue::SyncQueue;
use crate::settings::AISettings;
use crate::system::SystemStats;
use crate::workflows::workflow::Workflow;
use crate::workflows::{CloudWorkflowModel, WorkflowId};
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::user_profiles::UserProfiles;
use crate::workspaces::user_workspaces::UserWorkspaces;
fn mock_server_metadata() -> ServerMetadata {
ServerMetadata {
@@ -64,26 +54,26 @@ fn mock_server_permissions(owner: Owner) -> ServerPermissions {
}
fn mock_server_workflow(id: WorkflowId, owner: Owner) -> ServerWorkflow {
ServerWorkflow {
id: SyncId::ServerId(id.into()),
metadata: mock_server_metadata(),
permissions: mock_server_permissions(owner),
model: CloudWorkflowModel::new(Workflow::new(format!("foo{id}"), format!("bar{id}"))),
}
ServerWorkflow::new(
SyncId::ServerId(id.into()),
CloudWorkflowModel::new(Workflow::new(format!("foo{id}"), format!("bar{id}"))),
mock_server_metadata(),
mock_server_permissions(owner),
)
}
fn mock_server_notebook(id: NotebookId, owner: Owner) -> ServerNotebook {
ServerNotebook {
id: SyncId::ServerId(id.into()),
metadata: mock_server_metadata(),
permissions: mock_server_permissions(owner),
model: CloudNotebookModel {
ServerNotebook::new(
SyncId::ServerId(id.into()),
CloudNotebookModel {
title: format!("foo{id}"),
data: format!("bar{id}"),
ai_document_id: None,
conversation_id: None,
},
}
mock_server_metadata(),
mock_server_permissions(owner),
)
}
fn initialize_app(app: &mut App) {
@@ -1,23 +1,25 @@
#![cfg_attr(not(feature = "local_fs"), allow(dead_code))]
use super::search_item::{CreateFileSearchItem, FileSearchItem};
use crate::code::opened_files::OpenedFilesModel;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::files::model::FileSearchModel;
use crate::search::files::search_item::FileSearchResult;
use crate::search::mixer::{AsyncDataSource, BoxFuture, DataSourceRunErrorWrapper};
use futures_lite::FutureExt;
use fuzzy_match::FuzzyMatchResult;
use galaxy_util::path::CleanPathResult;
use galaxyui::{AppContext, Entity, SingletonEntity};
use instant::Instant;
use itertools::Itertools;
use std::collections::HashSet;
#[cfg(feature = "local_fs")]
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use futures_lite::FutureExt;
use fuzzy_match::FuzzyMatchResult;
use instant::Instant;
use itertools::Itertools;
use galaxy_util::path::CleanPathResult;
use galaxyui::{AppContext, Entity, SingletonEntity};
use super::search_item::{CreateFileSearchItem, FileSearchItem};
use crate::code::opened_files::{OpenedFilesInRepo, OpenedFilesModel};
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::data_source::{Query, QueryFilter, QueryResult};
use crate::search::files::model::FileSearchModel;
use crate::search::files::search_item::FileSearchResult;
use crate::search::mixer::{AsyncDataSource, BoxFuture, DataSourceRunErrorWrapper};
const MAX_RESULTS: usize = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@@ -82,7 +84,11 @@ impl AsyncDataSource for FileDataSource {
self.run_zero_state_query(app)
} else {
// Non-empty query: use fuzzy matching
self.run_fuzzy_search_query(app, query_text)
self.run_fuzzy_search_query(
app,
query_text,
query.filters.contains(&QueryFilter::Files),
)
}
}
}
@@ -103,11 +109,11 @@ impl FileDataSource {
}
}
fn contents(&self, app: &AppContext) -> Arc<Vec<FileSearchResult>> {
fn contents(&self, query: &str, app: &AppContext) -> Arc<Vec<FileSearchResult>> {
match &self.mode {
FileDataSourceMode::Repo => {
let file_search_model = FileSearchModel::as_ref(app);
file_search_model.get_repo_contents(app)
file_search_model.get_repo_contents(query, app)
}
FileDataSourceMode::CurrentFolder { cached_contents } => {
Arc::new(cached_contents.clone())
@@ -127,47 +133,54 @@ impl FileDataSource {
let (contents, git_changed_files) = self.contents_with_git_changes(app);
let mut results = Vec::new();
let opened_files = OpenedFilesModel::as_ref(app);
let repo_root = file_search_model.repo_root(app);
let opened_files =
repo_root.and_then(|repo_root| opened_files.opened_files_for_repo(&repo_root));
let repo_root = file_search_model.repo_root_location(app);
let opened_files = repo_root
.and_then(|repo_root| opened_files.opened_files_for_repo(&repo_root))
.cloned();
for item in contents.iter() {
let mut file_ranking = if git_changed_files.contains(&item.path) {
FileRanking::ChangedInGit
} else {
FileRanking::None
};
Box::pin(async move {
let mut results = Vec::new();
if let Some(last_opened_timestamp) =
opened_files.and_then(|opened_files| opened_files.get(&PathBuf::from(&item.path)))
{
file_ranking = FileRanking::OpenedInWarp {
timestamp: *last_opened_timestamp,
};
for chunk in contents.chunks(50) {
for item in chunk {
let mut file_ranking = if git_changed_files.contains(&item.path) {
FileRanking::ChangedInGit
} else {
FileRanking::None
};
if let Some(last_opened_timestamp) = opened_files
.as_ref()
.and_then(|of: &OpenedFilesInRepo| of.get(&item.path))
{
file_ranking = FileRanking::OpenedInWarp {
timestamp: *last_opened_timestamp,
};
}
let match_result = FuzzyMatchResult {
score: 0,
matched_indices: vec![], // No highlighting needed for zero state
};
let search_item = FileSearchItem {
path: PathBuf::from(&item.path),
project_directory: item.project_directory.clone(),
match_result,
line_and_column_arg: None,
is_directory: item.is_directory,
};
results.push((file_ranking, QueryResult::from(search_item)));
}
futures_lite::future::yield_now().await;
}
let match_result = FuzzyMatchResult {
score: 0,
matched_indices: vec![], // No highlighting needed for zero state
};
results.sort_by_key(|(ranking, _)| *ranking);
let search_item = FileSearchItem {
path: PathBuf::from(&item.path),
project_directory: item.project_directory.clone(),
match_result,
line_and_column_arg: None,
is_directory: item.is_directory,
};
results.push((file_ranking, QueryResult::from(search_item)));
}
results.sort_by_key(|(ranking, _)| *ranking);
Box::pin(async move { Ok(results.into_iter().map(|(_, ranking)| ranking).collect()) })
Ok(results.into_iter().map(|(_, ranking)| ranking).collect())
})
}
/// Handle non-empty query with fuzzy matching (no git status needed)
@@ -175,14 +188,13 @@ impl FileDataSource {
&self,
app: &AppContext,
query_text: &str,
allow_create_file: bool,
) -> BoxFuture<
'static,
Result<Vec<QueryResult<CommandPaletteItemAction>>, DataSourceRunErrorWrapper>,
> {
let file_search_model = FileSearchModel::as_ref(app);
let contents = self.contents(app);
// Strip any trailing : in case user is in the middle of typing a line / column arg.
let query_text = query_text.strip_suffix(':').unwrap_or(query_text);
@@ -191,7 +203,9 @@ impl FileDataSource {
let opened_files = OpenedFilesModel::as_ref(app);
#[cfg(feature = "local_fs")]
let repo_root = file_search_model.repo_root(app);
let repo_root_location = file_search_model.repo_root_location(app);
// For the "Create file" fallback, use the expanded (but not repo-root-stripped)
// path so that absolute paths work correctly with Path::join.
@@ -223,10 +237,15 @@ impl FileDataSource {
)
.unwrap_or(query_file_content);
let opened_files = repo_root
let opened_files = repo_root_location
.and_then(|repo_root| opened_files.opened_files_for_repo(&repo_root))
.cloned();
// Fetch contents using the finalized query so it is pushed down into
// the repo-metadata traversal as a filter (matching files are not
// truncated away before fuzzy matching).
let contents = self.contents(&query_file_content, app);
const CHUNK_SIZE: usize = 50;
Box::pin(async move {
@@ -249,7 +268,7 @@ impl FileDataSource {
if opened_files
.as_ref()
.and_then(|opened_files| opened_files.get(&PathBuf::from(&item.path)))
.and_then(|of: &OpenedFilesInRepo| of.get(&item.path))
.is_some()
{
// Apply a boost to opened files to rank them above non-opened files.
@@ -275,8 +294,8 @@ impl FileDataSource {
.collect();
// If no files matched and we have a valid query and current directory,
// add a "Create <filename>..." option
if results.is_empty() && !query_file_name.trim().is_empty() {
// add a "Create a file named <filename>..." option
if allow_create_file && results.is_empty() && !query_file_name.trim().is_empty() {
if let Some(current_dir) = current_directory {
let create_item = CreateFileSearchItem {
file_name: query_file_name,
@@ -1,19 +1,19 @@
use fuzzy_match::FuzzyMatchResult;
use galaxy_util::path::LineAndColumnArg;
use ordered_float::OrderedFloat;
use std::fmt::Debug;
use std::path::PathBuf;
use crate::appearance::Appearance;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::styles;
use crate::search::item::{IconLocation, SearchItem};
use crate::search::result_renderer::ItemHighlightState;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use galaxy_util::path::LineAndColumnArg;
use galaxyui::elements::{Align, ConstrainedBox, Container, Flex, Icon, ParentElement, Text};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::styles;
use crate::search::files::icon::icon_from_file_path;
use crate::search::item::{IconLocation, SearchItem};
use crate::search::result_renderer::ItemHighlightState;
use crate::ui_components::render_file_search_row::{render_file_search_row, FileSearchRowOptions};
#[derive(Debug)]
@@ -65,6 +65,7 @@ impl SearchItem for FileSearchItem {
FileSearchRowOptions {
match_result: Some(&self.match_result),
highlight_state,
max_combined_length: None,
..Default::default()
},
app,
@@ -160,7 +161,7 @@ impl SearchItem for CreateFileSearchItem {
let text_color = highlight_state.sub_text_fill(appearance).into_solid();
let label = Text::new_inline(
format!("Create {}", &self.file_name),
format!("Create a file named {}", &self.file_name),
appearance.ui_font_family(),
appearance.monospace_font_size(),
)
@@ -1,16 +1,16 @@
use crate::appearance::Appearance;
use crate::drive::cloud_object_styling::warp_drive_icon_color;
use crate::drive::DriveObjectType;
use crate::search::FilterChipRenderer as CommonFilterChipRenderer;
use crate::search::QueryFilter;
use crate::util::color::{ContrastingColor, MinimumAllowedContrast};
use pathfinder_color::ColorU;
use galaxyui::elements::{
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Hoverable, Icon,
MouseStateHandle, ParentElement, Radius, Text,
};
use galaxyui::platform::Cursor;
use galaxyui::{Element, EventContext};
use pathfinder_color::ColorU;
use crate::appearance::Appearance;
use crate::drive::cloud_object_styling::warp_drive_icon_color;
use crate::drive::DriveObjectType;
use crate::search::{FilterChipRenderer as CommonFilterChipRenderer, QueryFilter};
use crate::util::color::{ContrastingColor, MinimumAllowedContrast};
/// Trait to render filter chips for the command palette.
pub trait FilterChipRenderer: crate::search::FilterChipRenderer {
@@ -100,6 +100,7 @@ impl FilterChipRenderer for QueryFilter {
| QueryFilter::NaturalLanguage
| QueryFilter::Actions
| QueryFilter::Sessions
| QueryFilter::Tabs
| QueryFilter::Drive
| QueryFilter::LaunchConfigurations
| QueryFilter::PromptHistory
@@ -118,7 +119,7 @@ impl FilterChipRenderer for QueryFilter {
.theme()
.main_text_color(appearance.theme().surface_2())
.into_solid(),
QueryFilter::Conversations | QueryFilter::HistoricalConversations => appearance
QueryFilter::Conversations => appearance
.theme()
.main_text_color(appearance.theme().surface_2())
.into_solid(),
@@ -146,9 +147,10 @@ impl FilterChipRenderer for QueryFilter {
}
mod styles {
use crate::themes::theme::{Blend, Fill, GalaxyTheme};
use galaxyui::elements::{Border, MouseState};
use crate::themes::theme::{Blend, Fill, WarpTheme};
/// Size of the border when the query filter is hovered.
const HOVERED_BORDER_SIZE: f32 = 2.;
/// Size of the border when the query filter is _not_ hovered.
@@ -1,13 +1,15 @@
use std::collections::HashMap;
use std::sync::Arc;
use fuzzy_match::match_indices_case_insensitive;
use warpui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
use crate::launch_configs::launch_config::LaunchConfig;
use crate::search::command_palette::launch_config::search_item::SearchItem;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::data_source::{DataSourceSearchError, Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::user_config::{GalaxyConfig, GalaxyConfigUpdateEvent};
use fuzzy_match::match_indices_case_insensitive;
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity};
use std::collections::HashMap;
use std::sync::Arc;
use crate::user_config::{WarpConfig, WarpConfigUpdateEvent};
/// Datasource that searches against `LaunchConfig`s.
pub struct DataSource {
@@ -48,10 +50,11 @@ impl DataSource {
fn handle_config_event(
&mut self,
event: &GalaxyConfigUpdateEvent,
_: ModelHandle<WarpConfig>,
event: &WarpConfigUpdateEvent,
ctx: &mut ModelContext<Self>,
) {
if matches!(event, GalaxyConfigUpdateEvent::LaunchConfigs) {
if matches!(event, WarpConfigUpdateEvent::LaunchConfigs) {
self.searcher.refresh_search_index(ctx);
}
}
@@ -68,9 +71,7 @@ impl SyncDataSource for DataSource {
.searcher
.search(&query.text.trim().to_lowercase())
.map_err(|err| {
Box::new(DataSourceSearchError {
message: err.to_string(),
}) as DataSourceRunErrorWrapper
Box::new(DataSourceSearchError::new(err.to_string())) as DataSourceRunErrorWrapper
})?
.into_iter()
.map(QueryResult::from)
@@ -121,17 +122,15 @@ impl LaunchConfigSearcher for FuzzyLaunchConfigSearcher {
#[cfg(not(target_family = "wasm"))]
mod full_text_searcher {
use crate::define_search_schema;
use crate::launch_configs::launch_config::LaunchConfig;
use crate::search::command_palette::launch_config::data_source::LaunchConfigSearcher;
use crate::search::command_palette::launch_config::search_item::SearchItem;
use crate::search::searcher::{AsyncSearcher, DEFAULT_MEMORY_BUDGET, SCORE_CONVERSION_FACTOR};
use crate::user_config::GalaxyConfig;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::r#async::executor::Background;
use galaxyui::{AppContext, SingletonEntity};
use std::collections::HashMap;
use std::sync::Arc;
use warp_search_core::define_search_schema;
use warpui::r#async::executor::Background;
use warpui::{AppContext, SingletonEntity};
use crate::search::command_palette::launch_config::data_source::LaunchConfigSearcher;
use crate::search::searcher::{AsyncSearcher, DEFAULT_MEMORY_BUDGET, SCORE_CONVERSION_FACTOR};
use crate::user_config::WarpConfig;
// The name of the launch configs are duplicated to ensure that the searcher
// hashes the name to uniquely identify the launch config.
@@ -1,15 +1,11 @@
use galaxyui::{
elements::{
Align, Border, ConstrainedBox, Container, CornerRadius, Flex, Highlight, ParentElement,
Radius, Shrinkable, Text,
},
fonts::{Properties, Weight},
ui_components::{
components::{Coords, UiComponent, UiComponentStyles},
text::Span,
},
Element,
use galaxyui::elements::{
Align, Border, ConstrainedBox, Container, CornerRadius, Flex, Highlight, ParentElement, Radius,
Shrinkable, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use warpui::ui_components::text::Span;
use warpui::Element;
use crate::appearance::Appearance;
use crate::launch_configs::launch_config::LaunchConfig;
@@ -1,14 +1,15 @@
use crate::launch_configs::launch_config::LaunchConfig;
use crate::{appearance::Appearance, ui_components::icons::Icon};
use fuzzy_match::FuzzyMatchResult;
use std::sync::Arc;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::launch_configs::launch_config::LaunchConfig;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::render_util::render_search_item_icon;
use crate::search::result_renderer::ItemHighlightState;
use galaxyui::{AppContext, Element, SingletonEntity};
use ordered_float::OrderedFloat;
use std::sync::Arc;
use crate::ui_components::icons::Icon;
/// SearchItem for a matching [`LaunchConfig`].
#[derive(Debug)]
+18 -5
View File
@@ -1,3 +1,10 @@
use std::sync::Arc;
use strum_macros::IntoStaticStr;
use warp_util::path::LineAndColumnArg;
use warpui::keymap::BindingId;
use warpui::{EntityId, WindowId};
use crate::ai::agent::conversation::AIConversationId;
use crate::drive::CloudObjectTypeAndId;
use crate::launch_configs::launch_config::LaunchConfig;
@@ -6,11 +13,6 @@ use crate::search::mixer::SearchMixer;
use crate::server::ids::SyncId;
use crate::util::bindings::CommandBinding;
use crate::workspace::PaneViewLocator;
use galaxy_util::path::LineAndColumnArg;
use galaxyui::keymap::BindingId;
use galaxyui::{EntityId, WindowId};
use std::sync::Arc;
use strum_macros::IntoStaticStr;
pub type CommandPaletteMixer = SearchMixer<CommandPaletteItemAction>;
@@ -37,6 +39,11 @@ pub enum CommandPaletteItemAction {
pane_view_locator: PaneViewLocator,
window_id: WindowId,
},
/// Navigate to a specific tab identified by its pane_group EntityId.
NavigateToTab {
pane_group_id: EntityId,
window_id: WindowId,
},
/// Navigate to a specific conversation.
NavigateToConversation {
pane_view_locator: Option<PaneViewLocator>,
@@ -94,6 +101,9 @@ impl CommandPaletteItemAction {
} => ItemSummary::Session {
pane_view_locator: *pane_view_locator,
},
CommandPaletteItemAction::NavigateToTab { pane_group_id, .. } => ItemSummary::Tab {
pane_group_id: *pane_group_id,
},
CommandPaletteItemAction::NavigateToConversation {
conversation_id, ..
} => ItemSummary::Conversation {
@@ -169,6 +179,9 @@ pub enum ItemSummary {
Session {
pane_view_locator: PaneViewLocator,
},
Tab {
pane_group_id: EntityId,
},
NewSession {
id: NewSessionOptionId,
},
+1
View File
@@ -11,6 +11,7 @@ pub mod render_util;
pub mod repos;
mod selected_items;
pub mod separator_search_item;
pub mod tabs;
pub mod view;
pub mod warp_drive;
mod zero_state;
@@ -1,3 +1,5 @@
use warpui::{AppContext, Entity, ModelHandle};
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::navigation::search::{
FuzzySessionSearcher, MatchedSession, SessionMatchResult, SessionSearcher,
@@ -8,7 +10,6 @@ use crate::search::mixer::DataSourceRunErrorWrapper;
use crate::search::SyncDataSource;
use crate::session_management::{SessionNavigationData, SessionSource};
use crate::workspace::PaneViewLocator;
use galaxyui::{AppContext, Entity, ModelHandle};
/// Data source that produces possible running sessions a user could navigate to.
pub struct DataSource {
@@ -56,9 +57,7 @@ impl SyncDataSource for DataSource {
self.searcher
.search(&query.text.trim().to_lowercase(), app)
.map_err(|err| {
let search_error = DataSourceSearchError {
message: err.to_string(),
};
let search_error = DataSourceSearchError::new(err.to_string());
Box::new(search_error) as DataSourceRunErrorWrapper
})
}
@@ -1,3 +1,13 @@
use pathfinder_geometry::vector::vec2f;
use warpui::elements::{
Align, Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Highlight,
ParentElement, Radius, Shrinkable, Wrap,
};
use warpui::fonts::{Properties, Weight};
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
use warpui::units::IntoPixels;
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::context_chips::display_chip::{
chip_container, render_git_diff_stats_content, render_udi_chip, udi_font_size, GitLineChanges,
@@ -16,15 +26,6 @@ use crate::terminal::model::blockgrid::BlockGrid;
use crate::terminal::model::grid::Dimensions;
use crate::terminal::safe_mode_settings::get_secret_obfuscation_mode;
use crate::terminal::SizeInfo;
use galaxyui::elements::{
Align, Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Highlight,
ParentElement, Radius, Shrinkable, Wrap,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::units::IntoPixels;
use galaxyui::{AppContext, Element, SingletonEntity};
use pathfinder_geometry::vector::vec2f;
/// Renders a navigation session.
pub fn render_navigation_session(
@@ -163,6 +164,7 @@ fn render_prompt_udi(snapshot: &PromptSnapshot, appearance: &Appearance) -> Box<
};
parsed
}
ChipValue::GitBranchStatus(_) => continue,
};
let font_size = udi_font_size(appearance);
let content = render_git_diff_stats_content(
@@ -1,3 +1,9 @@
use std::ops::Range;
use fuzzy_match::match_indices_case_insensitive;
use itertools::Itertools;
use warpui::{AppContext, ModelHandle};
use crate::pane_group::PaneId;
use crate::search::command_palette::navigation::render::CommandRenderInfo;
use crate::search::command_palette::navigation::search_item::SearchItem;
@@ -5,10 +11,6 @@ use crate::search::command_palette::navigation::DataSource;
use crate::search::data_source::QueryResult;
use crate::search::SyncDataSource;
use crate::session_management::{CommandContext, SessionNavigationData, SessionSource};
use fuzzy_match::match_indices_case_insensitive;
use galaxyui::{AppContext, ModelHandle};
use itertools::Itertools;
use std::ops::Range;
/// A session that was fuzzy matched against a search term.
pub struct MatchedSession {
@@ -242,19 +244,16 @@ impl SessionSearcher for FuzzySessionSearcher {
pub use full_text_searcher::FullTextSessionSearcher;
#[cfg(not(target_family = "wasm"))]
mod full_text_searcher {
use crate::define_search_schema;
use crate::pane_group::PaneId;
use std::collections::HashMap;
use warp_search_core::define_search_schema;
use crate::search::command_palette::navigation::search::{
searchable_session_string_and_ranges, MatchedSession, SearcherAction,
SessionHighlightIndices, SessionMatchResult, SessionSearcher,
};
use crate::search::command_palette::navigation::search_item::SearchItem;
use crate::search::data_source::QueryResult;
use crate::search::searcher::{DEFAULT_MEMORY_BUDGET, SCORE_CONVERSION_FACTOR};
use crate::session_management::{SessionNavigationData, SessionSource};
use galaxyui::{AppContext, ModelHandle};
use itertools::Itertools;
use std::collections::HashMap;
define_search_schema!(
schema_name: SESSION_SEARCH_SCHEMA,
@@ -1,3 +1,7 @@
use ordered_float::OrderedFloat;
use warpui::elements::Container;
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::pane_group::PaneId;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
@@ -8,9 +12,6 @@ use crate::search::item::IconLocation;
use crate::search::result_renderer::ItemHighlightState;
use crate::session_management::SessionNavigationData;
use crate::ui_components::icons::Icon;
use galaxyui::elements::Container;
use galaxyui::{AppContext, Element, SingletonEntity};
use ordered_float::OrderedFloat;
/// Search item to render a session within the command palette.
pub struct SearchItem {
@@ -1,19 +1,19 @@
use super::new_session_option::{Direction, NewSessionConfig};
use super::new_session_option::{NewSessionOption, NewSessionOptionId};
use super::search_item::SearchItem;
use crate::search::data_source::DataSourceSearchError;
use crate::search::{
binding_source::BindingSource,
command_palette::mixer::CommandPaletteItemAction,
data_source::{Query, QueryResult},
mixer::{DataSourceRunErrorWrapper, SyncDataSource},
};
use crate::terminal::available_shells::AvailableShells;
use std::collections::HashMap;
use std::sync::Arc;
use fuzzy_match::{match_indices_case_insensitive, FuzzyMatchResult};
use galaxy_core::features::FeatureFlag;
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
use std::collections::HashMap;
use std::sync::Arc;
use super::new_session_option::{
Direction, NewSessionConfig, NewSessionOption, NewSessionOptionId,
};
use super::search_item::SearchItem;
use crate::search::binding_source::BindingSource;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::data_source::{DataSourceSearchError, Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::terminal::available_shells::AvailableShells;
/// Controls which kinds of new sessions the data source should surface.
#[derive(Copy, Clone, Debug)]
@@ -47,7 +47,7 @@ impl AllowedSessionKinds {
/// Gathers this data by:
/// - Listening for any binding source changes
/// - Comparing the options in binding sources (open new tab, open new window, etc.)
/// to the list of available shells, and creates an interesction of those items.
/// to the list of available shells, and creates an intersection of those items.
pub struct NewSessionDataSource {
searcher: Box<dyn NewSessionSearcher>,
allowed: AllowedSessionKinds,
@@ -189,9 +189,7 @@ impl SyncDataSource for NewSessionDataSource {
) -> Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper> {
let search_term = query.text.as_str();
self.searcher.search(search_term).map_err(|err| {
let search_error = DataSourceSearchError {
message: err.to_string(),
};
let search_error = DataSourceSearchError::new(err.to_string());
Box::new(search_error) as DataSourceRunErrorWrapper
})
}
@@ -301,7 +299,11 @@ impl NewSessionSearcher for FuzzyNewSessionSearcher {
#[cfg(not(target_family = "wasm"))]
mod full_text_searcher {
use crate::define_search_schema;
use fuzzy_match::FuzzyMatchResult;
use warp_search_core::define_search_schema;
use warpui::r#async::executor::Background;
use crate::search::command_palette::new_session::data_source::{
NewSessionSearcher, SearcherAction, SEARCHER_BASE_STRINGS,
};
@@ -311,10 +313,6 @@ mod full_text_searcher {
use crate::search::searcher::{
AsyncSearcher, DEFAULT_MEMORY_BUDGET, MIN_MEMORY_BUDGET, SCORE_CONVERSION_FACTOR,
};
use fuzzy_match::FuzzyMatchResult;
use galaxyui::r#async::executor::Background;
use std::collections::HashMap;
use std::sync::Arc;
define_search_schema!(
schema_name: NEW_SESSION_SEARCH_SCHEMA,
@@ -1,10 +1,12 @@
use std::borrow::Cow;
use std::fmt;
use warpui::Action;
use crate::server::telemetry::AddTabWithShellSource;
use crate::terminal::available_shells::AvailableShell;
use crate::terminal::view::TerminalAction;
use crate::WorkspaceAction;
use galaxyui::Action;
use std::borrow::Cow;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct NewSessionOptionId(pub(crate) String);
@@ -1,13 +1,10 @@
use warpui::elements::{Container, Flex, Highlight, ParentElement, Text};
use warpui::fonts::{Properties, Weight};
use warpui::Element;
use super::new_session_option::NewSessionOption;
use crate::search::command_palette::styles::SEARCH_ITEM_TEXT_PADDING;
use galaxyui::{
elements::{Container, Flex, Highlight, ParentElement, Text},
fonts::{Properties, Weight},
Element,
};
use crate::appearance::Appearance;
use crate::search::command_palette::styles::SEARCH_ITEM_TEXT_PADDING;
use crate::search::result_renderer::ItemHighlightState;
impl NewSessionOption {
@@ -1,17 +1,16 @@
use super::new_session_option::NewSessionOption;
use crate::{
appearance::Appearance, search::command_palette::render_util::render_search_item_icon,
ui_components::icons::Icon,
};
use fuzzy_match::FuzzyMatchResult;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::result_renderer::ItemHighlightState;
use galaxyui::{AppContext, Element, SingletonEntity};
use ordered_float::OrderedFloat;
use std::sync::Arc;
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use galaxyui::{AppContext, Element, SingletonEntity};
use super::new_session_option::NewSessionOption;
use crate::appearance::Appearance;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::render_util::render_search_item_icon;
use crate::search::result_renderer::ItemHighlightState;
use crate::ui_components::icons::Icon;
#[derive(Debug)]
pub struct SearchItem {
match_result: FuzzyMatchResult,
@@ -1,12 +1,13 @@
use pathfinder_color::ColorU;
use galaxy_core::ui::theme::Fill;
use warpui::elements::{Align, ConstrainedBox, Container, Empty};
use warpui::Element;
use crate::appearance::Appearance;
use crate::search::result_renderer::ItemHighlightState;
use crate::themes::theme::Blend;
use crate::ui_components::icons::Icon;
use crate::util::color::{ContrastingColor, MinimumAllowedContrast};
use galaxy_core::ui::theme::Fill;
use galaxyui::elements::{Align, ConstrainedBox, Container, Empty};
use galaxyui::Element;
use pathfinder_color::ColorU;
/// Helper function to render an icon for any search item within the command palette with consistent
/// styling.
@@ -1,13 +1,12 @@
use std::path::Path;
use ai::workspace::WorkspaceMetadata;
use fuzzy_match::FuzzyMatchResult;
use galaxy_core::ui::theme::Fill;
use galaxyui::{
elements::{Align, ConstrainedBox, Flex, Highlight, ParentElement, Shrinkable, Text},
fonts::{Properties, Weight},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
use std::path::Path;
use galaxy_core::ui::theme::Fill;
use galaxyui::elements::{Align, ConstrainedBox, Flex, Highlight, ParentElement, Shrinkable, Text};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::action::search_item::styles;
@@ -1,7 +1,8 @@
use crate::search::command_palette::mixer::ItemSummary;
use bounded_vec_deque::BoundedVecDeque;
use galaxyui::{Entity, SingletonEntity};
use crate::search::command_palette::mixer::ItemSummary;
/// Maximum number of elements to store. Per the [`BoundedVecDeque`] docs, it is recommended that
/// this is one less than the power of two to avoid unnecessary allocations.
///
@@ -1,7 +1,7 @@
use super::*;
use galaxyui::keymap::BindingId;
use itertools::Itertools;
use super::*;
#[test]
fn test_enqueue_new_item() {
let mut selected_items = SelectedItems::new();
@@ -1,11 +1,11 @@
use ordered_float::OrderedFloat;
use warpui::elements::{Empty, Text};
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use crate::{appearance::Appearance, search::command_palette::mixer::CommandPaletteItemAction};
use galaxyui::{
elements::{Empty, Text},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
/// A simple separator item that displays a title to visually separate sections in search results.
#[derive(Debug)]
@@ -0,0 +1,65 @@
use warpui::{AppContext, Entity};
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::tabs::SearchItem;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::session_management::TabNavigationData;
/// Data source that produces tabs sorted by MRU order for the Ctrl+Tab palette.
///
/// Holds a pre-computed snapshot of tab data rather than a workspace handle,
/// because the synchronous query runs while the workspace view is borrowed
/// and a `WeakViewHandle::upgrade()` would fail.
pub struct DataSource {
tabs: Vec<TabNavigationData>,
}
impl Default for DataSource {
fn default() -> Self {
Self::new()
}
}
impl DataSource {
pub fn new() -> Self {
Self { tabs: vec![] }
}
pub fn set_tabs(&mut self, tabs: Vec<TabNavigationData>) {
self.tabs = tabs;
}
}
impl SyncDataSource for DataSource {
type Action = CommandPaletteItemAction;
fn run_query(
&self,
query: &Query,
_ctx: &AppContext,
) -> Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper> {
let query_text = query.text.trim().to_lowercase();
let results = self
.tabs
.iter()
.enumerate()
.filter(|(_, tab)| {
query_text.is_empty()
|| tab.title.to_lowercase().contains(&query_text)
|| tab
.subtitle
.as_deref()
.is_some_and(|s| s.to_lowercase().contains(&query_text))
})
.map(|(i, tab)| QueryResult::from(SearchItem::new(tab.clone(), i)))
.collect();
Ok(results)
}
}
impl Entity for DataSource {
type Event = ();
}
@@ -0,0 +1,5 @@
pub mod data_source;
pub mod search_item;
pub use data_source::DataSource;
pub use search_item::SearchItem;
@@ -0,0 +1,117 @@
use ordered_float::OrderedFloat;
use warpui::elements::{ConstrainedBox, Container, Flex, ParentElement, Text};
use warpui::fonts::{Properties, Weight};
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::render_util::render_search_item_icon;
use crate::search::item::{IconLocation, SearchItem as SearchItemTrait};
use crate::search::result_renderer::ItemHighlightState;
use crate::session_management::TabNavigationData;
use crate::ui_components::icons::Icon;
/// These items appear in the ctrl-tab palette only, not the main command palette.
/// Scoring matches against queries is not supported since only ranking by recency is needed.
pub struct SearchItem {
tab: TabNavigationData,
mru_rank: usize,
}
impl SearchItem {
pub fn new(tab: TabNavigationData, mru_rank: usize) -> Self {
Self { tab, mru_rank }
}
}
impl SearchItemTrait for SearchItem {
type Action = CommandPaletteItemAction;
fn is_multiline(&self) -> bool {
self.tab.subtitle.is_some()
}
fn render_icon(
&self,
highlight_state: ItemHighlightState,
appearance: &Appearance,
) -> Box<dyn Element> {
let color = if let Some(tab_color) = self.tab.color {
tab_color
.to_ansi_color(&appearance.theme().terminal_colors().normal)
.into()
} else {
highlight_state.icon_fill(appearance).into_solid()
};
render_search_item_icon(appearance, Icon::Navigation, color, highlight_state)
}
fn icon_location(&self, appearance: &Appearance) -> IconLocation {
let margin_top = (appearance.line_height_ratio() * appearance.monospace_font_size())
- appearance.monospace_font_size();
IconLocation::Top { margin_top }
}
fn render_item(
&self,
highlight_state: ItemHighlightState,
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let title_text = Text::new_inline(
format!("{} · Tab {}", self.tab.title, self.tab.tab_index),
appearance.ui_font_family(),
appearance.monospace_font_size(),
)
.with_color(highlight_state.main_text_fill(appearance).into_solid())
.with_style(Properties::default().weight(Weight::Bold))
.finish();
if let Some(subtitle) = &self.tab.subtitle {
let subtitle_text = Text::new_inline(
subtitle.clone(),
appearance.ui_font_family(),
appearance.monospace_font_size() - 2.,
)
.with_color(highlight_state.sub_text_fill(appearance).into_solid())
.finish();
let contents = Flex::column()
.with_child(title_text)
.with_child(Container::new(subtitle_text).with_padding_top(4.).finish())
.finish();
ConstrainedBox::new(contents).with_height(50.).finish()
} else {
ConstrainedBox::new(Flex::column().with_child(title_text).finish())
.with_height(50.)
.finish()
}
}
fn score(&self) -> OrderedFloat<f64> {
OrderedFloat::from(1000.0 - self.mru_rank as f64)
}
fn accept_result(&self) -> Self::Action {
CommandPaletteItemAction::NavigateToTab {
pane_group_id: self.tab.pane_group_id,
window_id: self.tab.window_id,
}
}
fn execute_result(&self) -> Self::Action {
self.accept_result()
}
fn accessibility_label(&self) -> String {
format!("Selected tab: {}.", self.tab.title)
}
fn accessibility_help_message(&self) -> Option<String> {
Some(format!(
"Press enter to navigate to tab: {}.",
self.tab.title
))
}
}
+69 -68
View File
@@ -1,60 +1,53 @@
use crate::appearance::Appearance;
use crate::drive::CloudObjectTypeAndId;
use crate::search::binding_source::{BindingFilterFn, BindingSource};
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::SelectedItems;
use crate::search::result_renderer::QueryResultRenderer;
use crate::search::search_bar::SelectionUpdate;
use crate::search::search_bar::{SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering};
use crate::search::QueryFilter;
use crate::send_telemetry_from_ctx;
use crate::server::telemetry::LaunchConfigUiLocation;
use crate::server::telemetry::TelemetryEvent;
use crate::settings::CtrlTabBehavior;
use crate::terminal::keys_settings::KeysSettings;
use crate::themes::theme::GalaxyTheme;
use crate::view_components::DismissibleToast;
use crate::ToastStack;
use galaxy_core::send_telemetry_from_app_ctx;
use galaxy_util::path::LineAndColumnArg;
use lazy_static::lazy_static;
use crate::search::action::search_item::MatchedBinding;
use galaxyui::elements::DispatchEventResult;
use galaxyui::elements::EventHandler;
use galaxyui::event::KeyState;
use galaxyui::platform::keyboard::KeyCode;
use galaxyui::FocusContext;
use itertools::Itertools;
use crate::search::command_palette::zero_state::{self, Event as ZeroStateEvent, ZeroState};
use crate::search::data_source::QueryResult;
use std::collections::HashSet;
use std::ops::Deref;
use std::sync::Arc;
use crate::features::FeatureFlag;
use crate::palette::PaletteMode;
use crate::root_view::OpenLaunchConfigArg;
use crate::search::command_palette::data_sources::DataSourceStore;
use crate::server::ids::SyncId;
use crate::session_management::SessionSource;
use crate::workspace::{active_terminal_in_window, ForkedConversationDestination, WorkspaceAction};
use itertools::Itertools;
use lazy_static::lazy_static;
use galaxy_core::send_telemetry_from_app_ctx;
use galaxy_util::path::LineAndColumnArg;
use galaxyui::elements::{
Align, Border, ChildView, Clipped, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox,
Container, CornerRadius, Dismiss, Empty, Fill, Flex, ParentElement, Radius, SavePosition,
Shrinkable,
Container, CornerRadius, Dismiss, DispatchEventResult, Empty, EventHandler, Fill, Flex,
ParentElement, Radius, SavePosition, Shrinkable,
};
use galaxyui::event::KeyState;
use galaxyui::keymap::BindingId;
use galaxyui::platform::keyboard::KeyCode;
use galaxyui::units::{IntoPixels, Pixels};
use galaxyui::{
AppContext, Element, Entity, EntityId, ModelHandle, SingletonEntity, TypedActionView,
ViewContext, ViewHandle, WindowId,
AppContext, Element, Entity, EntityId, FocusContext, ModelHandle, SingletonEntity,
TypedActionView, ViewContext, ViewHandle, WindowId,
};
use super::super::palette_styles as styles;
use super::CommandPaletteMixer;
use crate::appearance::Appearance;
use crate::drive::CloudObjectTypeAndId;
use crate::features::FeatureFlag;
use crate::palette::PaletteMode;
use crate::root_view::OpenLaunchConfigArg;
use crate::search::action::search_item::MatchedBinding;
use crate::search::binding_source::{BindingFilterFn, BindingSource};
use crate::search::command_palette::data_sources::DataSourceStore;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::zero_state::{self, Event as ZeroStateEvent, ZeroState};
use crate::search::command_palette::SelectedItems;
use crate::search::data_source::QueryResult;
use crate::search::result_renderer::QueryResultRenderer;
use crate::search::search_bar::{
SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering, SelectionUpdate,
};
use crate::search::QueryFilter;
use crate::server::ids::SyncId;
use crate::server::telemetry::{LaunchConfigUiLocation, TelemetryEvent};
use crate::session_management::SessionSource;
use crate::settings::CtrlTabBehavior;
use crate::terminal::keys_settings::KeysSettings;
use crate::themes::theme::WarpTheme;
use crate::view_components::DismissibleToast;
use crate::workspace::{active_terminal_in_window, ForkedConversationDestination, WorkspaceAction};
use crate::{send_telemetry_from_ctx, ToastStack};
lazy_static! {
/// Set of hardcoded action names that we want to show in the command palette zero state.
@@ -179,7 +172,7 @@ impl galaxyui::View for View {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let body = if self.search_bar_state.as_ref(app).should_show_zero_state() {
let body = if self.search_bar.as_ref(app).should_show_zero_state(app) {
ChildView::new(&self.zero_state_handle).finish()
} else {
self.render_palette_list(theme, app)
@@ -337,22 +330,10 @@ impl View {
.map(|item| &item.search_result)
}
pub fn set_fixed_query_filters(
&mut self,
title: String,
filters: Vec<QueryFilter>,
ctx: &mut ViewContext<Self>,
) {
self.search_bar.update(ctx, |search_bar, ctx| {
search_bar.set_fixed_filters(title, filters, ctx);
});
ctx.notify();
}
/// Set the active query filter in the search bar to be `filter`.
pub fn set_active_query_filter(&mut self, filter: QueryFilter, ctx: &mut ViewContext<Self>) {
self.search_bar.update(ctx, |view, ctx| {
view.set_visible_query_filter(Some((filter, filter.filter_atom().primary_text)), ctx)
view.set_query_filter(Some((filter, filter.filter_atom().primary_text)), ctx)
});
ctx.notify();
}
@@ -364,15 +345,15 @@ impl View {
}
pub fn select_next_item(&mut self, ctx: &mut ViewContext<Self>) {
self.search_bar_state.update(ctx, |state, ctx| {
state.handle_selection_update(SelectionUpdate::Down, ctx);
self.search_bar.update(ctx, |search_bar, ctx| {
search_bar.handle_selection_update(SelectionUpdate::Down, ctx);
});
ctx.notify();
}
pub fn select_prev_item(&mut self, ctx: &mut ViewContext<Self>) {
self.search_bar_state.update(ctx, |state, ctx| {
state.handle_selection_update(SelectionUpdate::Up, ctx);
self.search_bar.update(ctx, |search_bar, ctx| {
search_bar.handle_selection_update(SelectionUpdate::Up, ctx);
});
ctx.notify();
}
@@ -385,9 +366,7 @@ impl View {
/// Returns the active query filters
pub fn active_query_filter(&self, app: &AppContext) -> Option<QueryFilter> {
self.search_bar_state
.as_ref(app)
.active_visible_query_filter()
self.search_bar_state.as_ref(app).active_query_filter()
}
pub fn is_mode_enabled(&self, mode: PaletteMode, app: &AppContext) -> bool {
@@ -739,10 +718,17 @@ impl View {
result_action: CommandPaletteItemAction,
ctx: &mut ViewContext<Self>,
) {
let selected_items_handle = SelectedItems::handle(ctx);
selected_items_handle.update(ctx, |selected_items, _ctx| {
selected_items.enqueue(result_action.to_summary())
});
// Tab navigations don't appear in the main command palette to avoid confusion with session
// navigations, so they can't evict real recent items from SelectedItems.
if !matches!(
result_action,
CommandPaletteItemAction::NavigateToTab { .. }
) {
let selected_items_handle = SelectedItems::handle(ctx);
selected_items_handle.update(ctx, |selected_items, _ctx| {
selected_items.enqueue(result_action.to_summary())
});
}
if let CommandPaletteItemAction::AcceptBinding { binding } = &result_action {
if let Some(action) = &binding.action {
@@ -812,6 +798,20 @@ impl View {
send_telemetry_from_ctx!(TelemetryEvent::SelectNavigationPaletteItem, ctx);
}
CommandPaletteItemAction::NavigateToTab {
pane_group_id,
window_id,
} => {
if let Some(root_view_id) = ctx.root_view_id(window_id) {
ctx.dispatch_action_for_view(
window_id,
root_view_id,
"root_view:activate_tab_by_pane_group_id",
&pane_group_id,
);
}
send_telemetry_from_ctx!(TelemetryEvent::SelectNavigationPaletteItem, ctx);
}
CommandPaletteItemAction::NavigateToConversation {
pane_view_locator,
window_id,
@@ -863,6 +863,7 @@ impl View {
summarize_after_fork: false,
summarization_prompt: None,
initial_prompt: None,
initial_attachments: vec![],
destination: ForkedConversationDestination::SplitPane,
});
}
@@ -1,3 +1,7 @@
use std::collections::HashMap;
use warpui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
use super::env_var_collection_search_item::EnvVarCollectionSearchItem;
use super::notebook_search_item::NotebookSearchItem;
use super::workflow_search_item::WorkflowSearchItem;
@@ -18,8 +22,6 @@ use crate::search::QueryFilter;
use crate::server::ids::{ObjectUid, SyncId};
use crate::settings::AISettings;
use crate::workflows::CloudWorkflow;
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity};
use std::collections::HashMap;
/// Datasource that searches against all Warp Drive objects
pub struct DataSource {
@@ -64,6 +66,7 @@ impl DataSource {
fn handle_cloud_object_updated(
&mut self,
_: ModelHandle<CloudModel>,
event: &CloudModelEvent,
ctx: &mut ModelContext<Self>,
) {
@@ -162,9 +165,8 @@ impl crate::search::mixer::SyncDataSource for DataSource {
self.searcher
.search_notebook(&query.text.to_lowercase(), app)
.map_err(|err| {
Box::new(DataSourceSearchError {
message: err.to_string(),
}) as DataSourceRunErrorWrapper
Box::new(DataSourceSearchError::new(err.to_string()))
as DataSourceRunErrorWrapper
})?
.into_iter()
.map(QueryResult::from),
@@ -176,9 +178,8 @@ impl crate::search::mixer::SyncDataSource for DataSource {
self.searcher
.search_plans(&query.text.to_lowercase(), app)
.map_err(|err| {
Box::new(DataSourceSearchError {
message: err.to_string(),
}) as DataSourceRunErrorWrapper
Box::new(DataSourceSearchError::new(err.to_string()))
as DataSourceRunErrorWrapper
})?
.into_iter()
.map(QueryResult::from),
@@ -201,9 +202,8 @@ impl crate::search::mixer::SyncDataSource for DataSource {
app,
)
.map_err(|err| {
Box::new(DataSourceSearchError {
message: err.to_string(),
}) as DataSourceRunErrorWrapper
Box::new(DataSourceSearchError::new(err.to_string()))
as DataSourceRunErrorWrapper
})?
.into_iter()
.map(QueryResult::from),
@@ -217,9 +217,8 @@ impl crate::search::mixer::SyncDataSource for DataSource {
self.searcher
.search_env_var(&query.text.to_lowercase(), app)
.map_err(|err| {
Box::new(DataSourceSearchError {
message: err.to_string(),
}) as DataSourceRunErrorWrapper
Box::new(DataSourceSearchError::new(err.to_string()))
as DataSourceRunErrorWrapper
})?
.into_iter()
.map(QueryResult::from),
@@ -541,31 +540,25 @@ impl WarpDriveSearcher for FuzzyWarpDriveSearcher {
mod full_text_searcher {
use std::sync::Arc;
use fuzzy_match::FuzzyMatchResult;
use itertools::Itertools;
use warp_search_core::define_search_schema;
use warpui::r#async::executor::Background;
use warpui::{AppContext, SingletonEntity};
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::{
CloudObject, CloudObjectLocation, GenericStringObjectFormat, JsonObjectType, ObjectType,
};
use crate::define_search_schema;
use crate::drive::folders::CloudFolder;
use crate::env_vars::CloudEnvVarCollection;
use crate::notebooks::manager::NotebookManager;
use crate::notebooks::CloudNotebook;
use crate::search::command_palette::warp_drive::data_source::WarpDriveSearcher;
use crate::search::command_palette::warp_drive::env_var_collection_search_item::{
EnvVarCollectionSearchItem, ENV_VAR_NAME_SEPARATOR,
};
use crate::search::command_palette::warp_drive::notebook_search_item::NotebookSearchItem;
use crate::search::command_palette::warp_drive::workflow_search_item::WorkflowSearchItem;
use crate::search::env_var_collections::fuzzy_match::FuzzyMatchEnvVarCollectionResult;
use crate::search::notebooks::fuzzy_match::FuzzyMatchNotebookResult;
use crate::search::searcher::{AsyncSearcher, DEFAULT_MEMORY_BUDGET, SCORE_CONVERSION_FACTOR};
use crate::search::workflows::fuzzy_match::FuzzyMatchWorkflowResult;
use crate::server::ids::ObjectUid;
use crate::workflows::CloudWorkflow;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::r#async::executor::Background;
use galaxyui::{AppContext, SingletonEntity};
use itertools::Itertools;
/// Memory budget for the search index of warp drive.
/// Warp could potentially have a lot of objects, so we increase it from the default of 50MB to 100MB
@@ -1,3 +1,9 @@
use itertools::Itertools;
use ordered_float::OrderedFloat;
use warpui::elements::{Container, Flex, Highlight, ParentElement, Text};
use warpui::fonts::{Properties, Weight};
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::cloud_object::CloudObject;
use crate::drive::cloud_object_styling::warp_drive_icon_color;
@@ -10,11 +16,6 @@ use crate::search::env_var_collections::fuzzy_match::FuzzyMatchEnvVarCollectionR
use crate::search::item::{IconLocation, SearchItem};
use crate::search::result_renderer::ItemHighlightState;
use crate::ui_components::icons::Icon;
use galaxyui::elements::{Container, Flex, Highlight, ParentElement, Text};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use itertools::Itertools;
use ordered_float::OrderedFloat;
pub const ENV_VAR_NAME_SEPARATOR: &str = ", ";
@@ -1,3 +1,8 @@
use ordered_float::OrderedFloat;
use warpui::elements::{Container, Flex, Highlight, ParentElement, Text};
use warpui::fonts::{Properties, Weight};
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::cloud_object::CloudObject;
use crate::drive::cloud_object_styling::warp_drive_icon_color;
@@ -12,10 +17,6 @@ use crate::search::notebooks::fuzzy_match::{
};
use crate::search::result_renderer::ItemHighlightState;
use crate::ui_components::icons::Icon;
use galaxyui::elements::{Container, Flex, Highlight, ParentElement, Text};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use ordered_float::OrderedFloat;
/// Search item result for a cloud notebook.
#[derive(Debug)]
@@ -1,3 +1,8 @@
use ordered_float::OrderedFloat;
use warpui::elements::{Clipped, Container, Flex, Highlight, ParentElement, Shrinkable, Text};
use warpui::fonts::{Properties, Weight};
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::cloud_object::CloudObject;
use crate::drive::cloud_object_styling::warp_drive_icon_color;
@@ -10,10 +15,6 @@ use crate::search::result_renderer::ItemHighlightState;
use crate::search::workflows::fuzzy_match::FuzzyMatchWorkflowResult;
use crate::ui_components::icons::Icon;
use crate::workflows::CloudWorkflow;
use galaxyui::elements::{Clipped, Container, Flex, Highlight, ParentElement, Shrinkable, Text};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use ordered_float::OrderedFloat;
/// Search item result for a cloud workflow.
#[derive(Debug)]
+10 -10
View File
@@ -1,21 +1,21 @@
mod items;
use std::collections::HashMap;
pub use items::Items;
use galaxy_core::context_flag::ContextFlag;
use galaxy_core::features::FeatureFlag;
pub use items::Items;
use crate::appearance::Appearance;
use crate::search::command_palette::FilterChipRenderer;
use crate::drive::settings::WarpDriveSettings;
use crate::search::QueryFilter;
use crate::settings::AISettings;
use crate::workspace::Workspace;
use galaxyui::elements::{Container, Flex, MouseStateHandle, ParentElement, Shrinkable, Wrap};
use galaxyui::{
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
WindowId,
};
use std::collections::HashMap;
use crate::appearance::Appearance;
use crate::drive::settings::WarpDriveSettings;
use crate::search::command_palette::FilterChipRenderer;
use crate::search::QueryFilter;
use crate::settings::AISettings;
use crate::workspace::Workspace;
/// A zero-state view for the command palette.
pub struct ZeroState {
@@ -1,13 +1,13 @@
use crate::appearance::Appearance;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::result_renderer::QueryResultRenderer;
use crate::search::search_bar::SelectionUpdate;
use galaxyui::elements::{Container, Flex, ParentElement};
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::ui_components::text::WrappableText;
use galaxyui::{AppContext, Element, Entity, ModelContext, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::result_renderer::QueryResultRenderer;
use crate::search::search_bar::SelectionUpdate;
/// List of items shown within the zero state. "Recent" items are shown first followed by
/// "Suggested" items.
pub struct Items {
@@ -1,14 +1,13 @@
use galaxyui::{AppContext, SingletonEntity};
use itertools::Itertools;
use super::AIQuerySearchResultItem;
use crate::ai::blocklist::BlocklistAIHistoryModel;
use crate::search::ai_queries::fuzzy_match::FuzzyMatchAIQueryResults;
use crate::search::command_search::searcher::CommandSearchItemAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use super::AIQuerySearchResultItem;
/// Manages querying the AI queries in history for Command Search.
pub struct AIQueriesDataSource {}
@@ -1,31 +1,25 @@
use crate::{
ai::blocklist::AIQueryHistoryOutputStatus,
terminal::rich_history::{render_row_with_icon_and_paragraph, DETAILS_PARAGRAPH_SPACING},
util::time_format::format_approx_duration_from_now,
};
use chrono::{DateTime, Local};
use galaxy_core::ui::builder::MIN_FONT_SIZE;
use galaxyui::{
elements::{
Align, ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon,
MainAxisAlignment, MainAxisSize, ParentElement, Shrinkable, Text,
},
fonts::{Properties, Weight},
ui_components::components::{Coords, UiComponent, UiComponentStyles},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
use crate::ui_components::icons::Icon as UiIcon;
use crate::{
appearance::Appearance,
search::{
ai_queries::fuzzy_match::FuzzyMatchAIQueryResults,
command_search::searcher::CommandSearchItemAction, item::SearchItem,
result_renderer::ItemHighlightState,
},
use galaxy_core::ui::builder::MIN_FONT_SIZE;
use galaxyui::elements::{
Align, ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, MainAxisAlignment,
MainAxisSize, ParentElement, Shrinkable, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::ai::blocklist::AIQueryHistoryOutputStatus;
use crate::appearance::Appearance;
use crate::search::ai_queries::fuzzy_match::FuzzyMatchAIQueryResults;
use crate::search::command_search::searcher::CommandSearchItemAction;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use crate::terminal::rich_history::{
render_row_with_icon_and_paragraph, DETAILS_PARAGRAPH_SPACING,
};
use crate::ui_components::icons::Icon as UiIcon;
use crate::util::time_format::format_approx_duration_from_now;
/// Stores data needed to display an AI query search result item in Command Search.
#[derive(Clone, Debug)]
@@ -1,24 +1,19 @@
use galaxyui::{
elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, MainAxisAlignment,
MainAxisSize, ParentElement, Text,
},
fonts::{Properties, Weight},
ui_components::components::{UiComponent, UiComponentStyles},
AppContext, Element, SingletonEntity,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use crate::{
appearance::Appearance,
env_vars::CloudEnvVarCollection,
search::{
command_search::searcher::CommandSearchItemAction,
env_var_collections::fuzzy_match::FuzzyMatchEnvVarCollectionResult, item::SearchItem,
result_renderer::ItemHighlightState,
},
use galaxyui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, MainAxisAlignment,
MainAxisSize, ParentElement, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::env_vars::CloudEnvVarCollection;
use crate::search::command_search::searcher::CommandSearchItemAction;
use crate::search::env_var_collections::fuzzy_match::FuzzyMatchEnvVarCollectionResult;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
const ENV_VAR_COLLECTION_ICON_PATH: &str = "bundled/svg/env-var-collection.svg";
@@ -1,9 +1,8 @@
use galaxyui::{AppContext, SingletonEntity};
use itertools::Itertools;
use crate::cloud_object::model::persistence::CloudModel;
use super::EnvVarCollectionSearchItem;
use crate::cloud_object::model::persistence::CloudModel;
use crate::search::command_search::searcher::CommandSearchItemAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::env_var_collections::fuzzy_match::FuzzyMatchEnvVarCollectionResult;
@@ -1,7 +1,9 @@
use futures_lite::future::yield_now;
use galaxyui::{AppContext, SingletonEntity};
use std::sync::Arc;
use futures_lite::future::yield_now;
use galaxyui::{AppContext, SingletonEntity};
use super::HistorySearchItem;
use crate::search::async_snapshot_data_source::AsyncSnapshotDataSource;
use crate::search::command_search::searcher::CommandSearchItemAction;
use crate::search::data_source::{Query, QueryResult};
@@ -11,8 +13,6 @@ use crate::terminal;
use crate::terminal::model::session::SessionId;
use crate::terminal::HistoryEntry;
use super::HistorySearchItem;
pub(crate) struct HistorySnapshot {
commands: Arc<[Arc<HistoryEntry>]>,
query_text: String,
@@ -1,26 +1,23 @@
use crate::ui_components::icons::Icon as UiIcon;
use galaxy_core::ui::builder;
use galaxyui::{
elements::{
Align, ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon,
MainAxisAlignment, MainAxisSize, ParentElement, Shrinkable, Text,
},
fonts::{Properties, Weight},
ui_components::components::{Coords, UiComponent, UiComponentStyles},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
use std::sync::Arc;
use ordered_float::OrderedFloat;
use galaxy_core::ui::builder;
use warpui::elements::{
Align, ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, MainAxisAlignment,
MainAxisSize, ParentElement, Shrinkable, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use warpui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::command_search::searcher::{AcceptedHistoryItem, CommandSearchItemAction};
use crate::search::item::SearchItem;
use crate::search::{
command_search::searcher::AcceptedHistoryItem, result_renderer::ItemHighlightState,
};
use crate::{
appearance::Appearance, terminal::rich_history::render_rich_history,
util::time_format::format_approx_duration_from_now,
};
use crate::{search::command_search::searcher::CommandSearchItemAction, terminal::HistoryEntry};
use crate::search::result_renderer::ItemHighlightState;
use crate::terminal::rich_history::render_rich_history;
use crate::terminal::HistoryEntry;
use crate::ui_components::icons::Icon as UiIcon;
use crate::util::time_format::format_approx_duration_from_now;
const COMMAND_METADATA_LEFT_MARGIN_FROM_METADATA: f32 = 8.;
@@ -1,14 +1,12 @@
use std::sync::Arc;
use galaxyui::{
elements::{
ConstrainedBox, Container, Flex, Highlight, Icon, MainAxisAlignment, MainAxisSize,
ParentElement, Text,
},
fonts::{Properties, Weight},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
use galaxyui::elements::{
ConstrainedBox, Container, Flex, Highlight, Icon, MainAxisAlignment, MainAxisSize,
ParentElement, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::notebooks::CloudNotebookModel;
@@ -3,6 +3,7 @@ use std::sync::Arc;
use futures_lite::future::yield_now;
use galaxyui::{AppContext, SingletonEntity};
use super::NotebookSearchItem;
use crate::cloud_object::model::persistence::CloudModel;
use crate::notebooks::manager::NotebookManager;
use crate::notebooks::CloudNotebookModel;
@@ -12,8 +13,6 @@ use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{BoxFuture, DataSourceRunErrorWrapper};
use crate::server::ids::SyncId;
use super::NotebookSearchItem;
pub(crate) struct NotebookMatchCandidate {
id: SyncId,
model: Arc<CloudNotebookModel>,
@@ -1,7 +1,5 @@
pub mod project_data_source;
pub mod project_search_item;
pub mod suggested_projects_data_source;
pub use project_data_source::*;
pub use project_search_item::*;
pub use suggested_projects_data_source::*;
@@ -1,20 +1,21 @@
use std::cmp::Ordering;
use std::path::PathBuf;
use chrono::NaiveDateTime;
use fuzzy_match::FuzzyMatchResult;
use galaxy_core::ui::theme::Fill;
use galaxyui::{
elements::{Align, ConstrainedBox, Flex, Highlight, ParentElement, Shrinkable, Text},
fonts::{Properties, Weight},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
use std::{cmp::Ordering, path::PathBuf};
use galaxy_core::ui::theme::Fill;
use galaxyui::elements::{Align, ConstrainedBox, Flex, Highlight, ParentElement, Shrinkable, Text};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::action::search_item::styles;
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::render_util::render_search_item_icon;
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use crate::ui_components::icons::Icon as UiIcon;
use crate::{appearance::Appearance, search::command_palette::mixer::CommandPaletteItemAction};
/// Stores data needed to display a project search result item in Command Search.
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -26,12 +27,6 @@ pub struct ProjectSearchItem {
pub popularity_score: i32,
}
/// Mac and windows are insensitive, Linux probably IS sensitive
/// WARNING: Don't use this function for use cases dependent on the session, e.g. a remote session or WSL on Windows. It only considers this specific host.
pub fn os_probably_case_sensitive() -> bool {
!(cfg!(target_os = "macos") || cfg!(target_family = "windows"))
}
/// Extracts a display name from a project path (returns relative path from home directory).
fn project_display_name(project_path: &str) -> String {
let path = PathBuf::from(project_path);
@@ -147,14 +142,6 @@ impl SearchItem for ProjectSearchItem {
fn accessibility_label(&self) -> String {
format!("Project: {}", self.name)
}
fn dedup_key(&self) -> Option<String> {
if os_probably_case_sensitive() {
Some(self.path.clone())
} else {
Some(self.path.to_lowercase())
}
}
}
impl PartialOrd for ProjectSearchItem {
+1 -1
View File
@@ -68,5 +68,5 @@ pub enum CommandSearchItemAction {
}
#[cfg(test)]
#[path = "searcher_test.rs"]
#[path = "searcher_tests.rs"]
mod tests;
@@ -1,26 +1,28 @@
use std::collections::HashSet;
use std::time::Duration;
use itertools::Itertools;
use ordered_float::OrderedFloat;
use warpui::elements::Empty;
use warpui::r#async::Timer;
use warpui::{App, AppContext, Element};
use super::*;
use crate::appearance::Appearance;
use crate::auth::auth_manager::AuthManager;
use crate::auth::AuthStateProvider;
use crate::search::command_search::history::history_data_source;
use crate::search::command_search::searcher::CommandSearchMixer;
use crate::search::data_source::Query;
use crate::search::data_source::QueryResult;
use crate::search::data_source::{Query, QueryResult};
use crate::search::item::SearchItem;
use crate::search::mixer::DataSourceRunErrorWrapper;
use crate::search::mixer::{AddAsyncSourceOptions, AsyncDataSource, BoxFuture};
use crate::search::mixer::{
AddAsyncSourceOptions, AsyncDataSource, BoxFuture, DataSourceRunErrorWrapper,
};
use crate::search::result_renderer::ItemHighlightState;
use crate::search::{QueryFilter, SyncDataSource};
use crate::server::server_api::ServerApiProvider;
use crate::server::telemetry::context_provider::AppTelemetryContextProvider;
use crate::terminal::HistoryEntry;
use crate::{appearance::Appearance, search::command_search::history::history_data_source};
use galaxyui::r#async::Timer;
use galaxyui::AppContext;
use galaxyui::{elements::Empty, App, Element};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use std::collections::HashSet;
use std::time::Duration;
#[derive(Clone, Debug)]
enum TestItemAction {
+2 -1
View File
@@ -1,4 +1,5 @@
use settings::{macros::define_settings_group, SupportedPlatforms, SyncToCloud};
use settings::macros::define_settings_group;
use settings::{RespectUserSyncSetting, SupportedPlatforms, SyncToCloud};
define_settings_group!(CommandSearchSettings, settings: [
show_global_workflows_in_universal_search: ShowGlobalWorkflowsInUniversalSearch {
+69 -74
View File
@@ -1,67 +1,61 @@
use itertools::Itertools;
use std::collections::HashSet;
use std::ops::Range;
use std::sync::Arc;
use std::time::Duration;
use async_channel::Sender;
use itertools::Itertools;
use lazy_static::lazy_static;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::Vector2F;
use crate::search::mixer::AddAsyncSourceOptions;
use galaxy_core::features::FeatureFlag;
use galaxyui::accessibility::{AccessibilityContent, WarpA11yRole};
use galaxyui::elements::{
resizable_state_handle, Align, AnchorPair, Border, ConstrainedBox, Container, CornerRadius,
CrossAxisAlignment, Dismiss, Fill, Flex, MouseStateHandle, OffsetPositioning, OffsetType,
ParentElement, ParentOffsetBounds, PositionedElementOffsetBounds, PositioningAxis, Radius,
Resizable, ResizableStateHandle, SavePosition, ScrollStateHandle, Scrollable,
ScrollableElement, Shrinkable, Stack, UniformList, UniformListState, XAxisAnchor, YAxisAnchor,
};
use galaxyui::presenter::ChildView;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{
accessibility::{AccessibilityContent, GalaxyA11yRole},
elements::{
resizable_state_handle, Align, AnchorPair, Border, ConstrainedBox, Container, CornerRadius,
CrossAxisAlignment, Dismiss, Fill, Flex, MouseStateHandle, OffsetPositioning, OffsetType,
ParentElement, ParentOffsetBounds, PositionedElementOffsetBounds, PositioningAxis, Radius,
Resizable, ResizableStateHandle, SavePosition, ScrollStateHandle, Scrollable,
ScrollableElement, Shrinkable, Stack, UniformList, UniformListState, XAxisAnchor,
YAxisAnchor,
},
presenter::ChildView,
ui_components::components::{UiComponent, UiComponentStyles},
AppContext, Element, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use lazy_static::lazy_static;
use std::{collections::HashSet, ops::Range, sync::Arc, time::Duration};
use crate::{
ai_assistant::{
execution_context::WarpAiExecutionContext, GenerateCommandsFromNaturalLanguageError,
},
appearance::Appearance,
auth::{
auth_manager::AuthManager, auth_state::AuthState, auth_view_modal::AuthViewVariant,
AuthStateProvider, UserUid,
},
completer::SessionContext,
drive::settings::WarpDriveSettings,
search::{
command_search::searcher::{CommandSearchItemAction, CommandSearchMixer},
result_renderer::{QueryResultRenderer, QueryResultRendererStyles},
search_bar::{SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering},
QueryFilter,
},
send_telemetry_from_ctx,
server::{ids::ServerId, server_api::ai::AIClient, telemetry::TelemetryEvent},
settings::AISettings,
terminal::{
input::MenuPositioning,
model::session::SessionId,
resizable_data::{ModalType, ResizableData, DEFAULT_UNIVERSAL_SEARCH_WIDTH},
History, HistoryEvent,
},
workspaces::user_workspaces::UserWorkspaces,
};
use super::{
ai_queries::AIQueriesDataSource,
env_var_collections::EnvVarCollectionDataSource,
history::history_data_source_for_session,
notebooks::notebooks_data_source,
warp_ai::WarpAIDataSource,
workflows::{cloud_workflows_data_source, WorkflowsDataSource},
zero_state::{CommandSearchZeroStateEvent, CommandSearchZeroStateView},
};
use super::ai_queries::AIQueriesDataSource;
use super::env_var_collections::EnvVarCollectionDataSource;
use super::history::history_data_source_for_session;
use super::notebooks::notebooks_data_source;
use super::warp_ai::WarpAIDataSource;
use super::workflows::{cloud_workflows_data_source, WorkflowsDataSource};
use super::zero_state::{CommandSearchZeroStateEvent, CommandSearchZeroStateView};
use crate::ai_assistant::execution_context::WarpAiExecutionContext;
use crate::ai_assistant::GenerateCommandsFromNaturalLanguageError;
use crate::appearance::Appearance;
use crate::auth::auth_manager::AuthManager;
use crate::auth::auth_state::AuthState;
use crate::auth::auth_view_modal::AuthViewVariant;
use crate::auth::{AuthStateProvider, UserUid};
use crate::completer::SessionContext;
use crate::drive::settings::WarpDriveSettings;
use crate::search::command_search::searcher::{CommandSearchItemAction, CommandSearchMixer};
use crate::search::mixer::AddAsyncSourceOptions;
use crate::search::result_renderer::{QueryResultRenderer, QueryResultRendererStyles};
use crate::search::search_bar::{SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering};
use crate::search::QueryFilter;
use crate::send_telemetry_from_ctx;
use crate::server::ids::ServerId;
use crate::server::server_api::ai::AIClient;
use crate::server::telemetry::TelemetryEvent;
use crate::settings::AISettings;
use crate::terminal::input::MenuPositioning;
use crate::terminal::model::session::SessionId;
use crate::terminal::resizable_data::{ModalType, ResizableData, DEFAULT_UNIVERSAL_SEARCH_WIDTH};
use crate::terminal::{History, HistoryEvent};
use crate::workspaces::user_workspaces::UserWorkspaces;
const DEFAULT_PLACEHOLDER_TEXT: &str = "Search your history, workflows, and more";
const PANEL_POSITION_ID: &str = "CommandSearchViewPanel";
@@ -317,8 +311,9 @@ impl CommandSearchView {
ctx,
);
} else {
ctx.subscribe_to_model(&History::handle(ctx), move |mixer, history_event, ctx| {
match history_event {
ctx.subscribe_to_model(
&History::handle(ctx),
move |mixer, _, history_event, ctx| match history_event {
HistoryEvent::Initialized(id) => {
if id == &session_id {
let source = history_data_source_for_session(
@@ -336,11 +331,14 @@ impl CommandSearchView {
},
ctx,
);
if let Some(query) = mixer.current_query().cloned() {
mixer.run_query(query, ctx);
}
ctx.notify();
}
}
}
});
},
);
}
})
}
@@ -402,10 +400,7 @@ impl CommandSearchView {
fn close(&self, ctx: &mut ViewContext<Self>) {
let query = self.search_bar.as_ref(ctx).query(ctx);
let filter = self
.search_bar_state
.as_ref(ctx)
.active_visible_query_filter();
let filter = self.search_bar_state.as_ref(ctx).active_query_filter();
ctx.emit(CommandSearchEvent::Close { query, filter });
}
@@ -481,15 +476,13 @@ impl CommandSearchView {
ctx: &mut ViewContext<Self>,
) {
self.search_bar.update(ctx, |search_bar, ctx| {
search_bar.set_visible_query_filter(filter_and_atom_text, ctx);
search_bar.set_query_filter(filter_and_atom_text, ctx);
});
}
/// Returns the active query filters
fn active_query_filter(&self, app: &AppContext) -> Option<QueryFilter> {
self.search_bar_state
.as_ref(app)
.active_visible_query_filter()
self.search_bar_state.as_ref(app).active_query_filter()
}
/// Emits the `ItemSelected` event containing the passed `CommandSearchEventPayload` and closes
@@ -542,10 +535,7 @@ impl CommandSearchView {
TelemetryEvent::CommandSearchResultAccepted {
result_index,
result_type: (&result_action).into(),
query_filter: self
.search_bar_state
.as_ref(ctx)
.active_visible_query_filter(),
query_filter: self.search_bar_state.as_ref(ctx).active_query_filter(),
buffer_length: self.search_bar.as_ref(ctx).query(ctx).len(),
was_immediately_executed,
},
@@ -1014,7 +1004,7 @@ impl View for CommandSearchView {
let appearance = Appearance::as_ref(app);
let mixer = self.mixer.as_ref(app);
let should_show_zero_state = self.search_bar_state.as_ref(app).should_show_zero_state();
let should_show_zero_state = self.search_bar.as_ref(app).should_show_zero_state(app);
let panel_contents_body = if should_show_zero_state {
ChildView::new(&self.zero_state_handle).finish()
} else if mixer.is_loading() && mixer.are_results_empty() {
@@ -1117,14 +1107,19 @@ impl CommandSearchView {
pub fn search_bar(&self) -> &ViewHandle<SearchBar<CommandSearchItemAction>> {
&self.search_bar
}
pub fn has_search_results(&self, app: &AppContext) -> bool {
self.search_bar_state
.as_ref(app)
.query_result_renderers()
.is_some_and(|results| !results.is_empty())
}
}
pub mod styles {
use galaxyui::elements::{Border, DropShadow, ScrollbarWidth};
use lazy_static::lazy_static;
use pathfinder_color::ColorU;
use crate::{appearance::Appearance, themes::theme::Fill};
use crate::themes::theme::Fill;
pub const CORNER_RADIUS: f32 = 8.;
pub const VIEW_WIDTH: f32 = 700.;
@@ -1161,5 +1156,5 @@ pub mod styles {
}
#[cfg(test)]
#[path = "view_test.rs"]
#[path = "view_tests.rs"]
mod tests;
@@ -1,24 +1,20 @@
use galaxyui::{platform::WindowStyle, App};
use crate::{
cloud_object::model::persistence::CloudModel,
network::NetworkStatus,
server::{
cloud_objects::{listener::Listener, update_manager::UpdateManager},
server_api::ServerApiProvider,
sync_queue::SyncQueue,
telemetry::context_provider::AppTelemetryContextProvider,
},
settings_view::keybindings::KeybindingChangedNotifier,
system::SystemStats,
test_util::settings::initialize_settings_for_tests,
workspaces::{
team_tester::TeamTesterStatus, update_manager::TeamUpdateManager,
user_workspaces::UserWorkspaces,
},
};
use galaxyui::platform::WindowStyle;
use galaxyui::App;
use super::*;
use crate::cloud_object::model::persistence::CloudModel;
use crate::network::NetworkStatus;
use crate::server::cloud_objects::listener::Listener;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::server_api::ServerApiProvider;
use crate::server::sync_queue::SyncQueue;
use crate::server::telemetry::context_provider::AppTelemetryContextProvider;
use crate::settings_view::keybindings::KeybindingChangedNotifier;
use crate::system::SystemStats;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::update_manager::TeamUpdateManager;
use crate::workspaces::user_workspaces::UserWorkspaces;
fn initialize_app(app: &mut App) {
initialize_settings_for_tests(app);
+24 -28
View File
@@ -1,29 +1,5 @@
use super::workflows::{WorkflowIdentity, WorkflowSearchItem};
use crate::{
ai::AIRequestUsageModel,
ai_assistant::{
execution_context::WarpAiExecutionContext, GenerateCommandsFromNaturalLanguageError,
AI_ASSISTANT_LOGO_COLOR,
},
appearance::Appearance,
features::FeatureFlag,
search::{
command_search::searcher::CommandSearchItemAction,
data_source::{Query, QueryResult},
item::SearchItem,
mixer::{
AsyncDataSource, BoxFuture, DataSourceRunError, DataSourceRunErrorWrapper,
SyncDataSource,
},
result_renderer::ItemHighlightState,
workflows::fuzzy_match::FuzzyMatchWorkflowResult,
},
server::server_api::ai::AIClient,
themes::theme::Blend,
ui_components::icons::Icon as UIIcon,
util::color::{ContrastingColor, MinimumAllowedContrast},
workflows::{AIWorkflowOrigin, WorkflowSource, WorkflowType},
};
use std::any::Any;
use std::sync::Arc;
use async_trait::async_trait;
use galaxy_core::ui::builder;
@@ -34,7 +10,28 @@ use galaxyui::{
use itertools::Itertools;
use ordered_float::OrderedFloat;
use serde_json::json;
use std::{any::Any, sync::Arc};
use galaxyui::elements::{ConstrainedBox, Container, Text};
use galaxyui::{AppContext, Element, SingletonEntity};
use super::workflows::{WorkflowIdentity, WorkflowSearchItem};
use crate::ai::AIRequestUsageModel;
use crate::ai_assistant::execution_context::WarpAiExecutionContext;
use crate::ai_assistant::{GenerateCommandsFromNaturalLanguageError, AI_ASSISTANT_LOGO_COLOR};
use crate::appearance::Appearance;
use crate::features::FeatureFlag;
use crate::search::command_search::searcher::CommandSearchItemAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::item::SearchItem;
use crate::search::mixer::{
AsyncDataSource, BoxFuture, DataSourceRunError, DataSourceRunErrorWrapper, SyncDataSource,
};
use crate::search::result_renderer::ItemHighlightState;
use crate::search::workflows::fuzzy_match::FuzzyMatchWorkflowResult;
use crate::server::server_api::ai::AIClient;
use crate::themes::theme::Blend;
use crate::ui_components::icons::Icon as UIIcon;
use crate::util::color::{ContrastingColor, MinimumAllowedContrast};
use crate::workflows::{AIWorkflowOrigin, WorkflowSource, WorkflowType};
const OPEN_WARP_AI_ITEM_BODY_TEXT: &str = "Ask Warp AI for command suggestions";
const TRANSLATE_WITH_WARP_AI_ITEM_BODY_TEXT: &str = "Translate into shell command using Warp AI";
@@ -260,7 +257,6 @@ impl DataSourceRunError for GenerateCommandsFromNaturalLanguageError {
}
mod styles {
use crate::appearance::Appearance;
/// Returns the icon size to be used for the 'sparkle' icon in the AI command search result.
/// The icon appeaars smaller than its size would indicate, so make a bit larger than icons
@@ -3,6 +3,7 @@ use std::sync::Arc;
use futures_lite::future::yield_now;
use galaxyui::{AppContext, SingletonEntity};
use super::WorkflowSearchItem;
use crate::cloud_object::model::persistence::CloudModel;
use crate::search::async_snapshot_data_source::AsyncSnapshotDataSource;
use crate::search::command_search::searcher::CommandSearchItemAction;
@@ -15,8 +16,6 @@ use crate::settings::AISettings;
use crate::workflows::{CloudWorkflowModel, WorkflowSource};
use crate::workspaces::user_workspaces::UserWorkspaces;
use super::WorkflowSearchItem;
pub(crate) struct WorkflowMatchCandidate {
pub id: SyncId,
pub model: Arc<CloudWorkflowModel>,
@@ -1,15 +1,13 @@
use std::sync::Arc;
use galaxyui::{
elements::{
Border, Clipped, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
Highlight, MainAxisAlignment, MainAxisSize, ParentElement, Radius, Shrinkable, Text,
},
fonts::{Properties, Weight},
ui_components::components::{UiComponent, UiComponentStyles},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
use galaxyui::elements::{
Border, Clipped, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Highlight,
MainAxisAlignment, MainAxisSize, ParentElement, Radius, Shrinkable, Text,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::appearance::Appearance;
use crate::search::command_search::searcher::{AcceptedWorkflow, CommandSearchItemAction};
@@ -1,22 +1,21 @@
use galaxyui::AppContext;
use itertools::Itertools;
use std::collections::HashMap;
use itertools::Itertools;
use warpui::{AppContext, SingletonEntity};
use super::{WorkflowIdentity, WorkflowSearchItem};
use crate::completer::SessionContext;
use crate::search::command_search::searcher::CommandSearchItemAction;
use crate::search::command_search::settings::CommandSearchSettings;
use crate::user_config::GalaxyConfig;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::search::workflows::fuzzy_match::FuzzyMatchWorkflowResult;
use crate::user_config::WarpConfig;
use crate::workflows::local_workflows::LocalWorkflows;
#[cfg(feature = "local_fs")]
use crate::workflows::local_workflows::UseCache;
use crate::workflows::workflow::Workflow;
use crate::workflows::{WorkflowSource, WorkflowType};
use galaxyui::SingletonEntity;
use super::{WorkflowIdentity, WorkflowSearchItem};
use crate::search::command_search::searcher::CommandSearchItemAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::search::workflows::fuzzy_match::FuzzyMatchWorkflowResult;
pub struct WorkflowsDataSource {
/// Contains workflows keyed by WorkflowSource.
+6 -11
View File
@@ -1,21 +1,17 @@
use std::collections::HashMap;
use lazy_static::lazy_static;
use galaxy_core::features::FeatureFlag;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::elements::Wrap;
use galaxyui::{
elements::{
Container, CornerRadius, Flex, Hoverable, MouseStateHandle, ParentElement, Radius, Text,
},
platform::Cursor,
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
use galaxyui::elements::{
Container, CornerRadius, Flex, Hoverable, MouseStateHandle, ParentElement, Radius, Text, Wrap,
};
use lazy_static::lazy_static;
use galaxyui::platform::Cursor;
use galaxyui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
use crate::appearance::Appearance;
use crate::drive::settings::{WarpDriveSettings, WarpDriveSettingsChangedEvent};
use crate::search::FilterChipRenderer;
use crate::search::QueryFilter;
use crate::search::{FilterChipRenderer, QueryFilter};
use crate::settings::{AISettings, AISettingsChangedEvent};
lazy_static! {
@@ -309,7 +305,6 @@ fn valid_query_filters(app: &AppContext) -> Vec<QueryFilter> {
}
mod styles {
use crate::appearance::Appearance;
pub const FILTER_CHIP_MARGIN: f32 = 8.;
pub const FILTER_CHIPS_MARGIN_BOTTOM: f32 = 16.;
-543
View File
@@ -1,543 +0,0 @@
use crate::search::item::IconLocation;
use crate::search::mixer::{DataSourceRunError, SyncDataSource};
use crate::search::result_renderer::ItemHighlightState;
use crate::{appearance::Appearance, ui_components::icons::Icon};
use enum_iterator::{all, Sequence};
use galaxy_core::features::FeatureFlag;
use galaxy_core::ui::theme::Fill;
use galaxyui::{Action, AppContext, Element, Entity, ModelHandle};
use lazy_static::lazy_static;
use ordered_float::OrderedFloat;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::any::Any;
use std::{collections::HashSet, sync::Arc};
use super::mixer::{AsyncDataSource, BoxFuture};
use super::{item::SearchItem, mixer::DataSourceRunErrorWrapper};
lazy_static! {
static ref HISTORY_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "history:",
aliases: vec!["h:"]
};
static ref WORKFLOWS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "workflows:",
aliases: vec!["w:"]
};
static ref AGENT_MODE_WORKFLOWS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "prompts:",
aliases: vec!["p:"]
};
static ref NOTEBOOKS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "notebooks:",
aliases: vec!["n:"]
};
static ref PLANS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "plans:",
aliases: vec![]
};
static ref NATURAL_LANGUAGE_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "#",
aliases: vec![]
};
static ref ACTIONS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "actions:",
aliases: vec![]
};
static ref DRIVE_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "drive:",
aliases: vec![]
};
static ref SESSIONS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "sessions:",
aliases: vec![]
};
static ref CONVERSATIONS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "conversations:",
aliases: vec![]
};
static ref LAUNCH_CONFIG_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "launch_configs:",
aliases: vec![]
};
static ref ENV_VARS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "env_vars:",
aliases: vec![]
};
static ref AI_PROMPTS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "ai_history:",
aliases: vec![]
};
static ref FILES_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "files:",
aliases: vec![]
};
static ref COMMANDS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "commands:",
aliases: vec![]
};
static ref BLOCKS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "blocks:",
aliases: vec!["b:"]
};
static ref CODE_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "code:",
aliases: vec![]
};
static ref RULES_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "rules:",
aliases: vec!["r:"]
};
static ref STATIC_SLASH_COMMANDS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "slash:",
aliases: vec![]
};
static ref REPOS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "repos:",
aliases: vec![]
};
static ref DIFFSETS_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "diffsets:",
aliases: vec!["diffs:"]
};
// If a query filter does not have a filter atom, it cannot be applied by typing
static ref NO_FILTER_ATOM: FilterAtom = FilterAtom {
primary_text: "",
aliases: vec![]
};
}
/// Represents a 'filter atom' that may be typed out in the search input to apply a filter.
pub struct FilterAtom {
/// The 'canonical' text representing the atom. This text is used for
/// autosuggestions/tab-completion to apply the filter. For example, this is 'history:' for the
/// history filter.
pub primary_text: &'static str,
/// Alternative strings that may be typed out in the search input to apply the filter. For
/// example, this is ['h:'] for the history filter.
pub aliases: Vec<&'static str>,
}
impl FilterAtom {
/// Returns the atom string that matches the given `query`, if any.
pub fn query_match(&self, query: &str) -> Option<&str> {
// If primary_text is empty, this is NO_ATOM, which never matches
if self.primary_text.is_empty() {
return None;
}
if query.starts_with(self.primary_text) {
Some(self.primary_text)
} else {
self.aliases
.iter()
.find(|alias| query.starts_with(**alias))
.copied()
}
}
}
/// Filters that may be included as part of the universal search query.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Sequence)]
pub enum QueryFilter {
/// Only include results from HistoryDataSource.
History,
/// Only include command workflows from WorkflowsDataSource.
Workflows,
/// Only include agent mode workflows (prompts) from WorkflowsDataSource.
AgentModeWorkflows,
/// Only include results from NotebooksDataSource.
Notebooks,
/// Only include results from PlansDataSource.
Plans,
/// Only include the Natural Language (AI) command search result.
NaturalLanguage,
/// Filter results for command palette actions.
Actions,
/// Filter results for open sessions.
Sessions,
/// Filter results for all conversations.
Conversations,
/// Filter results for only historical conversations. Used in the "View All" palette on new tabs
HistoricalConversations,
/// Filter results for launch configurations.
LaunchConfigurations,
/// Filter for objects in Warp Drive
Drive,
/// Filter results for environment variables.
EnvironmentVariables,
/// Filter results for historical AI history.
PromptHistory,
/// Filter results for files.
Files,
/// Filter results for commands.
Commands,
/// Filter results for terminal blocks.
Blocks,
/// Filter results for code symbols.
Code,
/// Filter results for AI rules.
Rules,
/// Filter results for known/indexed code repos.
Repos,
/// Filter results for diff sets.
DiffSets,
StaticSlashCommands,
/// Filter results for skills (used for browsing skills).
Skills,
/// Filter results for base agent models in the inline model selector.
BaseModels,
/// Filter results for full terminal use (CLI) models in the inline model selector.
FullTerminalUseModels,
/// Include only conversations whose most recent directory matches the session's current working directory.
CurrentDirectoryConversations,
}
impl QueryFilter {
/// Returns all possible `QueryFilter`s. Note all filters may not be enabled for a given
/// instance of a `SearchMixer`.
pub fn all() -> impl Iterator<Item = QueryFilter> {
all::<Self>()
}
/// Returns placeholder text to be shown in an empty input when the filter is active.
pub fn placeholder_text(&self) -> &'static str {
match self {
QueryFilter::History => "Search history",
QueryFilter::Workflows => "Search workflows",
QueryFilter::AgentModeWorkflows => "Search prompts",
QueryFilter::Notebooks => "Search notebooks",
QueryFilter::Plans => "Search plans",
QueryFilter::NaturalLanguage => "e.g. replace string in file",
QueryFilter::Actions => "Search actions",
QueryFilter::Sessions => "Search sessions",
QueryFilter::Conversations => "Search conversations",
QueryFilter::HistoricalConversations => "Search historical conversations",
QueryFilter::LaunchConfigurations => "Search launch configurations",
QueryFilter::Drive => "Search objects in drive",
QueryFilter::EnvironmentVariables => "Search environment variables",
QueryFilter::PromptHistory => "Search prompt history",
QueryFilter::Files => "Search files",
QueryFilter::Commands => "Search commands",
QueryFilter::Blocks => "Search blocks",
QueryFilter::Code => "Search code symbols",
QueryFilter::Rules => "Search AI rules",
QueryFilter::Repos => "Search code repos",
QueryFilter::DiffSets => "Search diff sets",
QueryFilter::StaticSlashCommands => "Search static slash commands",
QueryFilter::Skills => "Search skills",
QueryFilter::BaseModels => "Search base models",
QueryFilter::FullTerminalUseModels => "Search full terminal use models",
QueryFilter::CurrentDirectoryConversations => {
"Search conversations in current directory"
}
}
}
/// Returns text that is used to represent the filter as a filter 'atom' in the search input.
pub fn filter_atom(&self) -> &'static FilterAtom {
match self {
QueryFilter::History => &HISTORY_FILTER_ATOM,
QueryFilter::Workflows => &WORKFLOWS_FILTER_ATOM,
QueryFilter::AgentModeWorkflows => &AGENT_MODE_WORKFLOWS_FILTER_ATOM,
QueryFilter::Notebooks => &NOTEBOOKS_FILTER_ATOM,
QueryFilter::Plans => &PLANS_FILTER_ATOM,
QueryFilter::NaturalLanguage => &NATURAL_LANGUAGE_FILTER_ATOM,
QueryFilter::Actions => &ACTIONS_FILTER_ATOM,
QueryFilter::Sessions => &SESSIONS_FILTER_ATOM,
QueryFilter::Conversations => &CONVERSATIONS_FILTER_ATOM,
QueryFilter::LaunchConfigurations => &LAUNCH_CONFIG_FILTER_ATOM,
QueryFilter::Drive => &DRIVE_FILTER_ATOM,
QueryFilter::EnvironmentVariables => &ENV_VARS_FILTER_ATOM,
QueryFilter::PromptHistory => &AI_PROMPTS_FILTER_ATOM,
QueryFilter::Files => &FILES_FILTER_ATOM,
QueryFilter::Commands => &COMMANDS_FILTER_ATOM,
QueryFilter::Blocks => &BLOCKS_FILTER_ATOM,
QueryFilter::Code => &CODE_FILTER_ATOM,
QueryFilter::Rules => &RULES_FILTER_ATOM,
QueryFilter::Repos => &REPOS_FILTER_ATOM,
QueryFilter::DiffSets => &DIFFSETS_FILTER_ATOM,
QueryFilter::StaticSlashCommands => &STATIC_SLASH_COMMANDS_FILTER_ATOM,
QueryFilter::HistoricalConversations => &NO_FILTER_ATOM,
QueryFilter::Skills => &NO_FILTER_ATOM,
QueryFilter::BaseModels => &NO_FILTER_ATOM,
QueryFilter::FullTerminalUseModels => &NO_FILTER_ATOM,
QueryFilter::CurrentDirectoryConversations => &NO_FILTER_ATOM,
}
}
/// Returns the display name (e.g. the string to be used in UI) representing the filter.
pub fn display_name(&self) -> &'static str {
match self {
QueryFilter::History => "history",
QueryFilter::Workflows => "workflows",
QueryFilter::AgentModeWorkflows => "prompts",
QueryFilter::Notebooks => "notebooks",
QueryFilter::Plans => "plans",
QueryFilter::NaturalLanguage => "AI command suggestions",
QueryFilter::Actions => "actions",
QueryFilter::Sessions => "sessions",
QueryFilter::Conversations => "conversations",
QueryFilter::LaunchConfigurations => "launch configurations",
QueryFilter::Drive => "Galaxy Drive",
QueryFilter::EnvironmentVariables => "environment variables",
QueryFilter::PromptHistory => "prompt history",
QueryFilter::Files => "files",
QueryFilter::Commands => "commands",
QueryFilter::Blocks => "blocks",
QueryFilter::Code => "code",
QueryFilter::Rules => "rules",
QueryFilter::Repos => "repos",
QueryFilter::DiffSets => "diff sets",
QueryFilter::StaticSlashCommands => "slash commands",
QueryFilter::HistoricalConversations => "historical conversations",
QueryFilter::Skills => "skills",
QueryFilter::BaseModels => "base models",
QueryFilter::FullTerminalUseModels => "full terminal use models",
QueryFilter::CurrentDirectoryConversations => "current directory conversations",
}
}
/// Returns the path to the canonical icon for the filter.
pub fn icon_svg_path(&self) -> Option<&'static str> {
match self {
QueryFilter::History => Some("bundled/svg/history.svg"),
QueryFilter::Workflows => Some("bundled/svg/workflow.svg"),
QueryFilter::Notebooks => Some("bundled/svg/notebook.svg"),
QueryFilter::Plans => Some("bundled/svg/compass-3.svg"),
QueryFilter::NaturalLanguage => {
if !FeatureFlag::AgentMode.is_enabled() {
Some(Icon::AiAssistant.into())
} else {
Some(Icon::Oz.into())
}
}
QueryFilter::Actions => None,
QueryFilter::Sessions => Some("bundled/svg/terminal-input.svg"),
QueryFilter::Conversations | QueryFilter::HistoricalConversations => {
Some("bundled/svg/conversation.svg")
}
QueryFilter::LaunchConfigurations => Some("bundled/svg/navigation.svg"),
QueryFilter::Drive => Some("bundled/svg/warp-drive.svg"),
QueryFilter::EnvironmentVariables => Some("bundled/svg/env-var-collection.svg"),
QueryFilter::AgentModeWorkflows | QueryFilter::PromptHistory => {
Some(Icon::Prompt.into())
}
QueryFilter::Files => Some("bundled/svg/completion-file.svg"),
QueryFilter::Commands => Some("bundled/svg/terminal.svg"),
QueryFilter::Blocks => Some("bundled/svg/block.svg"),
QueryFilter::Code => Some("bundled/svg/code-02.svg"),
QueryFilter::Rules => Some("bundled/svg/book-open.svg"),
QueryFilter::Repos => Some("bundled/svg/folder.svg"),
QueryFilter::DiffSets => Some("bundled/svg/diff.svg"),
QueryFilter::StaticSlashCommands => None,
QueryFilter::Skills => None,
QueryFilter::BaseModels => None,
QueryFilter::FullTerminalUseModels => None,
QueryFilter::CurrentDirectoryConversations => None,
}
}
}
/// A structure representing a query that can be executed against a data source.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Query {
pub filters: HashSet<QueryFilter>,
pub text: String,
}
/// Allow anything that can be converted into a &str to be converted into a
/// Query.
impl<T> From<T> for Query
where
T: AsRef<str>,
{
fn from(s: T) -> Self {
Self {
filters: Default::default(),
text: s.as_ref().trim().to_owned(),
}
}
}
/// The type of a query result.
#[derive(Clone)]
pub struct QueryResult<T: Action + Clone> {
item: Arc<dyn SearchItem<Action = T>>,
/// Tiebreaker for sorting (results from earlier-registered data sources get a lower value
/// so they appear first among equal-scored results)
pub(crate) source_order: usize,
}
impl<T: Action + Clone> QueryResult<T> {
pub fn is_multiline(&self) -> bool {
self.item.is_multiline()
}
pub fn render_icon(
&self,
highlight_state: ItemHighlightState,
appearance: &Appearance,
) -> Box<dyn Element> {
self.item.render_icon(highlight_state, appearance)
}
pub fn icon_location(&self, appearance: &Appearance) -> IconLocation {
self.item.icon_location(appearance)
}
pub fn render_item(
&self,
highlight_state: ItemHighlightState,
app: &AppContext,
) -> Box<dyn Element> {
self.item.render_item(highlight_state, app)
}
pub fn item_background(
&self,
highlight_state: ItemHighlightState,
appearance: &Appearance,
) -> Option<Fill> {
self.item.item_background(highlight_state, appearance)
}
pub fn render_details(&self, ctx: &AppContext) -> Option<Box<dyn Element>> {
self.item.render_details(ctx)
}
pub fn priority_tier(&self) -> u8 {
self.item.priority_tier()
}
pub fn score(&self) -> OrderedFloat<f64> {
self.item.score()
}
pub fn accept_result(&self) -> T {
self.item.accept_result()
}
pub fn execute_result(&self) -> T {
self.item.execute_result()
}
pub fn accessibility_label(&self) -> String {
self.item.accessibility_label()
}
pub fn accessibility_help_message(&self) -> Option<String> {
self.item.accessibility_help_message()
}
/// Returns an optional deduplication key for this item from the [`SearchItem`].
pub fn dedup_key(&self) -> Option<String> {
self.item.dedup_key()
}
/// Returns whether this item is a static separator,
/// meaning it is a non-interactible item that should act as a simple UI element.
pub fn is_static_separator(&self) -> bool {
self.item.is_static_separator()
}
/// Returns whether this item is disabled.
/// Disabled items cannot be accepted or selected.
pub fn is_disabled(&self) -> bool {
self.item.is_disabled()
}
/// Returns an optional tooltip string to display when hovering over this item.
pub fn tooltip(&self) -> Option<String> {
self.item.tooltip()
}
}
impl<G: Action + Clone, T: SearchItem<Action = G> + 'static> From<T> for QueryResult<G> {
fn from(value: T) -> Self {
Self {
item: Arc::new(value),
source_order: usize::MAX,
}
}
}
/// Blanket impl of [`SyncDataSource`] for any [`ModelHandle`] of a type that also implements
/// `SyncDataSource`.
impl<T> SyncDataSource for ModelHandle<T>
where
T: SyncDataSource + Entity,
{
type Action = T::Action;
fn run_query(
&self,
query: &Query,
app: &AppContext,
) -> Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper> {
self.as_ref(app).run_query(query, app)
}
}
/// Blanket impl of [`AsyncDataSource`] for any [`ModelHandle`] of a type that also implements
/// `AsyncDataSource`.
impl<T> AsyncDataSource for ModelHandle<T>
where
T: AsyncDataSource + Entity,
{
type Action = T::Action;
fn run_query(
&self,
query: &Query,
app: &AppContext,
) -> BoxFuture<'static, Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper>> {
self.as_ref(app).run_query(query, app)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DataSourceSearchError {
pub(crate) message: String,
}
impl DataSourceRunError for DataSourceSearchError {
fn user_facing_error(&self) -> String {
self.message.clone()
}
fn telemetry_payload(&self) -> serde_json::Value {
json!(self)
}
fn as_any(&self) -> &dyn Any {
self
}
}
@@ -1,12 +1,11 @@
use crate::external_secrets::ExternalSecret;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use galaxyui::AppContext;
use itertools::Itertools;
use super::external_secret_fuzzy_match::FuzzyMatchExternalSecretResult;
use super::external_secret_search_item::ExternalSecretSearchItem;
use super::searcher::ExternalSecretSearchItemAction;
use crate::external_secrets::ExternalSecret;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
pub struct ExternalSecretDataSource {
secrets: Vec<ExternalSecret>,
@@ -1,22 +1,15 @@
use galaxyui::{
elements::{ConstrainedBox, Container, Highlight, Text},
fonts::{Properties, Weight},
AppContext, Element, SingletonEntity,
};
use ordered_float::OrderedFloat;
use galaxyui::elements::{ConstrainedBox, Container, Highlight, Text};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::{AppContext, Element, SingletonEntity};
use crate::search::item::SearchItem;
use super::external_secret_fuzzy_match::FuzzyMatchExternalSecretResult;
use super::searcher::ExternalSecretSearchItemAction;
use crate::appearance::Appearance;
use crate::external_secrets::{ExternalSecret, ExternalSecretManager};
use crate::search::external_secrets::view::styles;
use crate::search::item::{IconLocation, SearchItem};
use crate::search::result_renderer::ItemHighlightState;
use crate::{
appearance::Appearance,
external_secrets::{ExternalSecret, ExternalSecretManager},
search::{external_secrets::view::styles, item::IconLocation},
};
use super::{
external_secret_fuzzy_match::FuzzyMatchExternalSecretResult,
searcher::ExternalSecretSearchItemAction,
};
const ICON_SIZE: f32 = 16.;
+18 -21
View File
@@ -1,31 +1,28 @@
use std::collections::HashSet;
use std::ops::Range;
use itertools::Itertools;
use lazy_static::lazy_static;
use std::{collections::HashSet, ops::Range};
use galaxyui::elements::{
Align, ConstrainedBox, Container, CornerRadius, Dismiss, Empty, Fill, Flex, ParentElement,
Radius, SavePosition, ScrollStateHandle, Scrollable, ScrollableElement, Shrinkable,
UniformList, UniformListState,
};
use galaxyui::presenter::ChildView;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{
elements::{
Align, ConstrainedBox, Container, CornerRadius, Dismiss, Empty, Fill, Flex, ParentElement,
Radius, SavePosition, ScrollStateHandle, Scrollable, ScrollableElement, Shrinkable,
UniformList, UniformListState,
},
presenter::ChildView,
ui_components::components::{UiComponent, UiComponentStyles},
AppContext, Element, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use crate::{
appearance::Appearance,
external_secrets::ExternalSecret,
search::{
external_secrets::{
external_secret_data_source::ExternalSecretDataSource,
searcher::{ExternalSecretSearchItemAction, ExternalSecretSearchMixer},
},
result_renderer::{QueryResultRenderer, QueryResultRendererStyles},
search_bar::{SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering},
},
use crate::appearance::Appearance;
use crate::external_secrets::ExternalSecret;
use crate::search::external_secrets::external_secret_data_source::ExternalSecretDataSource;
use crate::search::external_secrets::searcher::{
ExternalSecretSearchItemAction, ExternalSecretSearchMixer,
};
use crate::search::result_renderer::{QueryResultRenderer, QueryResultRendererStyles};
use crate::search::search_bar::{SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering};
lazy_static! {
static ref QUERY_RESULT_RENDERER_STYLES: QueryResultRendererStyles =
@@ -369,7 +366,7 @@ pub mod styles {
use galaxyui::elements::{Border, DropShadow, ScrollbarWidth};
use pathfinder_color::ColorU;
use crate::{appearance::Appearance, themes::theme::Fill};
use crate::themes::theme::Fill;
pub const CORNER_RADIUS: f32 = 6.;
pub const VIEW_WIDTH: f32 = 450.;
+4 -2
View File
@@ -1,6 +1,8 @@
use crate::search::result_renderer::ItemHighlightState;
use galaxy_core::ui::appearance::Appearance;
use galaxyui::{elements::Icon, Element};
use galaxyui::elements::Icon;
use galaxyui::Element;
use crate::search::result_renderer::ItemHighlightState;
/// Assumes the path is a file, not a folder
pub fn icon_from_file_path(
+260 -125
View File
@@ -1,14 +1,13 @@
#![cfg_attr(not(feature = "local_fs"), allow(dead_code))]
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use fuzzy_match::{
contains_wildcards, match_indices_case_insensitive, match_wildcard_pattern_case_insensitive,
FuzzyMatchResult,
};
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity};
use std::sync::Arc;
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
cfg_if::cfg_if! {
if #[cfg(feature = "local_fs")] {
@@ -17,9 +16,11 @@ cfg_if::cfg_if! {
use repo_metadata::local_model::GetContentsArgs;
use repo_metadata::wrapper_model::RepoMetadataEvent;
use repo_metadata::RepoMetadataModel;
use repo_metadata::repository_identifier::RepositoryIdentifier;
use repo_metadata::repositories::DetectedRepositories;
use std::cell::RefCell;
use std::collections::HashMap;
use warp_util::local_or_remote_path::LocalOrRemotePath;
}
}
@@ -28,10 +29,10 @@ use super::search_item::FileSearchResult;
/// Shared model for file search functionality across different UI components.
/// This singleton provides common file discovery, fuzzy matching, and git integration.
pub struct FileSearchModel {
/// Cached flattened repo contents keyed by repo root path.
/// Cached flattened repo contents keyed by repo root location (local or remote).
/// Populated lazily on first query, invalidated when the file tree changes.
#[cfg(feature = "local_fs")]
repo_contents_cache: RefCell<HashMap<PathBuf, Arc<Vec<FileSearchResult>>>>,
repo_contents_cache: RefCell<HashMap<LocalOrRemotePath, Arc<Vec<FileSearchResult>>>>,
}
impl FileSearchModel {
@@ -40,32 +41,23 @@ impl FileSearchModel {
#[cfg(feature = "local_fs")]
ctx.subscribe_to_model(
&RepoMetadataModel::handle(ctx),
|me, event, _ctx| match event {
|me, _, event, _ctx| match event {
RepoMetadataEvent::FileTreeUpdated { ids } => {
let mut cache = me.repo_contents_cache.borrow_mut();
for id in ids {
if let repo_metadata::RepositoryIdentifier::Local(path) = id {
if let Some(local) = path.to_local_path() {
cache.remove(&local);
}
if let Some(key) = id.to_local_or_remote_path() {
cache.remove(&key);
}
}
}
RepoMetadataEvent::RepositoryRemoved { id } => {
if let repo_metadata::RepositoryIdentifier::Local(path) = id {
if let Some(local) = path.to_local_path() {
me.repo_contents_cache.borrow_mut().remove(&local);
}
}
}
RepoMetadataEvent::RepositoryUpdated { id } => {
if let repo_metadata::RepositoryIdentifier::Local(path) = id {
if let Some(local) = path.to_local_path() {
me.repo_contents_cache.borrow_mut().remove(&local);
}
RepoMetadataEvent::RepositoryRemoved { id }
| RepoMetadataEvent::RepositoryUpdated { id } => {
if let Some(key) = id.to_local_or_remote_path() {
me.repo_contents_cache.borrow_mut().remove(&key);
}
}
RepoMetadataEvent::FileTreeEntryUpdated { .. }
| RepoMetadataEvent::StandingQueryResultsUpdated { .. }
| RepoMetadataEvent::UpdatingRepositoryFailed { .. }
| RepoMetadataEvent::IncrementalUpdateReady { .. } => {}
},
@@ -84,12 +76,26 @@ impl FileSearchModel {
#[cfg(feature = "local_fs")]
pub fn repo_root(&self, app: &AppContext) -> Option<PathBuf> {
self.repo_root_location(app)
.and_then(|loc| PathBuf::try_from(loc).ok())
}
/// Returns the repo root as a `LocalOrRemotePath`, supporting both local and SSH sessions.
#[cfg(not(feature = "local_fs"))]
pub fn repo_root_location(
&self,
_app: &AppContext,
) -> Option<warp_util::local_or_remote_path::LocalOrRemotePath> {
None
}
/// Returns the repo root as a `LocalOrRemotePath`, supporting both local and SSH sessions.
#[cfg(feature = "local_fs")]
pub fn repo_root_location(&self, app: &AppContext) -> Option<LocalOrRemotePath> {
let active_window_id = app.windows().state().active_window;
let current_dir = active_window_id
.and_then(|window_id| ActiveSession::as_ref(app).path_if_local(window_id))?;
DetectedRepositories::as_ref(app).get_root_for_path(current_dir)
let working_dir =
active_window_id.and_then(|wid| ActiveSession::as_ref(app).working_directory(wid))?;
DetectedRepositories::as_ref(app).get_root_for_path(working_dir)
}
#[cfg(not(feature = "local_fs"))]
@@ -97,66 +103,116 @@ impl FileSearchModel {
Vec::new()
}
/// Fetches files and folders from the filesystem at a given path. This uses the ordering generated by
/// fs::read_dir, which is not guaranteed to be stable and in practice appears random.
/// Fetches files and folders from the current working directory (non-recursive).
///
/// For local sessions this reads the filesystem directly via `std::fs::read_dir`.
/// For remote sessions it queries the `RepoMetadataModel`, which receives
/// lazy-loaded first-level snapshots from the remote server on every
/// `NavigatedToDirectory`.
#[cfg(feature = "local_fs")]
pub fn get_folder_contents(&self, app: &AppContext) -> Vec<FileSearchResult> {
let active_window_id = app.windows().state().active_window;
let working_dir =
active_window_id.and_then(|wid| ActiveSession::as_ref(app).working_directory(wid));
let current_dir = match active_window_id
.and_then(|window_id| ActiveSession::as_ref(app).path_if_local(window_id))
{
Some(path) => path,
None => return Vec::new(),
};
match working_dir {
// Local session: read the filesystem directly.
Some(LocalOrRemotePath::Local(ref local_path)) => {
let current_dir: &Path = local_path.as_path();
let current_dir_string = current_dir.to_string_lossy().to_string();
let current_dir_string = current_dir.to_string_lossy().to_string();
match std::fs::read_dir(current_dir) {
Ok(entries) => entries
.filter_map(|entry| {
entry.ok().map(|entry| {
let path = entry.path();
FileSearchResult {
path: path
.strip_prefix(current_dir)
.expect("path should be a descendant of current_dir")
.to_string_lossy()
.to_string(),
project_directory: current_dir_string.clone(),
is_directory: path.is_dir(),
}
})
})
.collect(),
Err(err) => {
log::warn!("Failed to read {current_dir_string}: {err:#}");
Vec::new()
match std::fs::read_dir(current_dir) {
Ok(entries) => entries
.filter_map(|entry| {
entry.ok().map(|entry| {
let path = entry.path();
FileSearchResult {
path: path
.strip_prefix(current_dir)
.expect("path should be a descendant of current_dir")
.to_string_lossy()
.to_string(),
project_directory: current_dir_string.clone(),
is_directory: path.is_dir(),
}
})
})
.collect(),
Err(err) => {
log::warn!("Failed to read {current_dir_string}: {err:#}");
Vec::new()
}
}
}
// Remote session: query repo metadata (populated by the remote
// server's NavigatedToDirectory lazy-load).
Some(LocalOrRemotePath::Remote(ref remote_path)) => {
let id = RepositoryIdentifier::Remote(remote_path.clone());
let repo_metadata = RepoMetadataModel::as_ref(app);
// Truncated results (capped at the repo metadata budget) are
// intentionally used as-is to return partial matches rather
// than nothing.
let contents =
match repo_metadata.get_repo_contents(&id, GetContentsArgs::default(), app) {
Ok(repo_contents) => repo_contents.contents,
Err(_) => return Vec::new(),
};
let root_std_path = &remote_path.path;
contents
.iter()
.filter_map(|content| {
let (path_std, is_directory) = match content {
repo_metadata::RepoContent::File(file) => (&*file.path, false),
repo_metadata::RepoContent::Directory(dir) => (&*dir.path, true),
};
let relative = path_std.strip_prefix(root_std_path)?;
// Only include direct children (no nested paths).
let trimmed = relative.trim_end_matches('/');
if trimmed.is_empty() || trimmed.contains('/') {
return None;
}
let mut path = relative.to_owned();
if is_directory && !path.ends_with('/') {
path.push('/');
}
Some(FileSearchResult {
path,
project_directory: root_std_path.to_string(),
is_directory,
})
})
.collect()
}
None => Vec::new(),
}
}
/// Gets repository contents (files and directories) from the LocalRepoMetadataModel for the current working directory.
/// Results are cached per repo root and invalidated when the file tree changes.
/// Gets repository contents (files and directories) for the current working directory.
/// Supports both local and remote repos.
///
/// When `query` is non-empty, it is pushed down into the repo-metadata
/// traversal as a filter so the result cap applies to *matching* files
/// rather than the first files encountered in traversal order. These
/// query-specific results are not cached. When `query` is empty (zero
/// state) the full unfiltered contents are returned and cached per repo
/// root location, invalidated when the file tree changes.
#[cfg(feature = "local_fs")]
pub fn get_repo_contents(&self, app: &AppContext) -> Arc<Vec<FileSearchResult>> {
let Some(repo_root) = self.repo_root(app) else {
pub fn get_repo_contents(&self, query: &str, app: &AppContext) -> Arc<Vec<FileSearchResult>> {
let Some(repo_root) = self.repo_root_location(app) else {
return Arc::new(Vec::new());
};
// Query-filtered results are query-specific, so bypass the per-repo
// cache and traverse the in-memory index fresh.
if !query.is_empty() {
return Arc::new(self.get_contents_from_repo(&repo_root, query, app));
}
if let Some(cached) = self.repo_contents_cache.borrow().get(&repo_root) {
return cached.clone();
}
let repo_metadata = RepoMetadataModel::as_ref(app);
let Some(id) = repo_metadata::RepositoryIdentifier::try_local(&repo_root) else {
return Arc::new(Vec::new());
};
let contents = if repo_metadata.has_repository(&id, app) {
self.get_contents_from_repo(&repo_root, repo_metadata, GetContentsArgs::default(), app)
} else {
Vec::new()
};
let contents = self.get_contents_from_repo(&repo_root, query, app);
let arc = Arc::new(contents);
self.repo_contents_cache
@@ -172,7 +228,7 @@ impl FileSearchModel {
&self,
app: &AppContext,
) -> (Arc<Vec<FileSearchResult>>, HashSet<String>) {
let contents = self.get_repo_contents(app);
let contents = self.get_repo_contents("", app);
let git_changed_files = self
.repo_root(app)
.and_then(|repo_root| self.get_git_changed_files(&repo_root).ok())
@@ -182,7 +238,7 @@ impl FileSearchModel {
/// Gets repository contents from the LocalRepoMetadataModel for the current working directory (WASM stub)
#[cfg(not(feature = "local_fs"))]
pub fn get_repo_contents(&self, _app: &AppContext) -> Arc<Vec<FileSearchResult>> {
pub fn get_repo_contents(&self, _query: &str, _app: &AppContext) -> Arc<Vec<FileSearchResult>> {
Arc::new(Vec::new())
}
@@ -195,71 +251,150 @@ impl FileSearchModel {
(Arc::new(Vec::new()), HashSet::new())
}
/// Helper method to get repository contents from a specific repository
/// Builds the [`GetContentsArgs`] used to traverse repo metadata.
///
/// For an empty `query` this returns the default args (unfiltered). For a
/// non-empty `query` it installs a traversal filter that keeps only entries
/// whose repo-relative path (produced by `relative_path`) fuzzy-matches the
/// query. Pushing the query into traversal ensures the result cap applies
/// to *matching* files rather than the first files encountered.
#[cfg(feature = "local_fs")]
fn contents_args<F>(query: &str, relative_path: F) -> GetContentsArgs
where
F: for<'a> Fn(&repo_metadata::RepoContent<'a>) -> Option<String> + Send + Sync + 'static,
{
if query.is_empty() {
return GetContentsArgs::default();
}
let query = query.to_string();
GetContentsArgs::default().with_filter(move |content| {
relative_path(content)
.is_some_and(|path| FileSearchModel::fuzzy_match_path(&path, &query).is_some())
})
}
/// Gets repository contents for a local or remote repo root, converting
/// absolute paths to repo-relative `FileSearchResult`s.
///
/// When `query` is non-empty it is pushed down as a traversal filter (see
/// [`Self::contents_args`]) so the repo-metadata result cap applies to
/// matching files rather than the first files encountered in traversal
/// order.
#[cfg(feature = "local_fs")]
fn get_contents_from_repo(
&self,
repo_path: &Path,
repo_metadata: &repo_metadata::wrapper_model::RepoMetadataModel,
args: GetContentsArgs,
repo_root: &LocalOrRemotePath,
query: &str,
app: &AppContext,
) -> Vec<FileSearchResult> {
// Canonicalize the repository path to handle symlinks consistently
let Ok(canonical_repo_path) = dunce::canonicalize(repo_path) else {
return Vec::new();
};
let repo_metadata = RepoMetadataModel::as_ref(app);
let Some(id) = repo_metadata::RepositoryIdentifier::try_local(repo_path) else {
return Vec::new();
};
if let Some(contents) = repo_metadata.get_repo_contents(&id, args, app) {
contents
.iter()
.filter_map(|content| {
match content {
match repo_root {
LocalOrRemotePath::Local(local_path) => {
let Ok(canonical_repo_path) = dunce::canonicalize(local_path) else {
return Vec::new();
};
let Some(id) = RepositoryIdentifier::try_local(local_path) else {
return Vec::new();
};
let args = Self::contents_args(query, {
let canonical_repo_path = canonical_repo_path.clone();
move |content| {
let local = match content {
repo_metadata::RepoContent::File(file) => {
file.path.to_local_path_lossy()
}
repo_metadata::RepoContent::Directory(dir) => {
dir.path.to_local_path_lossy()
}
};
local
.strip_prefix(&canonical_repo_path)
.ok()
.map(|relative| relative.to_string_lossy().to_string())
}
});
// Truncated results (capped at the repo metadata budget) are
// intentionally used as-is to return partial matches rather
// than nothing.
let contents = match repo_metadata.get_repo_contents(&id, args, app) {
Ok(repo_contents) => repo_contents.contents,
Err(_) => return Vec::new(),
};
contents
.iter()
.filter_map(|content| match content {
repo_metadata::RepoContent::File(file_metadata) => {
let file_local = file_metadata.path.to_local_path_lossy();
// Convert absolute path to relative path from the canonical repository root
if let Ok(relative_path) = file_local.strip_prefix(&canonical_repo_path)
{
let path = relative_path.to_string_lossy().to_string();
Some(FileSearchResult {
path,
project_directory: canonical_repo_path
.to_string_lossy()
.to_string(),
is_directory: false,
})
} else {
None
}
let relative_path =
file_local.strip_prefix(&canonical_repo_path).ok()?;
Some(FileSearchResult {
path: relative_path.to_string_lossy().to_string(),
project_directory: canonical_repo_path
.to_string_lossy()
.to_string(),
is_directory: false,
})
}
repo_metadata::RepoContent::Directory(dir_entry) => {
let dir_local = dir_entry.path.to_local_path_lossy();
// Convert absolute path to relative path from the canonical repository root
if let Ok(relative_path) = dir_local.strip_prefix(&canonical_repo_path)
{
let mut path = relative_path.to_string_lossy().to_string();
// Add trailing slash for directories using platform-specific separator
if !path.ends_with(std::path::MAIN_SEPARATOR) {
path.push(std::path::MAIN_SEPARATOR);
}
Some(FileSearchResult {
path,
project_directory: canonical_repo_path
.to_string_lossy()
.to_string(),
is_directory: true,
})
} else {
None
let relative_path =
dir_local.strip_prefix(&canonical_repo_path).ok()?;
let mut path = relative_path.to_string_lossy().to_string();
if !path.ends_with(std::path::MAIN_SEPARATOR) {
path.push(std::path::MAIN_SEPARATOR);
}
Some(FileSearchResult {
path,
project_directory: canonical_repo_path
.to_string_lossy()
.to_string(),
is_directory: true,
})
}
})
.collect()
}
LocalOrRemotePath::Remote(remote_path) => {
let id = RepositoryIdentifier::Remote(remote_path.clone());
let args = Self::contents_args(query, {
let root = remote_path.path.clone();
move |content| {
let path_std = match content {
repo_metadata::RepoContent::File(file) => &*file.path,
repo_metadata::RepoContent::Directory(dir) => &*dir.path,
};
path_std.strip_prefix(&root).map(str::to_owned)
}
})
.collect()
} else {
Vec::new()
});
// Truncated results (capped at the repo metadata budget) are
// intentionally used as-is to return partial matches rather
// than nothing.
let contents = match repo_metadata.get_repo_contents(&id, args, app) {
Ok(repo_contents) => repo_contents.contents,
Err(_) => return Vec::new(),
};
let root_std_path = &remote_path.path;
contents
.iter()
.filter_map(|content| {
let (path_std, is_directory) = match content {
repo_metadata::RepoContent::File(file) => (&*file.path, false),
repo_metadata::RepoContent::Directory(dir) => (&*dir.path, true),
};
let relative = path_std.strip_prefix(root_std_path)?;
let mut path = relative.to_owned();
if is_directory && !path.ends_with('/') {
path.push('/');
}
Some(FileSearchResult {
path,
project_directory: root_std_path.to_string(),
is_directory,
})
})
.collect()
}
}
}
+6 -7
View File
@@ -1,10 +1,11 @@
use super::super::search_item::{FileSearchItem, FileSearchResult};
use super::FileSearchModel;
use fuzzy_match::FuzzyMatchResult;
use galaxyui::{App, SingletonEntity};
use repo_metadata::repositories::DetectedRepositories;
use repo_metadata::RepoMetadataModel;
use super::super::search_item::{FileSearchItem, FileSearchResult};
use super::FileSearchModel;
#[cfg(test)]
mod file_search_model_tests {
use super::*;
@@ -176,7 +177,6 @@ mod file_search_model_tests {
#[cfg(test)]
mod file_search_item_tests {
use super::*;
#[test]
fn test_file_search_item_from_result() {
@@ -252,9 +252,9 @@ mod file_search_item_tests {
#[cfg(test)]
mod strip_absolute_path_prefix_tests {
use super::*;
use std::path::{Path, PathBuf};
/// Builds an absolute path from the given components, using the platform's
/// root (`/` on Unix, `C:\` on Windows). This ensures the constructed
/// path is treated as absolute by `Path::is_absolute` on both platforms.
@@ -388,7 +388,6 @@ mod strip_absolute_path_prefix_tests {
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_typical_search_workflow() {
@@ -398,7 +397,7 @@ mod integration_tests {
"src/lib.rs",
"src/components/button.tsx",
"src/components/input.tsx",
"tests/integration_test.rs",
"tests/integration_tests.rs",
"README.md",
];
@@ -434,7 +433,7 @@ mod integration_tests {
"src/components/button.tsx",
"src/components/input.tsx",
"src/utils/button_helper.rs",
"tests/button_test.rs",
"tests/button_tests.rs",
];
let query = "comp button";
+4 -2
View File
@@ -1,5 +1,3 @@
use crate::appearance::Appearance;
use crate::search::QueryFilter;
use galaxy_core::ui::theme::color::internal_colors;
use galaxyui::elements::{
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Hoverable, Icon,
@@ -8,6 +6,9 @@ use galaxyui::elements::{
use galaxyui::platform::Cursor;
use galaxyui::{Element, EventContext};
use crate::appearance::Appearance;
use crate::search::QueryFilter;
/// Trait to render a filter chip.
pub trait FilterChipRenderer {
/// Returns how much larger the icon should be than the font size.
@@ -36,6 +37,7 @@ impl FilterChipRenderer for QueryFilter {
fn icon_margin_top(&self) -> f32 {
match self {
QueryFilter::Sessions => 2.,
QueryFilter::Tabs => 2.,
QueryFilter::NaturalLanguage => 2.,
_ => 0.,
}
-112
View File
@@ -1,112 +0,0 @@
use galaxy_core::ui::theme::Fill;
use galaxyui::{Action, AppContext, Element};
use ordered_float::OrderedFloat;
use crate::appearance::Appearance;
use super::result_renderer::ItemHighlightState;
/// Location where icon should be rendered relative to the [`SearchItem`].
pub enum IconLocation {
/// Icon should be centered within the element.
Centered,
/// Icon should be rendered at the top of the element, offset by `margin_top`.
Top { margin_top: f32 },
}
/// A trait representing a result from searching for a command.
pub trait SearchItem: Send + Sync {
/// The action that is dispatched when an item is accepted.
type Action: Action + Clone;
/// Returns whether this item should be treated as a multiline row.
///
/// This is used for styling decisions in renderers (e.g. applying extra vertical padding).
fn is_multiline(&self) -> bool {
false
}
/// Returns an [`Icon`] element to be rendered in a location determined by
/// [`SearchItem::icon_location`]
fn render_icon(
&self,
highlight_state: ItemHighlightState,
appearance: &Appearance,
) -> Box<dyn Element>;
/// Returns the location in which the icon should be rendered relative to the search item.
fn icon_location(&self, _appearance: &Appearance) -> IconLocation {
IconLocation::Centered
}
/// Returns an element to be rendered as the "body" of the item in the results list.
fn render_item(
&self,
highlight_state: ItemHighlightState,
app: &AppContext,
) -> Box<dyn Element>;
fn item_background(
&self,
highlight_state: ItemHighlightState,
appearance: &Appearance,
) -> Option<Fill> {
highlight_state.container_background_fill(appearance)
}
/// Optionally returns an [`Element`] to be rendered within a floating details panel when the
/// item is highlighted in the results list.
///
/// If this returns `None`, no details panel is shown for the item.
fn render_details(&self, _: &AppContext) -> Option<Box<dyn Element>> {
None
}
/// Returns a priority tier used to group result types.
///
/// Results are primarily ordered by this tier (higher tier wins). Scores are only compared
/// within the same tier.
fn priority_tier(&self) -> u8 {
0
}
/// Returns the "score" of the item used to rank the item in the results list.
fn score(&self) -> OrderedFloat<f64>;
/// Returns the [`CommandSearchItemAction`] to be emitted when the result is "accepted".
fn accept_result(&self) -> Self::Action;
/// Returns the [`CommandSearchItemAction`] to be emitted when the result is "executed".
fn execute_result(&self) -> Self::Action;
/// Returns the text that describes this item for accessibility purposes.
fn accessibility_label(&self) -> String;
/// Returns the a11y help message, if any, that describes this item.
fn accessibility_help_message(&self) -> Option<String> {
None
}
/// Returns an optional deduplication key for this item.
/// Items with the same deduplication key will be considered duplicates.
fn dedup_key(&self) -> Option<String> {
None
}
/// Returns whether this item is a static separator,
/// meaning it is a non-interactible item that should act as a simple UI element.
fn is_static_separator(&self) -> bool {
false
}
/// Returns whether this item is disabled.
/// Disabled items cannot be accepted or selected.
fn is_disabled(&self) -> bool {
false
}
/// Returns an optional tooltip string to display when hovering over this item.
fn tooltip(&self) -> Option<String> {
None
}
}
-228
View File
@@ -1,228 +0,0 @@
/// Converts Rust types to FullTextSearchFieldTypes variants
#[macro_export]
macro_rules! type_to_field_type {
($t:ty) => {
<$t as $crate::search::searcher::ToFieldType>::field_type()
};
}
pub use type_to_field_type;
#[macro_export]
macro_rules! data_from_owned_value {
($value:expr, $t:ty) => {
<$t as $crate::search::searcher::FromOwnedValue>::from_owned_value($value)
};
}
#[macro_export]
macro_rules! get_factor_or_default {
($factor:expr) => {
$factor
};
() => {
1.0
};
}
pub use get_factor_or_default;
/// Macro to define a search schema for a [`crate::search::searcher::SimpleFullTextSearcher`].
/// ### Parameters
/// * `schema_name` - The name of the schema. This would be the name of the static reference of the schema.
/// * `config_name` - The name of the generated type config corresponding to the defined schema.
/// * `search_doc` - The name of the search document struct. This is the type that the searcher expects when you insert
/// documents into the search index. This struct contains all the fields defined in the schema (both the search and
/// id fields).
/// * `identifying_doc` - The name of the identifying document struct. This is the type that the searcher expects when you
/// attempt to delete documents from the search index. This struct contains only the id fields defined in the schema.
/// It is expected that all the id fields in combination uniquely identify a document.
/// * `search_fields` - A list of fields that are searchable. Each field is a tuple of the field name and the weight.
/// The weight is used to determine the relevance of the field when searching. The higher the weight, the more relevant.
/// Note that the weights do not need to add up to 1 and will be normalized by the searcher.
/// * `id_fields` - A list of fields that are used to identify the document. These fields are not searchable and are the
/// "data" associated with the document. **It is expected that all the id fields together forms a uniquely-identifying key
/// of a document!** Failure to do so will result in unexpected behaviour when inserting and deleting documents.
/// ## Defining a new search schema
/// Here is an example of using this schema to create a simple searcher:
/// ```
/// use itertools::Itertools;
/// use galaxy::define_search_schema;
/// use galaxy::search::searcher::{SimpleFullTextSearcher, DEFAULT_MEMORY_BUDGET};
///
/// define_search_schema!(
/// schema_name: MY_SCHEMA,
/// config_name: MyConfig,
/// search_doc: MySearchDoc,
/// identifying_doc: MyIdDoc,
/// search_fields: [name: 1.0, description: 0.5],
/// id_fields: [id: u64]
/// );
///
/// struct SearchWrapper {
/// searcher: SimpleFullTextSearcher<MyConfig>,
/// }
///
/// struct SearchResult {
/// doc_id: usize,
/// /// Byte indices of highlighted matches in the name field.
/// name_highlights: Vec<usize>,
/// /// Byte indices of highlighted matches in the description field.
/// description_highlights: Vec<usize>,
/// /// Relevance score of the match.
/// score: f64,
/// }
///
/// impl SearchWrapper {
/// fn new(initial_index: impl IntoIterator<Item = (String, String, u64)>) -> anyhow::Result<Self> {
/// let searcher = MY_SCHEMA.create_searcher(DEFAULT_MEMORY_BUDGET);
/// searcher.build_index(initial_index.into_iter().map(|(name, description, id)| {
/// MySearchDoc { name, description, id }
/// }))?;
///
/// Ok(Self { searcher })
/// }
///
/// fn add_document(&mut self, name: String, description: String, id: u64) -> anyhow::Result<()> {
/// self.searcher.insert_document(MySearchDoc { name, description, id })
/// }
///
/// fn remove_document_by_id(&mut self, id: u64) -> anyhow::Result<()> {
/// self.searcher.delete_document(MyIdDoc { id })
/// }
///
/// fn search(&self, query: &str) -> anyhow::Result<Vec<SearchResult>> {
/// Ok(self.searcher
/// .search_full_doc(query)?
/// .into_iter()
/// .map(|match_result| {
/// SearchResult {
/// doc_id: match_result.values.id as usize,
/// name_highlights: match_result.highlights.name,
/// description_highlights: match_result.highlights.description,
/// score: match_result.score,
/// }
/// })
/// .sorted_by(|a, b| a.score.partial_cmp(&b.score).unwrap_or(std::cmp::Ordering::Equal))
/// .collect())
/// }
/// }
/// ```
#[macro_export]
macro_rules! define_search_schema {
(schema_name: $schema_name:ident, config_name: $config_name:ident, search_doc: $search_doc:ident, identifying_doc: $id_doc_name:ident, search_fields: [$($s_name:ident: $weight:literal$(,)?)*], id_fields: [$($i_name:ident: $value_type:ty$(,)?)*] $(, boost_factor: $boost_factor:expr)? $(,)?) => {
lazy_static::lazy_static! {
static ref $schema_name: $crate::search::searcher::FullTextSearchSchema<$config_name> = $crate::search::searcher::FullTextSearchSchema::new(
std::collections::HashMap::from([
$((stringify!($s_name).to_owned(), $weight)),*
]),
std::collections::HashMap::from([
$((stringify!($i_name).to_owned(), $crate::type_to_field_type!($value_type))),*
]),
$crate::get_factor_or_default!($($boost_factor)*),
);
}
#[derive(Debug, Clone)]
struct $search_doc {
$(
pub $s_name: String,
)*
$(
pub $i_name: $value_type
),*
}
impl $crate::search::searcher::SearchDocumentEntry for $search_doc {
fn into_document_entry(self) -> $crate::search::searcher::FullTextSearchDocumentEntry {
let mut entry = std::collections::HashMap::new();
$(
entry.insert(
stringify!($s_name).to_owned(),
self.$s_name.into(),
);
)*
$(
entry.insert(
stringify!($i_name).to_owned(),
self.$i_name.into(),
);
)*
entry
}
}
impl $crate::search::searcher::FullTextSearchMatchValues for $search_doc {
fn from_match_result_values(mut values: std::collections::HashMap<String, tantivy::schema::OwnedValue>) -> Option<Self> {
Some(Self {
$(
$s_name: $crate::data_from_owned_value!(values.remove(stringify!($s_name))?, String)?,
)*
$(
$i_name: $crate::data_from_owned_value!(values.remove(stringify!($i_name))?, $value_type)?
),*
})
}
}
#[allow(unused)]
#[derive(Debug, Clone)]
struct $id_doc_name {
$(
pub $i_name: $value_type
),*
}
#[allow(unused)]
impl $crate::search::searcher::SearchIdentifyingEntry for $id_doc_name {
fn into_identifying_entry(self) -> $crate::search::searcher::FullTextSearchDocumentEntry {
let mut entry = std::collections::HashMap::new();
$(
entry.insert(
stringify!($i_name).to_owned(),
self.$i_name.into(),
);
)*
entry
}
}
#[allow(unused)]
impl $crate::search::searcher::FullTextSearchMatchValues for $id_doc_name {
fn from_match_result_values(mut values: std::collections::HashMap<String, tantivy::schema::OwnedValue>) -> Option<Self> {
Some(Self {
$(
$i_name: $crate::data_from_owned_value!(values.remove(stringify!($i_name))?, $value_type)?,
)*
})
}
}
paste::paste! {
#[allow(unused)]
#[derive(Debug, Clone)]
struct [<_ $config_name HighlightResult>] {
$(
pub $s_name: Vec<usize>
),*
}
#[allow(unused)]
impl $crate::search::searcher::FullTextSearchMatchHighlights for [<_ $config_name HighlightResult>] {
fn from_match_result_highlights(mut highlights: std::collections::HashMap<String, Vec<usize>>) -> Option<Self> {
Some(Self {
$(
$s_name: highlights.remove(stringify!($s_name))?,
)*
})
}
}
struct $config_name;
impl $crate::search::searcher::SearchSchemaConfig for $config_name {
type SearchDocEntry = $search_doc;
type SearchIdEntry = $id_doc_name;
type SearchHighlight = [<_ $config_name HighlightResult>];
}
}
};
}
pub use define_search_schema;
-665
View File
@@ -1,665 +0,0 @@
use super::data_source::{Query, QueryResult};
use crate::debounce::debounce;
use crate::search::QueryFilter;
use crate::send_telemetry_from_ctx;
use crate::server::telemetry::TelemetryEvent;
use async_channel::Sender;
use async_trait::async_trait;
use futures_util::stream::AbortHandle;
use galaxyui::r#async::Timer;
use galaxyui::{Action, AppContext, Entity, ModelContext};
use itertools::Itertools;
use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
/// Maximum time to wait for matching data sources to return results before showing
/// partial results.
///
/// This is a UX tradeoff: waiting briefly reduces flicker in UIs that mix sync and async
/// sources (e.g. command palette file search), but we still want to show something quickly
/// if an async source is slow.
const INITIAL_RESULTS_TIMEOUT: Duration = Duration::from_millis(500);
#[cfg(not(target_family = "wasm"))]
pub(crate) type BoxFuture<'a, T> =
std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
#[cfg(target_family = "wasm")]
pub(crate) type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + 'a>>;
#[derive(Debug, Clone, Default)]
pub enum DedupeStrategy {
#[default]
AllowDuplicates,
HighestScore,
}
/// Deduplicate the results list based on provided keys, if any, and keep the highest score,
/// while preserving the original order of the kept items.
pub fn dedupe_score<T: Action + Clone>(original: Vec<QueryResult<T>>) -> Vec<QueryResult<T>> {
let mut deduped_results: Vec<(Option<String>, &QueryResult<T>)> = Vec::new();
for result in original.iter() {
let mut needs_insert = true;
let new_key = result.dedup_key();
if new_key.is_some() {
for (existing_key, existing_result) in deduped_results.iter_mut() {
// Note: at this point, new_key must be Some(str)
if new_key == *existing_key {
// This does not need to be inserted - either replace or discard
needs_insert = false;
if result.score() > existing_result.score() {
*existing_result = result;
}
break;
}
}
}
if needs_insert {
deduped_results.push((new_key, result));
}
}
deduped_results
.into_iter()
.map(|(_, r)| r.clone())
.collect()
}
/// A structure that combines results from various data sources to produce a
/// single, ordered, heterogeneous list of search results.
#[derive(Default)]
pub struct SearchMixer<T: Action + Clone> {
/// The set of sources to be used to run a query against.
sources: HashMap<DataSourceId, RegisteredDataSource<T>>,
/// The latest set of search results produced by the latest `query`.
results: Vec<QueryResult<T>>,
/// The latest query that was used to search against, if any.
query: Option<Query>,
/// The set of sources that have finished running for the latest query.
finished_sources: HashSet<DataSourceId>,
/// The strategy for deduplication
dedupe_strategy: DedupeStrategy,
/// Monotonically increasing counter incremented on each `run_query`. Used to discard stale
/// async callbacks and timeout callbacks whose futures completed before the abort took effect.
query_generation: u64,
/// Results buffered for the current query that haven't been committed to results yet.
/// `Some(vec)` means we're actively buffering (old results remain visible).
/// `None` means results have been committed; late-arriving results go directly to `results`.
pending_results: Option<Vec<QueryResult<T>>>,
/// Tracks whether the current query has emitted its initial set of visible results yet.
initial_results_emitted: bool,
}
impl<T: Action + Clone> Entity for SearchMixer<T> {
type Event = SearchMixerEvent;
}
/// A unique identifier for a DataSource.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
struct DataSourceId(usize);
impl DataSourceId {
/// Constructs a new globally-unique entity ID.
#[allow(clippy::new_without_default)]
pub fn new() -> DataSourceId {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
let raw = NEXT_ID.fetch_add(1, Ordering::Relaxed);
DataSourceId(raw)
}
}
pub enum SearchMixerEvent {
ResultsChanged,
}
pub struct AddAsyncSourceOptions {
pub debounce_interval: Option<Duration>,
/// Whether to run this source when the query text is empty
/// (i.e. the user hasn't typed anything yet).
pub run_in_zero_state: bool,
pub run_when_unfiltered: bool,
}
impl<T: Action + Clone> SearchMixer<T> {
pub fn new() -> Self {
Self {
sources: HashMap::new(),
finished_sources: HashSet::new(),
results: vec![],
query: None,
dedupe_strategy: DedupeStrategy::AllowDuplicates,
query_generation: 0,
pending_results: None,
initial_results_emitted: false,
}
}
/// Set the deduplication strategy for the mixer
pub fn set_dedupe_strategy(&mut self, strategy: DedupeStrategy) {
self.dedupe_strategy = strategy;
}
/// Resets the mixer's state.
pub fn reset(&mut self, ctx: &mut ModelContext<Self>) {
self.abort_in_flight_async_queries();
self.query_generation = self.query_generation.wrapping_add(1);
self.sources.clear();
self.finished_sources.clear();
self.results.clear();
self.pending_results = None;
self.query.take();
self.initial_results_emitted = false;
ctx.emit(SearchMixerEvent::ResultsChanged);
}
/// Abort the current in-flight query to avoid stale searches
/// continuing and passing back results when they are no longer wanted.
fn abort_in_flight_async_queries(&mut self) {
for registered_source in self.sources.values_mut() {
if let DataSource::AsyncDataSource {
latest_run_abort_handle,
..
} = &mut registered_source.source
{
if let Some(abort_handle) = latest_run_abort_handle.take() {
abort_handle.abort();
}
}
}
}
/// Resets the mixer's results. Use the all-encompassing [`reset`] API
/// to clear _all_ of the mixer's state.
pub fn reset_results(&mut self, ctx: &mut ModelContext<Self>) {
self.abort_in_flight_async_queries();
self.query_generation = self.query_generation.wrapping_add(1);
self.results.clear();
self.pending_results = None;
self.query.take();
self.initial_results_emitted = false;
ctx.emit(SearchMixerEvent::ResultsChanged);
}
/// Adds a [`SyncDataSource`] to produce results when the mixer is queried. Query results will
/// be produced from this source if there are no filters provided or if one of the filters
/// within a [`Query`] is equal to this filter.
pub fn add_sync_source(
&mut self,
source: impl SyncDataSource<Action = T>,
filters: impl Into<HashSet<QueryFilter>>,
) {
self.sources.insert(
DataSourceId::new(),
RegisteredDataSource::new(
DataSource::SyncDataSource {
source: Arc::new(source),
},
filters.into(),
),
);
}
/// Adds an [`AsyncDataSource`] to produce results when the mixer is queried.
/// The results will be produced asynchronously and the mixer will notify its
/// subscribers whenever the result set changes.
///
/// A debounce interval can be provided to only query the data source in a debounced fashion.
///
/// By default, async sources only run when the query's filters explicitly match. Set
/// `run_when_unfiltered` to `true` so the source also runs when `query.filters` is empty.
/// Only enable this when the source's work is cheap (e.g. local fuzzy matching) — expensive
/// operations like network requests should not run on every unfiltered keystroke.
pub fn add_async_source(
&mut self,
source: impl AsyncDataSource<Action = T>,
filters: impl Into<HashSet<QueryFilter>>,
options: AddAsyncSourceOptions,
ctx: &mut ModelContext<Self>,
) {
let source = Arc::new(source);
let data_source_id = DataSourceId::new();
let debounce_tx = options.debounce_interval.map(|interval| {
self.start_debounce_stream_for_data_source(data_source_id, interval, ctx)
});
self.sources.insert(
data_source_id,
RegisteredDataSource::new(
DataSource::AsyncDataSource {
source,
debounce_tx,
latest_run_abort_handle: None,
run_in_zero_state: options.run_in_zero_state,
run_when_unfiltered: options.run_when_unfiltered,
},
filters.into(),
),
);
}
pub fn current_query(&self) -> Option<&Query> {
self.query.as_ref()
}
/// Runs a query against the registered data sources using the provided Query configuration.
/// On completion, the mixer emits an event to subscribers to indicate the result set has changed.
///
/// Old results remain visible while new results are buffered. The visible result set is
/// replaced atomically once all sources finish, or after [`INITIAL_RESULTS_TIMEOUT`] elapses.
/// Late-arriving async results are appended without reordering existing results.
pub fn run_query(&mut self, query: Query, ctx: &mut ModelContext<Self>) {
self.pending_results = Some(Vec::new());
self.finished_sources.clear();
self.query = Some(query.clone());
self.query_generation = self.query_generation.wrapping_add(1);
self.initial_results_emitted = false;
let query = &query;
// We want to run the queries in the order that the data sources were added.
let data_source_ids_to_run = self.ordered_data_source_ids_for_query(query).collect_vec();
for id in data_source_ids_to_run {
self.run_query_internal(id, false, ctx);
}
// Sync sources (and skipped async sources) will have already finished
// inside the loop. If everything is done, commit immediately.
if self.pending_results.is_some() {
if !self.is_loading() {
self.commit_pending_results_for_current_query(ctx);
} else {
let query_generation = self.query_generation;
let _ = ctx.spawn(
async move { Timer::after(INITIAL_RESULTS_TIMEOUT).await },
move |mixer, _, ctx| {
mixer.commit_pending_results_after_timeout(query_generation, ctx);
},
);
}
}
}
pub fn results(&self) -> &Vec<QueryResult<T>> {
&self.results
}
pub fn are_results_empty(&self) -> bool {
self.results.is_empty()
}
/// Returns all the filters that are currently registered.
pub fn registered_filters(&self) -> impl Iterator<Item = QueryFilter> + '_ {
self.sources
.values()
.flat_map(|source| source.filters.clone())
}
/// Returns the query filter for the first data source that hasn't completed.
pub fn loading_query_filters(&self) -> Option<HashSet<QueryFilter>> {
if self.initial_results_emitted {
return None;
}
let query = self.query.as_ref()?;
self.ordered_data_source_ids_for_query(query)
.find(|id| !self.finished_sources.contains(id))
.and_then(|id| self.sources.get(&id))
.map(|data_source| data_source.filters.clone())
}
/// Returns true iff there is at least one loading data source.
/// Helper that computes over `loading_query_filter`.
pub fn is_loading(&self) -> bool {
self.loading_query_filters().is_some()
}
/// Returns the first error found from running the data sources against the query, if any.
pub fn first_data_source_error(
&self,
) -> Option<(HashSet<QueryFilter>, &DataSourceRunErrorWrapper)> {
let query = self.query.as_ref()?;
self.ordered_data_source_ids_for_query(query)
.find_map(|id| {
self.sources
.get(&id)
.and_then(|s| Some(s.filters.clone()).zip(s.latest_run_error.as_ref()))
})
}
/// Returns an ordered list of data source IDs in the order that the corresponding
/// data sources were registered in.
/// We could use a map that respects insertion order but that will likely be
// overkill since the number of data sources is usually minute.
fn ordered_data_source_ids_for_query<'a>(
&'a self,
query: &'a Query,
) -> impl Iterator<Item = DataSourceId> + 'a {
self.sources
.keys()
.sorted()
.filter(|id| {
self.sources
.get(id)
.is_some_and(|registered_source| registered_source.matches_query(query))
})
.copied()
}
/// Runs the query for the [`DataSource`] identified by the provided `data_source_id`.
/// If `skip_debounce` is true, then the query is started immediately even if queries
/// against the data source are meant to be debounced.
fn run_query_internal(
&mut self,
data_source_id: DataSourceId,
skip_debounce: bool,
ctx: &mut ModelContext<Self>,
) {
let Some(registered_source) = self.sources.get_mut(&data_source_id) else {
return;
};
let Some(query) = self.query.clone() else {
return;
};
// Clear the latest run error, if any, because we're about to run a new query.
registered_source.latest_run_error = None;
match &mut registered_source.source {
DataSource::SyncDataSource { source } => {
let new_results = source.run_query(&query, ctx);
self.add_new_results(data_source_id, new_results, ctx);
}
DataSource::AsyncDataSource {
source,
debounce_tx,
latest_run_abort_handle,
run_in_zero_state,
run_when_unfiltered: _,
} => {
// Abort any existing run before starting a new one.
// This is necessary to do even if we end up debouncing
// because there might already be a running query that's taking long.
if let Some(abort_handle) = latest_run_abort_handle.take() {
abort_handle.abort();
}
// Only run async sources in the zero state if the async source indicated it should run in the
// zero state when registered. It can be costly to run async sources on blank queries so we don't
// do this by default.
if query.text.is_empty() && !*run_in_zero_state {
self.mark_source_as_finished(data_source_id);
if self.pending_results.is_some() && !self.is_loading() {
self.commit_pending_results_for_current_query(ctx);
}
return;
}
// Check if we should just be debouncing the query rather than running it right now.
if let Some(debounce_tx) = debounce_tx {
if !skip_debounce {
let _ = debounce_tx.try_send(DataSourceDebounceArg {});
return;
}
}
// If we get here, then we should run the query against the data source right now.
let query_generation = self.query_generation;
let source = source.clone();
let filters = registered_source.filters.to_owned();
let new_abort_handle = ctx.spawn(
source.run_query(&query, ctx),
move |mixer, new_results, ctx| {
// Discard results from a previous query whose future completed before
// the abort took effect.
if mixer.query_generation != query_generation {
source.on_query_finished(ctx);
return;
}
let error_payload =
new_results.as_ref().err().map(|e| e.telemetry_payload());
send_telemetry_from_ctx!(
TelemetryEvent::CommandSearchAsyncQueryCompleted {
filters,
error_payload,
},
ctx
);
mixer.add_new_results(data_source_id, new_results, ctx);
source.on_query_finished(ctx);
},
);
*latest_run_abort_handle = Some(new_abort_handle.abort_handle());
}
}
}
fn add_new_results(
&mut self,
data_source_id: DataSourceId,
new_results: Result<Vec<QueryResult<T>>, DataSourceRunErrorWrapper>,
ctx: &mut ModelContext<Self>,
) {
if self.finished_sources.contains(&data_source_id) {
log::warn!(
"Ignoring duplicate results for source {data_source_id:?} that was already marked finished"
);
return;
}
self.mark_source_as_finished(data_source_id);
match new_results {
Ok(results) => {
let results_with_order = results
.into_iter()
.map(|mut result| {
result.source_order = data_source_id.0;
result
})
.collect_vec();
if let Some(pending) = &mut self.pending_results {
pending.extend(results_with_order);
if !self.is_loading() {
self.commit_pending_results_for_current_query(ctx);
}
} else if self.initial_results_emitted {
let mut late_results = results_with_order;
late_results.sort_by_key(|r| (r.priority_tier(), r.score(), r.source_order));
self.results.extend(late_results);
if matches!(self.dedupe_strategy, DedupeStrategy::HighestScore) {
self.results = dedupe_score(std::mem::take(&mut self.results));
}
ctx.emit(SearchMixerEvent::ResultsChanged);
} else {
self.results.extend(results_with_order);
self.sort_and_dedupe_results();
ctx.emit(SearchMixerEvent::ResultsChanged);
}
}
Err(e) => {
if let Some(source) = self.sources.get_mut(&data_source_id) {
source.latest_run_error = Some(e);
}
if self.pending_results.is_some() && !self.is_loading() {
self.commit_pending_results_for_current_query(ctx);
} else if self.pending_results.is_none() {
ctx.emit(SearchMixerEvent::ResultsChanged);
}
}
}
}
/// Commits buffered results from the current query, replacing the visible result set.
/// After this, any late-arriving results are added directly to `results`.
fn commit_pending_results(&mut self, ctx: &mut ModelContext<Self>) {
let Some(pending) = self.pending_results.take() else {
return;
};
self.results = pending;
self.sort_and_dedupe_results();
ctx.emit(SearchMixerEvent::ResultsChanged);
}
fn commit_pending_results_for_current_query(&mut self, ctx: &mut ModelContext<Self>) {
self.initial_results_emitted = true;
self.commit_pending_results(ctx);
}
/// Sort by (priority_tier, score, source_order) so that equal-scored results
/// from earlier-registered sources appear first, regardless of async completion order.
fn sort_and_dedupe_results(&mut self) {
self.results
.sort_by_key(|r| (r.priority_tier(), r.score(), r.source_order));
if matches!(self.dedupe_strategy, DedupeStrategy::HighestScore) {
self.results = dedupe_score(std::mem::take(&mut self.results));
}
}
fn mark_source_as_finished(&mut self, data_source_id: DataSourceId) {
self.finished_sources.insert(data_source_id);
}
fn commit_pending_results_after_timeout(
&mut self,
query_generation: u64,
ctx: &mut ModelContext<Self>,
) {
if query_generation != self.query_generation || self.pending_results.is_none() {
return;
}
self.commit_pending_results_for_current_query(ctx);
}
fn start_debounce_stream_for_data_source(
&mut self,
data_source_id: DataSourceId,
interval: Duration,
ctx: &mut ModelContext<Self>,
) -> Sender<DataSourceDebounceArg> {
let (debounce_tx, debounce_rx) = async_channel::unbounded();
let _ = ctx.spawn_stream_local(
debounce(interval, debounce_rx),
move |mixer, _, ctx| {
mixer.run_query_internal(data_source_id, true, ctx);
},
|_, _| {},
);
debounce_tx
}
}
/// A trait representing a set of data that can be queried for search results synchronously.
pub trait SyncDataSource: 'static {
/// The action that is dispatched when a result produced by this data source is
/// accepted.
type Action: Action + Clone;
fn run_query(
&self,
query: &Query,
app: &AppContext,
) -> Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper>;
}
/// A trait representing a set of data that can be queried for search results asynchronously.
#[cfg_attr(not(target_family = "wasm"), async_trait)]
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
pub trait AsyncDataSource: 'static + Send + Sync {
/// The action that is dispatched when a result produced by this data source is
/// accepted.
type Action: Action + Clone;
fn run_query(
&self,
query: &Query,
app: &AppContext,
) -> BoxFuture<'static, Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper>>;
/// Function that should be run in the callback after `run_query` finishes.
fn on_query_finished(&self, _ctx: &mut AppContext) {}
}
/// Helper type alias for a DataSourceRunError.
pub type DataSourceRunErrorWrapper = Box<dyn DataSourceRunError>;
pub trait DataSourceRunError: 'static + Send + Sync + std::fmt::Debug {
fn user_facing_error(&self) -> String;
fn telemetry_payload(&self) -> serde_json::Value;
fn as_any(&self) -> &dyn Any;
}
struct DataSourceDebounceArg {}
enum DataSource<T: Action + Clone> {
SyncDataSource {
source: Arc<dyn SyncDataSource<Action = T>>,
},
AsyncDataSource {
latest_run_abort_handle: Option<AbortHandle>,
source: Arc<dyn AsyncDataSource<Action = T>>,
debounce_tx: Option<Sender<DataSourceDebounceArg>>,
run_in_zero_state: bool,
run_when_unfiltered: bool,
},
}
/// A registered [`DataSource`] for a [`SearchMixer`].
struct RegisteredDataSource<T: Action + Clone> {
source: DataSource<T>,
/// Corresponding filter for this data source.
filters: HashSet<QueryFilter>,
/// The error produced by this data source during its last run.
latest_run_error: Option<DataSourceRunErrorWrapper>,
}
impl<T: Action + Clone> RegisteredDataSource<T> {
/// Sync sources always run when the query has no filters. Async sources only run on
/// unfiltered queries when `run_when_unfiltered` is set, to avoid running expensive
/// operations (e.g. network requests) on every keystroke.
fn matches_query(&self, query: &Query) -> bool {
match &self.source {
DataSource::SyncDataSource { .. } => {
query.filters.is_empty() || query.filters.intersection(&self.filters).count() > 0
}
DataSource::AsyncDataSource {
run_when_unfiltered,
..
} => {
(*run_when_unfiltered && query.filters.is_empty())
|| query.filters.intersection(&self.filters).count() > 0
}
}
}
}
impl<T: Action + Clone> RegisteredDataSource<T> {
fn new(source: DataSource<T>, filters: HashSet<QueryFilter>) -> Self {
Self {
source,
filters,
latest_run_error: None,
}
}
}
#[cfg(test)]
#[path = "mixer_test.rs"]
mod mixer_test;

Some files were not shown because too many files have changed in this diff Show More