From cd00ada3ac028ffb9fdfa4e4f8992cad7e06f439 Mon Sep 17 00:00:00 2001 From: SteveLauC Date: Sun, 19 Oct 2025 09:58:01 +0800 Subject: [PATCH] feat: return sub-exts when extension type exts themselves are matched (#928) Take the 'Spotify Control' extension as an example: - Spotify Control - Toggle Play/Pause - Next Track - Previous Track Previously, these sub-extensions were only returned when the query string matched them, and thus counterintuitively, searching for 'Spotify Control' would not hit them. This commit changes that behavior: when a main extension (of type Extension) matches the query, all of its sub-extensions are now included in the results. --- docs/content.en/docs/release-notes/_index.md | 1 + .../built_in/window_management/mod.rs | 1 + .../window_management/search_source.rs | 11 ++++ src-tauri/src/extension/third_party/mod.rs | 63 +++++++++++++++---- src-tauri/src/search/mod.rs | 14 ++--- 5 files changed, 71 insertions(+), 19 deletions(-) diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index a159ab90..bbb80e79 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -17,6 +17,7 @@ feat: support switching groups via keyboard shortcuts #911 feat: support opening logs from about page #915 feat: support moving cursor with home and end keys #918 feat: support pageup/pagedown to navigate search results #920 +feat: return sub-exts when extension type exts themselves are matched #928 ### 🐛 Bug fix diff --git a/src-tauri/src/extension/built_in/window_management/mod.rs b/src-tauri/src/extension/built_in/window_management/mod.rs index bff19e05..1cc187c1 100644 --- a/src-tauri/src/extension/built_in/window_management/mod.rs +++ b/src-tauri/src/extension/built_in/window_management/mod.rs @@ -28,6 +28,7 @@ use tauri_plugin_global_shortcut::GlobalShortcutExt; use tauri_plugin_global_shortcut::ShortcutState; pub(crate) const EXTENSION_ID: &str = "Window Management"; +pub(crate) const EXTENSION_NAME_LOWERCASE: &str = "window management"; /// JSON file for this extension. pub(crate) const PLUGIN_JSON_FILE: &str = include_str!("./plugin.json"); diff --git a/src-tauri/src/extension/built_in/window_management/search_source.rs b/src-tauri/src/extension/built_in/window_management/search_source.rs index 5d7ab5f7..6b53dafa 100644 --- a/src-tauri/src/extension/built_in/window_management/search_source.rs +++ b/src-tauri/src/extension/built_in/window_management/search_source.rs @@ -1,4 +1,5 @@ use super::EXTENSION_ID; +use super::EXTENSION_NAME_LOWERCASE; use crate::common::document::{DataSourceReference, Document}; use crate::common::{ error::SearchError, @@ -81,6 +82,16 @@ impl SearchSource for WindowManagementSearchSource { } } + // An "extension" type extension should return all its + // sub-extensions when the query string matches its name. + // To do this, we score the extension name and take that + // into account. + if let Some(main_extension_score) = + calculate_text_similarity(&query_string_lowercase, &EXTENSION_NAME_LOWERCASE) + { + score += main_extension_score; + } + score }; diff --git a/src-tauri/src/extension/third_party/mod.rs b/src-tauri/src/extension/third_party/mod.rs index 65f2e8ab..c2c33b53 100644 --- a/src-tauri/src/extension/third_party/mod.rs +++ b/src-tauri/src/extension/third_party/mod.rs @@ -15,6 +15,7 @@ use crate::common::search::QuerySource; use crate::common::search::SearchQuery; use crate::common::traits::SearchSource; use crate::extension::ExtensionBundleIdBorrowed; +use crate::extension::ExtensionType; use crate::extension::calculate_text_similarity; use crate::extension::canonicalize_relative_page_path; use crate::util::platform::Platform; @@ -757,11 +758,22 @@ impl SearchSource for ThirdPartyExtensionsSearchSource { for extension in extensions_read_lock.iter().filter(|ext| ext.enabled) { if extension.r#type.contains_sub_items() { + let opt_main_extension_lowercase_name = + if extension.r#type == ExtensionType::Extension { + Some(extension.name.to_lowercase()) + } else { + // None if it is of type `ExtensionType::Group` + None + }; + if let Some(ref commands) = extension.commands { for command in commands.iter().filter(|cmd| cmd.enabled) { - if let Some(hit) = - extension_to_hit(command, &query_lower, opt_data_source.as_deref()) - { + if let Some(hit) = extension_to_hit( + command, + &query_lower, + opt_data_source.as_deref(), + opt_main_extension_lowercase_name.as_deref(), + ) { hits.push(hit); } } @@ -769,9 +781,12 @@ impl SearchSource for ThirdPartyExtensionsSearchSource { if let Some(ref scripts) = extension.scripts { for script in scripts.iter().filter(|script| script.enabled) { - if let Some(hit) = - extension_to_hit(script, &query_lower, opt_data_source.as_deref()) - { + if let Some(hit) = extension_to_hit( + script, + &query_lower, + opt_data_source.as_deref(), + opt_main_extension_lowercase_name.as_deref(), + ) { hits.push(hit); } } @@ -783,6 +798,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource { quicklink, &query_lower, opt_data_source.as_deref(), + opt_main_extension_lowercase_name.as_deref(), ) { hits.push(hit); } @@ -791,16 +807,19 @@ impl SearchSource for ThirdPartyExtensionsSearchSource { if let Some(ref views) = extension.views { for view in views.iter().filter(|link| link.enabled) { - if let Some(hit) = - extension_to_hit(view, &query_lower, opt_data_source.as_deref()) - { + if let Some(hit) = extension_to_hit( + view, + &query_lower, + opt_data_source.as_deref(), + opt_main_extension_lowercase_name.as_deref(), + ) { hits.push(hit); } } } } else { if let Some(hit) = - extension_to_hit(extension, &query_lower, opt_data_source.as_deref()) + extension_to_hit(extension, &query_lower, opt_data_source.as_deref(), None) { hits.push(hit); } @@ -839,10 +858,18 @@ pub(crate) async fn uninstall_extension( .await } +/// Argument `opt_main_extension_lowercase_name`: If `extension` is a sub-extension +/// of an `extension` type extension, then this argument contains the lowercase +/// name of that extension. Otherwise, None. +/// +/// This argument is needed as an "extension" type extension should return all its +/// sub-extensions when the query string matches its name. To do this, we pass the +/// extension name, score it and take that into account. pub(crate) fn extension_to_hit( extension: &Extension, query_lower: &str, opt_data_source: Option<&str>, + opt_main_extension_lowercase_name: Option<&str>, ) -> Option<(Document, f64)> { if !extension.searchable() { return None; @@ -865,14 +892,26 @@ pub(crate) fn extension_to_hit( if let Some(title_score) = calculate_text_similarity(&query_lower, &extension.name.to_lowercase()) { - total_score += title_score * 1.0; // Weight for title + total_score += title_score; } // Score based on alias match if available // Alias is considered less important than title, so it gets a lower weight. if let Some(alias) = &extension.alias { if let Some(alias_score) = calculate_text_similarity(&query_lower, &alias.to_lowercase()) { - total_score += alias_score * 0.7; // Weight for alias + total_score += alias_score; + } + } + + // An "extension" type extension should return all its + // sub-extensions when the query string matches its ID. + // To do this, we score the extension ID and take that + // into account. + if let Some(main_extension_lowercase_id) = opt_main_extension_lowercase_name { + if let Some(main_extension_score) = + calculate_text_similarity(&query_lower, main_extension_lowercase_id) + { + total_score += main_extension_score; } } diff --git a/src-tauri/src/search/mod.rs b/src-tauri/src/search/mod.rs index 6a0fc508..ddf11f76 100644 --- a/src-tauri/src/search/mod.rs +++ b/src-tauri/src/search/mod.rs @@ -265,13 +265,6 @@ async fn query_coco_fusion_multi_query_sources( }); } - /* - * Re-rank the hits - */ - if n_sources > 1 { - boosted_levenshtein_rerank(&query_keyword, &mut all_hits_grouped_by_source_id); - } - /* * Sort hits within each source by score (descending) in case data sources * do not sort them @@ -363,6 +356,13 @@ async fn query_coco_fusion_multi_query_sources( } } + /* + * Re-rank the final hits + */ + if n_sources > 1 { + boosted_levenshtein_rerank(&query_keyword, &mut final_hits_grouped_by_source_id); + } + let mut final_hits = Vec::new(); for (_source_id, hits) in final_hits_grouped_by_source_id { final_hits.extend(hits);