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.
This commit is contained in:
SteveLauC
2025-10-19 09:58:01 +08:00
committed by GitHub
parent be6611133a
commit cd00ada3ac
5 changed files with 71 additions and 19 deletions

View File

@@ -17,6 +17,7 @@ feat: support switching groups via keyboard shortcuts #911
feat: support opening logs from about page #915 feat: support opening logs from about page #915
feat: support moving cursor with home and end keys #918 feat: support moving cursor with home and end keys #918
feat: support pageup/pagedown to navigate search results #920 feat: support pageup/pagedown to navigate search results #920
feat: return sub-exts when extension type exts themselves are matched #928
### 🐛 Bug fix ### 🐛 Bug fix

View File

@@ -28,6 +28,7 @@ use tauri_plugin_global_shortcut::GlobalShortcutExt;
use tauri_plugin_global_shortcut::ShortcutState; use tauri_plugin_global_shortcut::ShortcutState;
pub(crate) const EXTENSION_ID: &str = "Window Management"; pub(crate) const EXTENSION_ID: &str = "Window Management";
pub(crate) const EXTENSION_NAME_LOWERCASE: &str = "window management";
/// JSON file for this extension. /// JSON file for this extension.
pub(crate) const PLUGIN_JSON_FILE: &str = include_str!("./plugin.json"); pub(crate) const PLUGIN_JSON_FILE: &str = include_str!("./plugin.json");

View File

@@ -1,4 +1,5 @@
use super::EXTENSION_ID; use super::EXTENSION_ID;
use super::EXTENSION_NAME_LOWERCASE;
use crate::common::document::{DataSourceReference, Document}; use crate::common::document::{DataSourceReference, Document};
use crate::common::{ use crate::common::{
error::SearchError, 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 score
}; };

View File

@@ -15,6 +15,7 @@ use crate::common::search::QuerySource;
use crate::common::search::SearchQuery; use crate::common::search::SearchQuery;
use crate::common::traits::SearchSource; use crate::common::traits::SearchSource;
use crate::extension::ExtensionBundleIdBorrowed; use crate::extension::ExtensionBundleIdBorrowed;
use crate::extension::ExtensionType;
use crate::extension::calculate_text_similarity; use crate::extension::calculate_text_similarity;
use crate::extension::canonicalize_relative_page_path; use crate::extension::canonicalize_relative_page_path;
use crate::util::platform::Platform; use crate::util::platform::Platform;
@@ -757,11 +758,22 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
for extension in extensions_read_lock.iter().filter(|ext| ext.enabled) { for extension in extensions_read_lock.iter().filter(|ext| ext.enabled) {
if extension.r#type.contains_sub_items() { 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 { if let Some(ref commands) = extension.commands {
for command in commands.iter().filter(|cmd| cmd.enabled) { for command in commands.iter().filter(|cmd| cmd.enabled) {
if let Some(hit) = if let Some(hit) = extension_to_hit(
extension_to_hit(command, &query_lower, opt_data_source.as_deref()) command,
{ &query_lower,
opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(),
) {
hits.push(hit); hits.push(hit);
} }
} }
@@ -769,9 +781,12 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
if let Some(ref scripts) = extension.scripts { if let Some(ref scripts) = extension.scripts {
for script in scripts.iter().filter(|script| script.enabled) { for script in scripts.iter().filter(|script| script.enabled) {
if let Some(hit) = if let Some(hit) = extension_to_hit(
extension_to_hit(script, &query_lower, opt_data_source.as_deref()) script,
{ &query_lower,
opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(),
) {
hits.push(hit); hits.push(hit);
} }
} }
@@ -783,6 +798,7 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
quicklink, quicklink,
&query_lower, &query_lower,
opt_data_source.as_deref(), opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(),
) { ) {
hits.push(hit); hits.push(hit);
} }
@@ -791,16 +807,19 @@ impl SearchSource for ThirdPartyExtensionsSearchSource {
if let Some(ref views) = extension.views { if let Some(ref views) = extension.views {
for view in views.iter().filter(|link| link.enabled) { for view in views.iter().filter(|link| link.enabled) {
if let Some(hit) = if let Some(hit) = extension_to_hit(
extension_to_hit(view, &query_lower, opt_data_source.as_deref()) view,
{ &query_lower,
opt_data_source.as_deref(),
opt_main_extension_lowercase_name.as_deref(),
) {
hits.push(hit); hits.push(hit);
} }
} }
} }
} else { } else {
if let Some(hit) = 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); hits.push(hit);
} }
@@ -839,10 +858,18 @@ pub(crate) async fn uninstall_extension(
.await .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( pub(crate) fn extension_to_hit(
extension: &Extension, extension: &Extension,
query_lower: &str, query_lower: &str,
opt_data_source: Option<&str>, opt_data_source: Option<&str>,
opt_main_extension_lowercase_name: Option<&str>,
) -> Option<(Document, f64)> { ) -> Option<(Document, f64)> {
if !extension.searchable() { if !extension.searchable() {
return None; return None;
@@ -865,14 +892,26 @@ pub(crate) fn extension_to_hit(
if let Some(title_score) = if let Some(title_score) =
calculate_text_similarity(&query_lower, &extension.name.to_lowercase()) 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 // Score based on alias match if available
// Alias is considered less important than title, so it gets a lower weight. // Alias is considered less important than title, so it gets a lower weight.
if let Some(alias) = &extension.alias { if let Some(alias) = &extension.alias {
if let Some(alias_score) = calculate_text_similarity(&query_lower, &alias.to_lowercase()) { 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;
} }
} }

View File

@@ -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 * Sort hits within each source by score (descending) in case data sources
* do not sort them * 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(); let mut final_hits = Vec::new();
for (_source_id, hits) in final_hits_grouped_by_source_id { for (_source_id, hits) in final_hits_grouped_by_source_id {
final_hits.extend(hits); final_hits.extend(hits);