CmdPal: Update WebSearch extension history immediately (#41398)

## Summary of the Pull Request

This PR ensures that the list of recent searches in the Web Search
extension is updated immediately after a new item is added or when
settings controlling the number of items are changed.

- Refactors the Web Search extension history to keep it in memory after
being loaded at startup
- Adds an event to notify subscribers when the history changes  
- Implements `IDisposable` to ensure that `WebSearchListPage`
unsubscribes from the event
- Moves responsibility for creating all list items to single class
(`WebSearchListPage`)
- Updated unit tests
- 
## PR Checklist

- [x] Closes: #40548
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** nothing
- [x] **New binaries:** none
- [x] **Documentation updated:** nope

<!-- 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-09-03 20:47:33 +02:00
committed by GitHub
parent 347c3f1efa
commit 7d8f64cf3c
10 changed files with 342 additions and 208 deletions

View File

@@ -2,10 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests; namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
@@ -13,34 +12,22 @@ public class MockSettingsInterface : ISettingsInterface
{ {
private readonly List<HistoryItem> _historyItems; private readonly List<HistoryItem> _historyItems;
public event EventHandler HistoryChanged;
public bool GlobalIfURI { get; set; } public bool GlobalIfURI { get; set; }
public uint HistoryItemCount { get; set; } public int HistoryItemCount { get; set; }
public MockSettingsInterface(uint historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null) public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
{ {
_historyItems = mockHistory ?? new List<HistoryItem>(); _historyItems = mockHistory ?? new List<HistoryItem>();
GlobalIfURI = globalIfUri; GlobalIfURI = globalIfUri;
HistoryItemCount = historyItemCount; HistoryItemCount = historyItemCount;
} }
public List<ListItem> LoadHistory() public void AddHistoryItem(HistoryItem historyItem)
{
var listItems = new List<ListItem>();
foreach (var historyItem in _historyItems)
{
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", System.Globalization.CultureInfo.InvariantCulture),
});
}
listItems.Reverse();
return listItems;
}
public void SaveHistory(HistoryItem historyItem)
{ {
if (historyItem is null) if (historyItem is null)
{ {
@@ -54,15 +41,18 @@ public class MockSettingsInterface : ISettingsInterface
{ {
while (_historyItems.Count > HistoryItemCount) while (_historyItems.Count > HistoryItemCount)
{ {
_historyItems.RemoveAt(0); // Remove the oldest item _historyItems.RemoveAt(0);
} }
} }
HistoryChanged?.Invoke(this, EventArgs.Empty);
} }
// Helper method for testing // Helper method for testing
public void ClearHistory() public void ClearHistory()
{ {
_historyItems.Clear(); _historyItems.Clear();
HistoryChanged?.Invoke(this, EventArgs.Empty);
} }
// Helper method for testing // Helper method for testing

View File

@@ -45,7 +45,7 @@ public class QueryTests : CommandPaletteUnitTestBase
} }
[TestMethod] [TestMethod]
public async Task LoadHistoryReturnsExpectedItems() public async Task HistoryReturnsExpectedItems()
{ {
// Setup // Setup
var mockHistoryItems = new List<HistoryItem> var mockHistoryItems = new List<HistoryItem>
@@ -77,7 +77,7 @@ public class QueryTests : CommandPaletteUnitTestBase
} }
[TestMethod] [TestMethod]
public async Task LoadHistoryMoreThanLimitation() public async Task HistoryExceedingLimitReturnsMaxItems()
{ {
// Setup // Setup
var mockHistoryItems = new List<HistoryItem> var mockHistoryItems = new List<HistoryItem>
@@ -109,7 +109,7 @@ public class QueryTests : CommandPaletteUnitTestBase
} }
[TestMethod] [TestMethod]
public async Task LoadHistoryWithDisableSetting() public async Task HistoryWhenSetToNoneReturnEmptyList()
{ {
// Setup // Setup
var mockHistoryItems = new List<HistoryItem> var mockHistoryItems = new List<HistoryItem>

View File

@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Pages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
[TestClass]
public class SettingsManagerTests : CommandPaletteUnitTestBase
{
[TestMethod]
public async Task HistoryChangedEventIsRaisedWhenItemIsAdded()
{
// Setup
var settings = new MockSettingsInterface(historyItemCount: 5);
var page = new WebSearchListPage(settings);
var eventRaised = false;
try
{
settings.HistoryChanged += Handler;
// Act
settings.AddHistoryItem(new HistoryItem("test event", DateTime.UtcNow));
await Task.Delay(50);
// Assert
Assert.IsTrue(eventRaised, "Expected HistoryChanged to be raised when saving history.");
}
finally
{
settings.HistoryChanged -= Handler;
page.Dispose();
}
return;
void Handler(object s, EventArgs e) => eventRaised = true;
}
}

View File

@@ -36,7 +36,7 @@ internal sealed partial class SearchWebCommand : InvokableCommand
if (_settingsManager.HistoryItemCount != 0) if (_settingsManager.HistoryItemCount != 0)
{ {
_settingsManager.SaveHistory(new HistoryItem(Arguments, DateTime.Now)); _settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
} }
return CommandResult.Dismiss(); return CommandResult.Dismiss();

View File

@@ -0,0 +1,119 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading;
using ManagedCommon;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
internal sealed class HistoryStore
{
private readonly string _filePath;
private readonly List<HistoryItem> _items = [];
private readonly Lock _lock = new();
private int _capacity;
public event EventHandler? Changed;
public HistoryStore(string filePath, int capacity)
{
ArgumentNullException.ThrowIfNull(filePath);
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
_filePath = filePath;
_capacity = capacity;
_items.AddRange(LoadFromDiskSafe());
TrimNoLock();
}
public IReadOnlyList<HistoryItem> HistoryItems
{
get
{
lock (_lock)
{
return [.. _items];
}
}
}
public void Add(HistoryItem item)
{
ArgumentNullException.ThrowIfNull(item);
lock (_lock)
{
_items.Add(item);
_ = TrimNoLock();
SaveNoLock();
}
Changed?.Invoke(this, EventArgs.Empty);
}
public void SetCapacity(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
bool trimmed;
lock (_lock)
{
_capacity = capacity;
trimmed = TrimNoLock();
if (trimmed)
{
SaveNoLock();
}
}
if (trimmed)
{
Changed?.Invoke(this, EventArgs.Empty);
}
}
private bool TrimNoLock()
{
var max = _capacity;
if (_items.Count > max)
{
_items.RemoveRange(0, _items.Count - max);
return true;
}
return false;
}
private List<HistoryItem> LoadFromDiskSafe()
{
try
{
if (!File.Exists(_filePath))
{
return [];
}
var fileContent = File.ReadAllText(_filePath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
return historyItems;
}
catch (Exception ex)
{
Logger.LogError("Unable to load history", ex);
return [];
}
}
private void SaveNoLock()
{
var json = JsonSerializer.Serialize(_items, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_filePath, json);
}
}

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -9,11 +10,13 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
public interface ISettingsInterface public interface ISettingsInterface
{ {
event EventHandler? HistoryChanged;
public bool GlobalIfURI { get; } public bool GlobalIfURI { get; }
public uint HistoryItemCount { get; } public int HistoryItemCount { get; }
public List<ListItem> LoadHistory(); public IReadOnlyList<HistoryItem> HistoryItems { get; }
public void SaveHistory(HistoryItem historyItem); public void AddHistoryItem(HistoryItem historyItem);
} }

View File

@@ -4,11 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using ManagedCommon;
using System.Text.Json;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -17,10 +14,16 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
public class SettingsManager : JsonSettingsManager, ISettingsInterface public class SettingsManager : JsonSettingsManager, ISettingsInterface
{ {
private const string HistoryItemCountLegacySettingsKey = "ShowHistory"; private const string HistoryItemCountLegacySettingsKey = "ShowHistory";
private readonly string _historyPath;
private static readonly string _namespace = "websearch"; private static readonly string _namespace = "websearch";
public event EventHandler? HistoryChanged
{
add => _history.Changed += value;
remove => _history.Changed -= value;
}
private readonly HistoryStore _history;
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
private static readonly List<ChoiceSetSetting.Choice> _choices = private static readonly List<ChoiceSetSetting.Choice> _choices =
@@ -46,9 +49,26 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
public bool GlobalIfURI => _globalIfURI.Value; public bool GlobalIfURI => _globalIfURI.Value;
public uint HistoryItemCount => uint.TryParse(_historyItemCount.Value, out var value) ? value : 0; public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0;
internal static string SettingsJsonPath() public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems;
public SettingsManager()
{
FilePath = SettingsJsonPath();
Settings.Add(_globalIfURI);
Settings.Add(_historyItemCount);
LoadSettings();
// Initialize history store after loading settings to get the correct capacity
_history = new HistoryStore(HistoryStateJsonPath(), HistoryItemCount);
Settings.SettingsChanged += (_, _) => SaveSettings();
}
private static string SettingsJsonPath()
{ {
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
@@ -57,7 +77,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
return Path.Combine(directory, "settings.json"); return Path.Combine(directory, "settings.json");
} }
internal static string HistoryStateJsonPath() private static string HistoryStateJsonPath()
{ {
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
@@ -66,156 +86,30 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
return Path.Combine(directory, "websearch_history.json"); return Path.Combine(directory, "websearch_history.json");
} }
public void SaveHistory(HistoryItem historyItem) public void AddHistoryItem(HistoryItem historyItem)
{ {
if (historyItem is null)
{
return;
}
try try
{ {
List<HistoryItem> historyItems; _history.Add(historyItem);
// Check if the file exists and load existing history
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
}
else
{
historyItems = [];
}
// Add the new history item
historyItems.Add(historyItem);
// Determine the maximum number of items to keep based on HistoryItemCount
if (HistoryItemCount > 0)
{
// Keep only the most recent `maxHistoryItems` items
while (historyItems.Count > HistoryItemCount)
{
historyItems.RemoveAt(0); // Remove the oldest item
}
}
// Serialize the updated list back to JSON and save it
var historyJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, historyJson);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError("Failed to add item to the search history", ex);
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
} }
} }
public List<ListItem> LoadHistory()
{
try
{
if (!File.Exists(_historyPath))
{
return [];
}
// Read and deserialize JSON into a list of HistoryItem objects
var fileContent = File.ReadAllText(_historyPath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Convert each HistoryItem to a ListItem
var listItems = new List<ListItem>();
foreach (var historyItem in historyItems)
{
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture), // Ensures consistent formatting
});
}
listItems.Reverse();
return listItems;
}
catch (Exception ex)
{
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
return [];
}
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
_historyPath = HistoryStateJsonPath();
Settings.Add(_globalIfURI);
Settings.Add(_historyItemCount);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();
}
private void ClearHistory()
{
try
{
if (File.Exists(_historyPath))
{
// Delete the history file
File.Delete(_historyPath);
// Log that the history was successfully cleared
ExtensionHost.LogMessage(new LogMessage() { Message = "History cleared successfully." });
}
else
{
// Log that there was no history file to delete
ExtensionHost.LogMessage(new LogMessage() { Message = "No history file found to clear." });
}
}
catch (Exception ex)
{
// Log any exception that occurs
ExtensionHost.LogMessage(new LogMessage() { Message = $"Failed to clear history: {ex}" });
}
}
public override void SaveSettings() public override void SaveSettings()
{ {
base.SaveSettings(); base.SaveSettings();
try try
{ {
if (HistoryItemCount == 0) _history.SetCapacity(HistoryItemCount);
{
ClearHistory();
}
else if (HistoryItemCount > 0)
{
// Trim the history file if there are more items than the new limit
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Check if trimming is needed
if (historyItems.Count > HistoryItemCount)
{
// Trim the list to keep only the most recent `HistoryItemCount` items
historyItems = historyItems.Skip((int)(historyItems.Count - HistoryItemCount)).ToList();
// Save the trimmed history back to the file
var trimmedHistoryJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, trimmedHistoryJson);
}
}
}
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError("Failed to save the search history", ex);
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() }); ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
} }
} }

