mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-07 03:36:44 +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:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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.
|
// 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user