Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 118 additions & 21 deletions src/common/query.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
use crate::actions::Action;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActionFilterMetadata {
pub normalized_id: String,
pub normalized_kind_candidates: Vec<String>,
}

impl ActionFilterMetadata {
pub fn from_action(action: &Action) -> Self {
let mut normalized_kind_candidates = Vec::new();
if !action.desc.trim().is_empty() {
normalized_kind_candidates.push(action.desc.trim().to_lowercase());
}
if let Some(prefix) = action.action.split(':').next() {
if !prefix.trim().is_empty() {
normalized_kind_candidates.push(prefix.trim().to_lowercase());
}
}
normalized_kind_candidates.sort();
normalized_kind_candidates.dedup();

Self {
normalized_id: action.action.to_lowercase(),
normalized_kind_candidates,
}
}
}

#[derive(Debug, Clone)]
pub struct ActionWithMetadata {
pub action: Action,
pub metadata: ActionFilterMetadata,
}

impl ActionWithMetadata {
pub fn from_action(action: Action) -> Self {
let metadata = ActionFilterMetadata::from_action(&action);
Self { action, metadata }
}
}

#[derive(Debug, Default, PartialEq, Eq)]
pub struct QueryFilters {
pub remaining_tokens: Vec<String>,
Expand Down Expand Up @@ -245,15 +285,29 @@ pub fn rebuild_query(tokens: &[String]) -> String {
}

pub fn apply_action_filters(actions: Vec<Action>, filters: &QueryFilters) -> Vec<Action> {
apply_action_filters_with_metadata(
actions
.into_iter()
.map(ActionWithMetadata::from_action)
.collect(),
filters,
)
}

pub fn apply_action_filters_with_metadata(
actions: Vec<ActionWithMetadata>,
filters: &QueryFilters,
) -> Vec<Action> {
actions
.into_iter()
.filter(|action| action_matches_filters(action, filters))
.filter(|cached| action_matches_filters(&cached.metadata, filters))
.map(|cached| cached.action)
.collect()
}

fn action_matches_filters(action: &Action, filters: &QueryFilters) -> bool {
let action_id = action.action.to_lowercase();
let kind_candidates = action_kind_candidates(action);
pub fn action_matches_filters(metadata: &ActionFilterMetadata, filters: &QueryFilters) -> bool {
let action_id = &metadata.normalized_id;
let kind_candidates = &metadata.normalized_kind_candidates;

if !filters.include_kinds.is_empty()
&& !filters
Expand All @@ -272,32 +326,17 @@ fn action_matches_filters(action: &Action, filters: &QueryFilters) -> bool {
return false;
}

if !filters.include_ids.is_empty() && !filters.include_ids.iter().any(|id| action_id == *id) {
if !filters.include_ids.is_empty() && !filters.include_ids.iter().any(|id| action_id == id) {
return false;
}

if filters.exclude_ids.iter().any(|id| action_id == *id) {
if filters.exclude_ids.iter().any(|id| action_id == id) {
return false;
}

true
}

fn action_kind_candidates(action: &Action) -> Vec<String> {
let mut kinds = Vec::new();
if !action.desc.trim().is_empty() {
kinds.push(action.desc.trim().to_lowercase());
}
if let Some(prefix) = action.action.split(':').next() {
if !prefix.trim().is_empty() {
kinds.push(prefix.trim().to_lowercase());
}
}
kinds.sort();
kinds.dedup();
kinds
}

fn split_negation(token: &str) -> (&str, bool) {
token
.strip_prefix('!')
Expand Down Expand Up @@ -383,4 +422,62 @@ mod tests {
assert_eq!(filters.include_kinds, vec!["todo"]);
assert_eq!(filters.exclude_ids, vec!["todo:done:1"]);
}

#[test]
fn action_matches_filters_uses_metadata_for_id_and_kind() {
let action = Action {
label: "Task".into(),
desc: "Todo".into(),
action: "todo:item:1".into(),
args: None,
};
let metadata = ActionFilterMetadata::from_action(&action);

let include_match = QueryFilters {
include_ids: vec!["todo:item:1".into()],
include_kinds: vec!["todo".into()],
..QueryFilters::default()
};
assert!(action_matches_filters(&metadata, &include_match));

let include_miss = QueryFilters {
include_ids: vec!["todo:item:2".into()],
..QueryFilters::default()
};
assert!(!action_matches_filters(&metadata, &include_miss));

let exclude_hit = QueryFilters {
exclude_ids: vec!["todo:item:1".into()],
..QueryFilters::default()
};
assert!(!action_matches_filters(&metadata, &exclude_hit));
}
#[test]
fn apply_action_filters_with_metadata_filters_without_recomputing() {
let keep = Action {
label: "Keep".into(),
desc: "Todo".into(),
action: "todo:item:1".into(),
args: None,
};
let drop = Action {
label: "Drop".into(),
desc: "Note".into(),
action: "note:item:2".into(),
args: None,
};
let actions = vec![
ActionWithMetadata::from_action(keep.clone()),
ActionWithMetadata::from_action(drop),
];
let filters = QueryFilters {
include_ids: vec!["todo:item:1".into()],
include_kinds: vec!["todo".into()],
..QueryFilters::default()
};

let filtered = apply_action_filters_with_metadata(actions, &filters);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].action, keep.action);
}
}
27 changes: 24 additions & 3 deletions src/gui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub use volume_dialog::VolumeDialog;
use crate::actions::folders;
use crate::actions::{load_actions, Action};
use crate::actions_editor::ActionsEditor;
use crate::common::query::split_action_filters;
use crate::common::query::{action_matches_filters, split_action_filters, ActionFilterMetadata};
use crate::dashboard::config::DashboardConfig;
use crate::dashboard::widgets::{WidgetRegistry, WidgetSettingsContext};
use crate::dashboard::{
Expand Down Expand Up @@ -436,6 +436,7 @@ pub struct LauncherApp {
/// actions are edited the entire `Arc` is replaced with a new one.
pub actions: Arc<Vec<Action>>,
action_cache: Vec<CachedSearchEntry>,
action_filter_metadata: Vec<ActionFilterMetadata>,
actions_by_id: HashMap<String, Action>,
command_cache: Vec<Action>,
command_search_cache: Vec<CachedSearchEntry>,
Expand Down Expand Up @@ -724,6 +725,11 @@ impl LauncherApp {
.iter()
.map(CachedSearchEntry::from_action)
.collect();
self.action_filter_metadata = self
.actions
.iter()
.map(ActionFilterMetadata::from_action)
.collect();
self.actions_by_id = self
.actions
.iter()
Expand Down Expand Up @@ -1644,6 +1650,7 @@ impl LauncherApp {
confirm_modal: ConfirmationModal::default(),
pending_confirm: None,
action_cache: Vec::new(),
action_filter_metadata: Vec::new(),
actions_by_id,
command_cache: Vec::new(),
command_search_cache: Vec::new(),
Expand Down Expand Up @@ -1759,12 +1766,26 @@ impl LauncherApp {
self.recompute_query_results_layout();
}

fn search_actions(&self, query: &str, query_lc: &str) -> Vec<(Action, f32)> {
fn search_actions(&self, query: &str, _query_lc: &str) -> Vec<(Action, f32)> {
let (filtered_query, filters) = split_action_filters(query);
let filtered_query = filtered_query.trim();
let filtered_query_lc = filtered_query.to_lowercase();
let query = filtered_query;
let query_lc = filtered_query_lc.as_str();

let mut res = Vec::new();
if query.is_empty() {
res.extend(self.actions.iter().cloned().map(|a| (a, 0.0)));
for (i, a) in self.actions.iter().enumerate() {
if action_matches_filters(&self.action_filter_metadata[i], &filters) {
res.push((a.clone(), 0.0));
}
}
} else {
for (i, a) in self.actions.iter().enumerate() {
if !action_matches_filters(&self.action_filter_metadata[i], &filters) {
continue;
}

let cached = &self.action_cache[i];
if self.is_exact_match_mode() {
let alias_match = self.alias_matches_lc(&a.action, query_lc);
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/omni_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ mod tests {
let mut value = json!("invalid");
let ctx = egui::Context::default();

ctx.run(Default::default(), |ctx| {
let _ = ctx.run(Default::default(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
plugin.settings_ui(ui, &mut value);
});
Expand Down
Loading