View File

@@ -5,8 +5,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Text; using System.Text;
using System.Threading;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
@@ -16,31 +16,30 @@ using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Pages; namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
internal sealed partial class WebSearchListPage : DynamicListPage internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{ {
private readonly string _iconPath = string.Empty; private readonly IconInfo _newSearchIcon = new(string.Empty);
private readonly List<ListItem>? _historyItems;
private readonly ISettingsInterface _settingsManager; private readonly ISettingsInterface _settingsManager;
private readonly Lock _sync = new();
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name); private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private List<ListItem> _allItems; private IListItem[] _allItems = [];
private List<ListItem> _historyItems = [];
public WebSearchListPage(ISettingsInterface settingsManager) public WebSearchListPage(ISettingsInterface settingsManager)
{ {
ArgumentNullException.ThrowIfNull(settingsManager);
Name = Resources.command_item_title; Name = Resources.command_item_title;
Title = Resources.command_item_title; Title = Resources.command_item_title;
Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"); Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
_allItems = [];
Id = "com.microsoft.cmdpal.websearch"; Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager; _settingsManager = settingsManager;
_historyItems = _settingsManager.HistoryItemCount != 0 ? _settingsManager.LoadHistory() : null; _settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
if (_historyItems is not null)
{
_allItems.AddRange(_historyItems);
}
// It just looks viewer to have string twice on the page, and default placeholder is good enough // It just looks viewer to have string twice on the page, and default placeholder is good enough
PlaceholderText = _allItems.Count > 0 ? Resources.plugin_description : string.Empty; PlaceholderText = _allItems.Length > 0 ? Resources.plugin_description : string.Empty;
EmptyContent = new CommandItem(new NoOpCommand()) EmptyContent = new CommandItem(new NoOpCommand())
{ {
@@ -48,45 +47,102 @@ internal sealed partial class WebSearchListPage : DynamicListPage
Title = Properties.Resources.plugin_description, Title = Properties.Resources.plugin_description,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
}; };
UpdateHistory();
RequeryAndUpdateItems(SearchText);
} }
public List<ListItem> Query(string query) private void SettingsManagerOnHistoryChanged(object? sender, EventArgs e)
{ {
ArgumentNullException.ThrowIfNull(query); UpdateHistory();
IEnumerable<ListItem>? filteredHistoryItems = null; RequeryAndUpdateItems(SearchText);
}
if (_historyItems is not null) private void UpdateHistory()
{
List<ListItem> history = [];
if (_settingsManager.HistoryItemCount > 0)
{ {
filteredHistoryItems = _settingsManager.HistoryItemCount != 0 ? ListHelpers.FilterList(_historyItems, query).OfType<ListItem>() : null; var items = _settingsManager.HistoryItems;
for (var index = items.Count - 1; index >= 0; index--)
{
var historyItem = items[index];
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture),
});
}
} }
var results = new List<ListItem>(); lock (_sync)
{
_historyItems = history;
}
}
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IconInfo newSearchIcon)
{
ArgumentNullException.ThrowIfNull(query);
var filteredHistoryItems = settingsManager.HistoryItemCount > 0
? ListHelpers.FilterList(historySnapshot, query)
: [];
var results = new List<IListItem>();
if (!string.IsNullOrEmpty(query)) if (!string.IsNullOrEmpty(query))
{ {
var searchTerm = query; var searchTerm = query;
var result = new ListItem(new SearchWebCommand(searchTerm, _settingsManager)) var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
{ {
Title = searchTerm, Title = searchTerm,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
Icon = new IconInfo(_iconPath), Icon = newSearchIcon,
}; };
results.Add(result); results.Add(result);
} }
if (filteredHistoryItems is not null) results.AddRange(filteredHistoryItems);
return [.. results];
}
private void RequeryAndUpdateItems(string search)
{
List<ListItem> historySnapshot;
lock (_sync)
{ {
results.AddRange(filteredHistoryItems); historySnapshot = _historyItems;
} }
return results; var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _newSearchIcon);
lock (_sync)
{
_allItems = items;
}
RaiseItemsChanged();
} }
public override void UpdateSearchText(string oldSearch, string newSearch) public override void UpdateSearchText(string oldSearch, string newSearch)
{ {
_allItems = [.. Query(newSearch)]; RequeryAndUpdateItems(newSearch);
RaiseItemsChanged(0);
} }
public override IListItem[] GetItems() => [.. _allItems]; public override IListItem[] GetItems()
{
lock (_sync)
{
return _allItems;
}
}
public void Dispose()
{
_settingsManager.HistoryChanged -= SettingsManagerOnHistoryChanged;
GC.SuppressFinalize(this);
}
} }

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
@@ -15,6 +16,9 @@ public partial class WebSearchCommandsProvider : CommandProvider
private readonly SettingsManager _settingsManager = new(); private readonly SettingsManager _settingsManager = new();
private readonly FallbackExecuteSearchItem _fallbackItem; private readonly FallbackExecuteSearchItem _fallbackItem;
private readonly FallbackOpenURLItem _openUrlFallbackItem; private readonly FallbackOpenURLItem _openUrlFallbackItem;
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
private readonly ICommandItem[] _topLevelItems;
private readonly IFallbackCommandItem[] _fallbackCommands;
public WebSearchCommandsProvider() public WebSearchCommandsProvider()
{ {
@@ -25,18 +29,27 @@ public partial class WebSearchCommandsProvider : CommandProvider
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager); _fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager); _openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
}
public override ICommandItem[] TopLevelCommands() _webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
{
return [new WebSearchTopLevelCommandItem(_settingsManager)
{ {
MoreCommands = [ MoreCommands =
[
new CommandContextItem(Settings!.SettingsPage), new CommandContextItem(Settings!.SettingsPage),
], ],
} };
]; _topLevelItems = [_webSearchTopLevelItem];
_fallbackCommands = [_openUrlFallbackItem, _fallbackItem];
} }
public override IFallbackCommandItem[]? FallbackCommands() => [_openUrlFallbackItem, _fallbackItem]; public override ICommandItem[] TopLevelCommands() => _topLevelItems;
public override IFallbackCommandItem[]? FallbackCommands() => _fallbackCommands;
public override void Dispose()
{
_webSearchTopLevelItem?.Dispose();
base.Dispose();
GC.SuppressFinalize(this);
}
} }

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.IO;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Pages; using Microsoft.CmdPal.Ext.WebSearch.Pages;
@@ -13,7 +12,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch; namespace Microsoft.CmdPal.Ext.WebSearch;
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
{ {
private readonly SettingsManager _settingsManager; private readonly SettingsManager _settingsManager;
@@ -27,17 +26,29 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
private void SetDefaultTitle() => Title = Resources.command_item_title; private void SetDefaultTitle() => Title = Resources.command_item_title;
private void ReplaceCommand(ICommand newCommand)
{
(Command as IDisposable)?.Dispose();
Command = newCommand;
}
public void UpdateQuery(string query) public void UpdateQuery(string query)
{ {
if (string.IsNullOrEmpty(query)) if (string.IsNullOrEmpty(query))
{ {
SetDefaultTitle(); SetDefaultTitle();
Command = new WebSearchListPage(_settingsManager); ReplaceCommand(new WebSearchListPage(_settingsManager));
} }
else else
{ {
Title = query; Title = query;
Command = new SearchWebCommand(query, _settingsManager); ReplaceCommand(new SearchWebCommand(query, _settingsManager));
} }
} }
public void Dispose()
{
(Command as IDisposable)?.Dispose();
GC.SuppressFinalize(this);
}
} }