Settings: Settings search fixes (#41381)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Fix 3 issues

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41369, #41374, #41380
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed


https://github.com/user-attachments/assets/0e0df9fb-5aca-4b26-9d53-e6ddc49cab04

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Jiří Polášek <me@jiripolasek.com>
This commit is contained in:
Kai Tao
2025-08-27 14:51:36 +08:00
committed by GitHub
parent bb6b36af3f
commit 7455d63bb5
7 changed files with 349 additions and 157 deletions

View File

@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -142,8 +141,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private const int SearchDebounceMs = 500;
private bool _disposed;
// Tracing id for correlating logs of a single search interaction
private static long _searchTraceIdCounter;
// Removed trace id counter per cleanup
/// <summary>
/// Initializes a new instance of the <see cref="ShellPage"/> class.
@@ -443,25 +441,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void ShellPage_Loaded(object sender, RoutedEventArgs e)
{
Logger.LogDebug("[Search][Index] Scheduling BuildIndex...");
var swIndex = Stopwatch.StartNew();
Task.Run(() =>
{
Logger.LogDebug("[Search][Index] BuildIndex started");
SearchIndexService.BuildIndex();
})
.ContinueWith(t =>
{
swIndex.Stop();
if (t.IsFaulted)
{
Logger.LogDebug($"[Search][Index] BuildIndex FAILED after {swIndex.ElapsedMilliseconds} ms: {t.Exception?.Flatten().InnerException?.Message}");
}
else
{
Logger.LogDebug($"[Search][Index] BuildIndex completed in {swIndex.ElapsedMilliseconds} ms.");
}
});
.ContinueWith(_ => { });
}
private void NavigationView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
@@ -512,10 +496,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var query = sender.Text?.Trim() ?? string.Empty;
var traceId = Interlocked.Increment(ref _searchTraceIdCounter);
var swOverall = Stopwatch.StartNew();
Logger.LogDebug($"[Search][TextChanged][{traceId}] start. query='{query}'");
// Debounce: cancel previous pending search
_searchDebounceCts?.Cancel();
_searchDebounceCts?.Dispose();
@@ -528,7 +508,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
sender.IsSuggestionListOpen = false;
_lastSearchResults.Clear();
_lastQueryText = string.Empty;
Logger.LogDebug($"[Search][TextChanged][{traceId}] empty query. end");
return;
}
@@ -538,14 +517,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
catch (TaskCanceledException)
{
// A newer keystroke arrived; abandon this run
Logger.LogDebug($"[Search][TextChanged][{traceId}] debounce canceled at +{swOverall.ElapsedMilliseconds} ms");
return;
return; // debounce canceled
}
if (token.IsCancellationRequested)
{
Logger.LogDebug($"[Search][TextChanged][{traceId}] token canceled post-debounce at +{swOverall.ElapsedMilliseconds} ms");
return;
}
@@ -554,106 +530,25 @@ namespace Microsoft.PowerToys.Settings.UI.Views
try
{
// If the token is already canceled before scheduling, the task won't start.
var swSearch = Stopwatch.StartNew();
Logger.LogDebug($"[Search][TextChanged][{traceId}] dispatch search...");
results = await Task.Run(() => SearchIndexService.Search(query, token), token);
swSearch.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] search done in {swSearch.ElapsedMilliseconds} ms. results={results?.Count ?? 0}");
}
catch (OperationCanceledException)
{
Logger.LogDebug($"[Search][TextChanged][{traceId}] search canceled at +{swOverall.ElapsedMilliseconds} ms");
return;
}
if (token.IsCancellationRequested)
{
Logger.LogDebug($"[Search][TextChanged][{traceId}] token canceled after search at +{swOverall.ElapsedMilliseconds} ms");
return;
}
_lastSearchResults = results;
_lastQueryText = query;
List<SuggestionItem> top;
if (results.Count == 0)
{
// Explicit no-results row
var rl = ResourceLoaderInstance.ResourceLoader;
var noResultsPrefix = rl.GetString("Shell_Search_NoResults");
if (string.IsNullOrEmpty(noResultsPrefix))
{
noResultsPrefix = "No results for";
}
var top = BuildSuggestionItems(query, results);
var headerText = $"{noResultsPrefix} '{query}'";
top =
[
new()
{
Header = headerText,
IsNoResults = true,
},
];
Logger.LogDebug($"[Search][TextChanged][{traceId}] no results -> added placeholder item (count={top.Count})");
}
else
{
// Project top 5 suggestions
var swProject = Stopwatch.StartNew();
top = [.. results.Take(5)
.Select(e =>
{
string subtitle = string.Empty;
if (e.Type != EntryType.SettingsPage)
{
var swSubtitle = Stopwatch.StartNew();
subtitle = SearchIndexService.GetLocalizedPageName(e.PageTypeName);
if (string.IsNullOrEmpty(subtitle))
{
// Fallback: look up the module title from the in-memory index
var swFallback = Stopwatch.StartNew();
subtitle = SearchIndexService.Index
.Where(x => x.Type == EntryType.SettingsPage && x.PageTypeName == e.PageTypeName)
.Select(x => x.Header)
.FirstOrDefault() ?? string.Empty;
swFallback.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] fallback subtitle for '{e.PageTypeName}' took {swFallback.ElapsedMilliseconds} ms");
}
swSubtitle.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] subtitle for '{e.PageTypeName}' took {swSubtitle.ElapsedMilliseconds} ms");
}
return new SuggestionItem
{
Header = e.Header,
Icon = e.Icon,
PageTypeName = e.PageTypeName,
ElementName = e.ElementName,
ParentElementName = e.ParentElementName,
Subtitle = subtitle,
IsShowAll = false,
};
})];
swProject.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] project suggestions took {swProject.ElapsedMilliseconds} ms. topCount={top.Count}");
if (results.Count > 5)
{
// Add a tail item to show all results if there are more than 5
top.Add(new SuggestionItem { IsShowAll = true });
Logger.LogDebug($"[Search][TextChanged][{traceId}] added 'Show all results' item");
}
}
var swUi = Stopwatch.StartNew();
sender.ItemsSource = top;
sender.IsSuggestionListOpen = top.Count > 0;
swUi.Stop();
swOverall.Stop();
Logger.LogDebug($"[Search][TextChanged][{traceId}] UI update took {swUi.ElapsedMilliseconds} ms. total={swOverall.ElapsedMilliseconds} ms");
}
private void SearchBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
@@ -714,19 +609,93 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void SearchBox_GotFocus(object sender, RoutedEventArgs e)
{
// do not prompt unless search for text.
return;
var box = sender as AutoSuggestBox;
var current = box?.Text?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(current))
{
return; // nothing to restore
}
// If current text matches last query and we have results, reconstruct the suggestion list.
if (string.Equals(current, _lastQueryText, StringComparison.Ordinal) && _lastSearchResults?.Count > 0)
{
try
{
var top = BuildSuggestionItems(current, _lastSearchResults);
box.ItemsSource = top;
box.IsSuggestionListOpen = top.Count > 0;
}
catch (Exception ex)
{
Logger.LogError($"Error restoring suggestion list {ex.Message}");
}
}
}
// Centralized suggestion projection logic used by TextChanged & GotFocus restore.
private List<SuggestionItem> BuildSuggestionItems(string query, List<SettingEntry> results)
{
results ??= new();
if (results.Count == 0)
{
var rl = ResourceLoaderInstance.ResourceLoader;
var noResultsPrefix = rl.GetString("Shell_Search_NoResults");
if (string.IsNullOrEmpty(noResultsPrefix))
{
noResultsPrefix = "No results for";
}
var headerText = $"{noResultsPrefix} '{query}'";
return new List<SuggestionItem>
{
new()
{
Header = headerText,
IsNoResults = true,
},
};
}
var list = results.Take(5).Select(e =>
{
string subtitle = string.Empty;
if (e.Type != EntryType.SettingsPage)
{
subtitle = SearchIndexService.GetLocalizedPageName(e.PageTypeName);
if (string.IsNullOrEmpty(subtitle))
{
subtitle = SearchIndexService.Index
.Where(x => x.Type == EntryType.SettingsPage && x.PageTypeName == e.PageTypeName)
.Select(x => x.Header)
.FirstOrDefault() ?? string.Empty;
}
}
return new SuggestionItem
{
Header = e.Header,
Icon = e.Icon,
PageTypeName = e.PageTypeName,
ElementName = e.ElementName,
ParentElementName = e.ParentElementName,
Subtitle = subtitle,
IsShowAll = false,
};
}).ToList();
if (results.Count > 5)
{
list.Add(new SuggestionItem { IsShowAll = true });
}
return list;
}
private async void SearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
var swSubmit = Stopwatch.StartNew();
Logger.LogDebug("[Search][Submit] start");
// If a suggestion is selected, navigate directly
if (args.ChosenSuggestion is SuggestionItem chosen)
{
Logger.LogDebug($"[Search][Submit] chosen suggestion -> navigate to {chosen.PageTypeName} element={chosen.ElementName ?? "<page>"}");
NavigateFromSuggestion(chosen);
return;
}
@@ -734,7 +703,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
var queryText = (args.QueryText ?? _lastQueryText)?.Trim();
if (string.IsNullOrWhiteSpace(queryText))
{
Logger.LogDebug("[Search][Submit] empty query -> navigate Dashboard");
NavigationService.Navigate<DashboardPage>();
return;
}
@@ -742,21 +710,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// Prefer cached results (from live search); if empty, perform a fresh search
var matched = _lastSearchResults?.Count > 0 && string.Equals(_lastQueryText, queryText, StringComparison.Ordinal)
? _lastSearchResults
: await Task.Run(() =>
{
var sw = Stopwatch.StartNew();
Logger.LogDebug($"[Search][Submit] background search for '{queryText}'...");
var r = SearchIndexService.Search(queryText);
sw.Stop();
Logger.LogDebug($"[Search][Submit] background search done in {sw.ElapsedMilliseconds} ms. results={r?.Count ?? 0}");
return r;
});
: await Task.Run(() => SearchIndexService.Search(queryText));
var searchParams = new SearchResultsNavigationParams(queryText, matched);
Logger.LogDebug($"[Search][Submit] navigate to SearchResultsPage (results={matched?.Count ?? 0})");
NavigationService.Navigate<SearchResultsPage>(searchParams);
swSubmit.Stop();
Logger.LogDebug($"[Search][Submit] total {swSubmit.ElapsedMilliseconds} ms");
}
public void Dispose()