Files
PowerToys/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs
Mike Griese 7211f7ed67 cmdpal: fix our settings load crash (#47296)
Fixes a category of crashes in CmdPal, related to our settings parsing. 

It would seem that I don't understand JSON parsing all that well in C#. 

Basically, we've got a bunch of places where we have
 
```c#
class Foo
{
  public Bar MySetting {get;init;} = new()
}
```

but when we JSON deserialize the settings, if there wasn't a
`"MySetting"` key, then the deserializer deserializes to `null`, not
`new()`. but since `Foo.MySetting` isn't a `Bar?`, then the compiler
can't check the fact that json _gets special rules to write a null to
it????_

Closes #47249

tested with the settings.json from that thread.
2026-04-28 12:24:27 -05:00

92 lines
2.7 KiB
C#

// 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.Collections.Immutable;
namespace Microsoft.CmdPal.UI.ViewModels;
public record RecentCommandsManager : IRecentCommandsManager
{
private ImmutableList<HistoryItem>? _history = ImmutableList<HistoryItem>.Empty;
internal ImmutableList<HistoryItem> History
{
get => _history ?? ImmutableList<HistoryItem>.Empty;
init => _history = value;
}
public RecentCommandsManager()
{
}
public int GetCommandHistoryWeight(string commandId)
{
var entry = History
.Index()
.Where(item => item.Item.CommandId == commandId)
.FirstOrDefault();
// These numbers are vaguely scaled so that "VS" will make "Visual Studio" the
// match after one use.
// Usually it has a weight of 84, compared to 109 for the VS cmd prompt
if (entry.Item is not null)
{
var index = entry.Index;
// First, add some weight based on how early in the list this appears
var bucket = index switch
{
_ when index <= 2 => 35,
_ when index <= 10 => 25,
_ when index <= 15 => 15,
_ when index <= 35 => 10,
_ => 5,
};
// Then, add weight for how often this is used, but cap the weight from usage.
var uses = Math.Min(entry.Item.Uses * 5, 35);
return bucket + uses;
}
return 0;
}
/// <summary>
/// Returns a new RecentCommandsManager with the given command added/promoted in history.
/// Pure function — does not mutate this instance.
/// </summary>
public RecentCommandsManager WithHistoryItem(string commandId)
{
var existing = History.FirstOrDefault(item => item.CommandId == commandId);
ImmutableList<HistoryItem> newHistory;
if (existing is not null)
{
newHistory = History.Remove(existing);
var updated = existing with { Uses = existing.Uses + 1 };
newHistory = newHistory.Insert(0, updated);
}
else
{
var newItem = new HistoryItem { CommandId = commandId, Uses = 1 };
newHistory = History.Insert(0, newItem);
}
if (newHistory.Count > 50)
{
newHistory = newHistory.RemoveRange(50, newHistory.Count - 50);
}
return this with { History = newHistory };
}
}
public interface IRecentCommandsManager
{
int GetCommandHistoryWeight(string commandId);
RecentCommandsManager WithHistoryItem(string commandId);
}