Skip to content

Commit 358805f

Browse files
committed
feat: save user search queries and display history in search
1 parent bad1f66 commit 358805f

7 files changed

Lines changed: 254 additions & 3 deletions

File tree

CmdPalWebSearchShortcut/WebSearchShortcut/Commands/SearchWebCommand.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.CommandPalette.Extensions.Toolkit;
22
using WebSearchShortcut.Browsers;
3+
using WebSearchShortcut.History;
34

45
namespace WebSearchShortcut.Commands;
56

@@ -28,6 +29,8 @@ public override CommandResult Invoke()
2829
return CommandResult.KeepOpen();
2930
}
3031

32+
HistoryService.Add(_shortcut.Name, _query);
33+
3134
// if (_settingsManager.ShowHistory != Resources.history_none)
3235
// {
3336
// _settingsManager.SaveHistory(new HistoryItem(Arguments, DateTime.Now));
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Text.Json.Serialization;
3+
4+
namespace WebSearchShortcut.History;
5+
6+
internal sealed record HistoryEntry
7+
{
8+
public string Query { get; init; }
9+
public DateTimeOffset Timestamp { get; init; }
10+
11+
[JsonConstructor]
12+
public HistoryEntry(string query, DateTimeOffset timestamp)
13+
=> (Query, Timestamp) = (query, timestamp);
14+
15+
public HistoryEntry(string query) : this(query, DateTimeOffset.UtcNow) { }
16+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Threading;
6+
using Microsoft.CommandPalette.Extensions;
7+
using Microsoft.CommandPalette.Extensions.Toolkit;
8+
9+
namespace WebSearchShortcut.History;
10+
11+
internal static class HistoryService
12+
{
13+
public static string HistoryFilePath
14+
{
15+
get
16+
{
17+
var directory = Utilities.BaseSettingsPath("WebSearchShortcut");
18+
19+
Directory.CreateDirectory(directory);
20+
21+
return Path.Combine(directory, "WebSearchShortcut_history.json");
22+
}
23+
}
24+
25+
private static readonly Lock _lock = new();
26+
private static readonly HistoryStorage _cache = new();
27+
private static readonly Dictionary<string, string[]> _shortcutQueriesMap = new(StringComparer.OrdinalIgnoreCase);
28+
29+
static HistoryService()
30+
{
31+
Reload();
32+
}
33+
34+
public static string[] Get(string shortcutName)
35+
{
36+
lock (_lock)
37+
{
38+
return [.. _shortcutQueriesMap.GetValueOrDefault(shortcutName, [])];
39+
}
40+
}
41+
42+
public static void Add(string shortcutName, string query)
43+
{
44+
lock (_lock)
45+
{
46+
if (!_cache.Data.TryGetValue(shortcutName, out var entries) || entries is null)
47+
{
48+
entries = [];
49+
_cache.Data[shortcutName] = entries;
50+
}
51+
52+
entries.Insert(0, new HistoryEntry(query));
53+
54+
RebuildShortcutQueriesMap();
55+
56+
Save();
57+
58+
ExtensionHost.LogMessage($"[WebSearchShortcut] History: Add Query shortcut=\"{shortcutName}\" query=\"{query}\"");
59+
}
60+
}
61+
62+
public static void Reload()
63+
{
64+
lock (_lock)
65+
{
66+
HistoryStorage storage;
67+
try
68+
{
69+
storage = HistoryStorage.ReadFromFile(HistoryFilePath);
70+
}
71+
catch (Exception ex)
72+
{
73+
ExtensionHost.LogMessage( new LogMessage() { Message = $"[WebSearchShortcut] History: Reload failed: {ex}", State = MessageState.Error });
74+
75+
return;
76+
}
77+
78+
_cache.Data = storage?.Data ?? new Dictionary<string, List<HistoryEntry>>(StringComparer.OrdinalIgnoreCase);
79+
80+
RebuildShortcutQueriesMap();
81+
82+
ExtensionHost.LogMessage($"[WebSearchShortcut] History: Reload succeeded");
83+
}
84+
}
85+
86+
private static void Save()
87+
{
88+
try
89+
{
90+
HistoryStorage.WriteToFile(HistoryFilePath, _cache);
91+
}
92+
catch (Exception ex)
93+
{
94+
ExtensionHost.LogMessage(new LogMessage() { Message = $"[WebSearchShortcut] History: Save failed: {ex}", State = MessageState.Error });
95+
96+
return;
97+
}
98+
99+
ExtensionHost.LogMessage($"[WebSearchShortcut] History: Save succeeded");
100+
}
101+
102+
private static void RebuildShortcutQueriesMap()
103+
{
104+
_shortcutQueriesMap.Clear();
105+
106+
foreach (var (shortcutName, historyEntries) in _cache.Data)
107+
{
108+
_shortcutQueriesMap[shortcutName] = [
109+
.. (historyEntries ?? Enumerable.Empty<HistoryEntry>())
110+
.OrderByDescending(entry => entry.Timestamp)
111+
.Select(entry => entry.Query)
112+
.Distinct(StringComparer.OrdinalIgnoreCase)
113+
];
114+
}
115+
}
116+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text.Json;
5+
using WebSearchShortcut.Helpers;
6+
7+
using Microsoft.CommandPalette.Extensions.Toolkit;
8+
9+
namespace WebSearchShortcut.History;
10+
11+
internal sealed class HistoryStorage
12+
{
13+
public Dictionary<string, List<HistoryEntry>> Data { get; set; } = [];
14+
15+
public static HistoryStorage ReadFromFile(string path)
16+
{
17+
if (!File.Exists(path))
18+
{
19+
HistoryStorage empty = new();
20+
21+
WriteToFile(path, empty);
22+
23+
return empty;
24+
}
25+
26+
string jsonString;
27+
28+
try
29+
{
30+
jsonString = File.ReadAllText(path);
31+
}
32+
catch (Exception ex)
33+
{
34+
ExtensionHost.LogMessage($"[HistoryStorage] ReadFile failed: {ex.GetType().Name}: {ex.Message}");
35+
36+
throw;
37+
}
38+
39+
if (string.IsNullOrWhiteSpace(jsonString))
40+
{
41+
return new HistoryStorage();
42+
}
43+
44+
try
45+
{
46+
return JsonSerializer.Deserialize(jsonString, AppJsonSerializerContext.Default.HistoryStorage) ?? new HistoryStorage();
47+
}
48+
catch (Exception ex)
49+
{
50+
ExtensionHost.LogMessage($"[HistoryStorage] JsonDeserialize failed: {ex.GetType().Name}: {ex.Message}");
51+
52+
throw;
53+
}
54+
}
55+
56+
public static void WriteToFile(string path, HistoryStorage data)
57+
{
58+
var jsonString = JsonPrettyFormatter.ToPrettyJson(data, AppJsonSerializerContext.Default.HistoryStorage);
59+
60+
try
61+
{
62+
File.WriteAllText(path, jsonString);
63+
}
64+
catch (Exception ex)
65+
{
66+
ExtensionHost.LogMessage($"[HistoryStorage] WriteFile failed: {ex.GetType().Name}: {ex.Message}");
67+
68+
throw;
69+
}
70+
}
71+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System.Text.Json.Serialization;
2+
using WebSearchShortcut.History;
23

34
namespace WebSearchShortcut;
45

56
[JsonSourceGenerationOptions(IncludeFields = true)]
67
[JsonSerializable(typeof(Storage))]
78
[JsonSerializable(typeof(WebSearchShortcutDataEntry))]
9+
[JsonSerializable(typeof(HistoryStorage))]
10+
[JsonSerializable(typeof(HistoryEntry))]
811
internal partial class AppJsonSerializerContext : JsonSerializerContext
912
{
1013
}

CmdPalWebSearchShortcut/WebSearchShortcut/Pages/SearchWebPage.cs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
using Microsoft.CommandPalette.Extensions.Toolkit;
77
using WebSearchShortcut.Commands;
88
using WebSearchShortcut.Helpers;
9+
using WebSearchShortcut.History;
910
using WebSearchShortcut.Properties;
1011
using WebSearchShortcut.Services;
1112

1213
namespace WebSearchShortcut;
1314

1415
internal sealed partial class SearchWebPage : DynamicListPage
1516
{
17+
private const int MaxDisplayCount = 100;
18+
1619
private readonly WebSearchShortcutDataEntry _shortcut;
1720

1821
private readonly IListItem _openHomepageListItem;
@@ -50,11 +53,20 @@ public SearchWebPage(WebSearchShortcutDataEntry shortcut)
5053
Title = StringFormatter.Format(Resources.OpenHomepageItem_TitleTemplate, new() { ["shortcut"] = shortcut.Name }),
5154
Icon = Icons.Home
5255
};
56+
}
5357

54-
_items = [_openHomepageListItem];
58+
public override IListItem[] GetItems()
59+
{
60+
if (_items.Length == 0)
61+
Rebuild();
62+
63+
return Volatile.Read(ref _items);
5564
}
5665

57-
public override IListItem[] GetItems() => Volatile.Read(ref _items);
66+
public void Rebuild()
67+
{
68+
UpdateSearchText(SearchText, SearchText);
69+
}
5870

5971
public override async void UpdateSearchText(string oldSearch, string newSearch)
6072
{
@@ -90,7 +102,9 @@ public override async void UpdateSearchText(string oldSearch, string newSearch)
90102
{
91103
UpdateSuggestionItems([], currentEpoch);
92104

93-
RenderItems([_openHomepageListItem], currentEpoch);
105+
var historyItems = BuildHistoryItems();
106+
107+
RenderItems([_openHomepageListItem, .. historyItems], currentEpoch);
94108

95109
return;
96110
}
@@ -182,6 +196,29 @@ private ListItem[] BuildPrimaryItems(string searchText)
182196
];
183197
}
184198

199+
private ListItem[] BuildHistoryItems()
200+
{
201+
var historyQueries = HistoryService
202+
.Get(_shortcut.Name)
203+
.Take(MaxDisplayCount - 1);
204+
205+
return [
206+
.. historyQueries.Select(historyQuery => new ListItem(
207+
new SearchWebCommand(_shortcut, historyQuery)
208+
{
209+
Name = StringFormatter.Format(Resources.SearchQueryItem_NameTemplate, new() { ["shortcut"] = _shortcut.Name, ["query"] = historyQuery }),
210+
}
211+
)
212+
{
213+
Title = StringFormatter.Format(Resources.SearchQueryItem_TitleTemplate, new() { ["shortcut"] = _shortcut.Name, ["query"] = historyQuery }),
214+
Subtitle = StringFormatter.Format(Resources.SearchQueryItem_SubtitleTemplate, new() { ["shortcut"] = _shortcut.Name, ["query"] = historyQuery }),
215+
Icon = Icons.History,
216+
TextToSuggest = historyQuery,
217+
MoreCommands = [_openHomepageContextItem]
218+
})
219+
];
220+
}
221+
185222
private async Task<ListItem[]> FetchSuggestionItemsAsync(string searchText, CancellationToken cancellationToken)
186223
{
187224
var suggestions = await SuggestionsRegistry

CmdPalWebSearchShortcut/WebSearchShortcut/Properties/Icons.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,9 @@ internal static class Icons
5050
/// Search icon
5151
/// </summary>
5252
public static IconInfo Search { get; } = new("\uE721");
53+
54+
/// <summary>
55+
/// History icon
56+
/// </summary>
57+
public static IconInfo History { get; } = new("\uE81C");
5358
}

0 commit comments

Comments
 (0)