Skip to content

Commit 88fc54a

Browse files
committed
feat: save user search queries and display history in search
1 parent 8274bc8 commit 88fc54a

7 files changed

Lines changed: 251 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: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
_cache.Data[shortcutName] = entries = [];
48+
49+
entries.Insert(0, new HistoryEntry(query));
50+
51+
RebuildShortcutQueriesMap();
52+
53+
Save();
54+
55+
ExtensionHost.LogMessage($"[WebSearchShortcut] History: Add Query shortcut=\"{shortcutName}\" query=\"{query}\"");
56+
}
57+
}
58+
59+
public static void Reload()
60+
{
61+
lock (_lock)
62+
{
63+
HistoryStorage storage;
64+
try
65+
{
66+
storage = HistoryStorage.ReadFromFile(HistoryFilePath);
67+
}
68+
catch (Exception ex)
69+
{
70+
ExtensionHost.LogMessage(new LogMessage() { Message = $"[WebSearchShortcut] History: Reload failed: {ex}", State = MessageState.Error });
71+
72+
return;
73+
}
74+
75+
_cache.Data = storage?.Data ?? new Dictionary<string, List<HistoryEntry>>(StringComparer.OrdinalIgnoreCase);
76+
77+
RebuildShortcutQueriesMap();
78+
79+
ExtensionHost.LogMessage($"[WebSearchShortcut] History: Reload succeeded");
80+
}
81+
}
82+
83+
private static void Save()
84+
{
85+
try
86+
{
87+
HistoryStorage.WriteToFile(HistoryFilePath, _cache);
88+
}
89+
catch (Exception ex)
90+
{
91+
ExtensionHost.LogMessage(new LogMessage() { Message = $"[WebSearchShortcut] History: Save failed: {ex}", State = MessageState.Error });
92+
93+
return;
94+
}
95+
96+
ExtensionHost.LogMessage($"[WebSearchShortcut] History: Save succeeded");
97+
}
98+
99+
private static void RebuildShortcutQueriesMap()
100+
{
101+
_shortcutQueriesMap.Clear();
102+
103+
foreach (var (shortcutName, historyEntries) in _cache.Data)
104+
{
105+
_shortcutQueriesMap[shortcutName] = [
106+
.. (historyEntries ?? Enumerable.Empty<HistoryEntry>())
107+
.OrderByDescending(entry => entry.Timestamp)
108+
.Select(entry => entry.Query)
109+
.Distinct(StringComparer.OrdinalIgnoreCase)
110+
];
111+
}
112+
}
113+
}
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)