CmdPal: Add custom search engine option to Web Search extension (#43941)

## Summary of the Pull Request

This PR allows user to customize a search query in Command Palette's Web
Search built-in extension. This will also solve a problem with some
browser that doesn't handle argument in form "? <query>" as it will
allow user to specify the complete URI.

- Introduces a new text box in Web Search extension settings for
specifying a custom search engine URI
- If the text box is non-empty, the provided URI is used for queries
- If left empty, the extension defaults to previous behavior, sending
queries in the format "? query"

## Pictures? Pictures!

<img width="825" height="566" alt="image"
src="https://github.com/user-attachments/assets/fbf3d3a5-ebfe-4c16-a5f1-0d044b6f9047"
/>

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

- [x] Closes: #43940 
- [x] Closes: #42867 
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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
This commit is contained in:
Jiří Polášek
2025-11-30 01:59:58 +01:00
committed by GitHub
parent 8aea589b01
commit 1ba5a258e9
6 changed files with 68 additions and 1 deletions

View File

@@ -18,6 +18,8 @@ public class MockSettingsInterface : ISettingsInterface
public int HistoryItemCount { get; set; }
public string CustomSearchUri { get; }
public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)

View File

@@ -28,12 +28,15 @@ internal sealed partial class SearchWebCommand : InvokableCommand
public override CommandResult Invoke()
{
if (!_browserInfoService.Open($"? {Arguments}"))
var uri = BuildUri();
if (!_browserInfoService.Open(uri))
{
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen();
}
// remember only the query, not the full URI
if (_settingsManager.HistoryItemCount != 0)
{
_settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
@@ -41,4 +44,28 @@ internal sealed partial class SearchWebCommand : InvokableCommand
return CommandResult.Dismiss();
}
private string BuildUri()
{
if (string.IsNullOrWhiteSpace(_settingsManager.CustomSearchUri))
{
return $"? " + Arguments;
}
// if the custom search URI contains query placeholder, replace it with the actual query
// otherwise append the query to the end of the URI
// support {query}, %query% or %s as placeholder
var placeholderVariants = new[] { "{query}", "%query%", "%s" };
foreach (var placeholder in placeholderVariants)
{
if (_settingsManager.CustomSearchUri.Contains(placeholder, StringComparison.OrdinalIgnoreCase))
{
return _settingsManager.CustomSearchUri.Replace(placeholder, Uri.EscapeDataString(Arguments), StringComparison.OrdinalIgnoreCase);
}
}
// is this too smart?
var separator = _settingsManager.CustomSearchUri.Contains('?') ? '&' : '?';
return $"{_settingsManager.CustomSearchUri}{separator}q={Uri.EscapeDataString(Arguments)}";
}
}

View File

@@ -18,5 +18,7 @@ public interface ISettingsInterface
public IReadOnlyList<HistoryItem> HistoryItems { get; }
string CustomSearchUri { get; }
public void AddHistoryItem(HistoryItem historyItem);
}

View File

@@ -41,6 +41,15 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Resources.plugin_global_if_uri,
false);
private readonly TextSetting _customSearchUri = new(
Namespaced(nameof(CustomSearchUri)),
Resources.plugin_custom_search_uri,
Resources.plugin_custom_search_uri,
string.Empty)
{
Placeholder = Resources.plugin_custom_search_uri_placeholder,
};
private readonly ChoiceSetSetting _historyItemCount = new(
Namespaced(HistoryItemCountLegacySettingsKey),
Resources.plugin_history_item_count,
@@ -51,6 +60,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0;
public string CustomSearchUri => _customSearchUri.Value ?? string.Empty;
public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems;
public SettingsManager()
@@ -59,6 +70,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_globalIfURI);
Settings.Add(_historyItemCount);
Settings.Add(_customSearchUri);
LoadSettings();

View File

@@ -159,6 +159,24 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Custom search engine URL.
/// </summary>
public static string plugin_custom_search_uri {
get {
return ResourceManager.GetString("plugin_custom_search_uri", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use {query} or %s as the search query placeholder; e.g. https://www.bing.com/search?q={query}.
/// </summary>
public static string plugin_custom_search_uri_placeholder {
get {
return ResourceManager.GetString("plugin_custom_search_uri_placeholder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Searches the web with your default search engine.
/// </summary>

View File

@@ -187,4 +187,10 @@
<data name="default_browser" xml:space="preserve">
<value>default browser</value>
</data>
<data name="plugin_custom_search_uri" xml:space="preserve">
<value>Custom search engine URL</value>
</data>
<data name="plugin_custom_search_uri_placeholder" xml:space="preserve">
<value>Use {query} or %s as the search query placeholder; e.g. https://www.bing.com/search?q={query}</value>
</data>
</root>