mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 02:36:19 +02:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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 Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -9,11 +10,13 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
public interface ISettingsInterface
|
||||
{
|
||||
event EventHandler? HistoryChanged;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -17,10 +14,16 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
{
|
||||
private const string HistoryItemCountLegacySettingsKey = "ShowHistory";
|
||||
private readonly string _historyPath;
|
||||
|
||||
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 readonly List<ChoiceSetSetting.Choice> _choices =
|
||||
@@ -46,9 +49,26 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
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");
|
||||
Directory.CreateDirectory(directory);
|
||||
@@ -57,7 +77,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
internal static string HistoryStateJsonPath()
|
||||
private static string HistoryStateJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
@@ -66,156 +86,30 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
return Path.Combine(directory, "websearch_history.json");
|
||||
}
|
||||
|
||||
public void SaveHistory(HistoryItem historyItem)
|
||||
public void AddHistoryItem(HistoryItem historyItem)
|
||||
{
|
||||
if (historyItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
List<HistoryItem> historyItems;
|
||||
|
||||
// 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);
|
||||
_history.Add(historyItem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to add item to the search history", ex);
|
||||
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()
|
||||
{
|
||||
base.SaveSettings();
|
||||
|
||||
try
|
||||
{
|
||||
if (HistoryItemCount == 0)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_history.SetCapacity(HistoryItemCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to save the search history", ex);
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user