CmdPal: Make Calculator Great Again (#44594)

## Summary of the Pull Request

This PR continues the tradition of alphabetical progress. After
[MBGA](#41961), we move on to **MCBA — Make Calculator Better Again!**

- Introduces limited automatic correction and completion of expressions.
- The goal is to allow uninterrupted typing and avoid disruptions when a
partially entered expression is temporarily invalid (which previously
caused the result to be replaced by an error message or hidden by the
fallback).
  - The implementation intentionally aims for a sweet spot:
    - Ignores trailing binary operators.
    - Automatically closes all opened parentheses.
- It is not exhaustive; for example, incomplete constants or functions
may still result in an invalid query.
- Copy current result to the search bar.
- Adds an option to copy the current result to the search bar when the
user types `=` at the end of the expression.
  - Adds a new menu item for the same action.
  - Fixes the **Save** command to also copy the result to the query.
- Adds support for the `factorial(x)` function and the `x!` expression.
- Factorial calculations are supported up to `170!` (limited by
`double`), but display is constrained by decimal conversion and allows
direct display of results up to `20!`.
- Adds support for the `sign(x)` function.
- Adds support for the `π` symbol as an alternative to the `pi`
constant.
- Adds a context menu item to the result list item and fallback that
displays the octal representation of the result.
- Implements beautification of the query:
- Converts technical symbols such as `*` or `/` to `×` or `÷`,
respectively.
- Not enabled for fallbacks for now, since the item text should match
the query to keep the score intact.
- Implements additional normalization of symbols in the query:
  - Percent: `%`, `%`, `﹪`
  - Minus: `−`, `-`, `–`, `—`
  - Factorial: `!`, `!`
- Multiplication: `*`, `×`, `∗`, `·`, `⋅`, `✕`, `✖`, `\u2062` (invisible
times)
  - Division: `/`, `÷`, ``, `:`
- Allows use of `²` and `³` as alternatives to `^2` and `^3`.
- Updates the unit test that was culture sensitive to force en-US output
(not an actual fix, but at least it clears false positive for now)
- Fixes pre-parsing of scientific notation to prevent capturing minus
sign as part of it.
- Fixes normalization/rounding of the result, so it can display small
values (the current solution turned it into a string with scientific
notation and couldn't parse it back).
- Updates test with new cases

## Pictures? Moving!

Previous behavior:


https://github.com/user-attachments/assets/ebcdcd85-797a-44f9-a8b1-a0f2f33c6b42

New behavior:


https://github.com/user-attachments/assets/5bd94663-a0d0-4d7d-8032-1030e79926c3





<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43481
- [x] Closes: #43460
- [x] Closes: #42078
- [x] Closes: #41839
- [x] Closes: #39659
- [x] Closes: #40502
- [x] Related to: #41715
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- 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:
Jiří Polášek
2026-01-29 04:23:39 +01:00
committed by GitHub
parent 4694e99477
commit 0de2af77ac
21 changed files with 721 additions and 57 deletions

View File

@@ -53,6 +53,56 @@ public static class BracketHelper
return trailTest.Count == 0;
}
public static string BalanceBrackets(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
return query ?? string.Empty;
}
var openBrackets = new Stack<TrailType>();
for (var i = 0; i < query.Length; i++)
{
var (direction, type) = BracketTrail(query[i]);
if (direction == TrailDirection.None)
{
continue;
}
if (direction == TrailDirection.Open)
{
openBrackets.Push(type);
}
else if (direction == TrailDirection.Close)
{
// Only pop if we have a matching open bracket
if (openBrackets.Count > 0 && openBrackets.Peek() == type)
{
openBrackets.Pop();
}
}
}
if (openBrackets.Count == 0)
{
return query;
}
// Build closing brackets in LIFO order
var closingBrackets = new char[openBrackets.Count];
var index = 0;
while (openBrackets.Count > 0)
{
var type = openBrackets.Pop();
closingBrackets[index++] = type == TrailType.Round ? ')' : ']';
}
return query + new string(closingBrackets);
}
private static (TrailDirection Direction, TrailType Type) BracketTrail(char @char)
{
switch (@char)

View File

@@ -1,9 +1,8 @@
// Copyright (c) Microsoft Corporation
// 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.Globalization;
using System.Text.RegularExpressions;
using CalculatorEngineCommon;
@@ -16,6 +15,7 @@ public static class CalculateEngine
private static readonly PropertySet _constants = new()
{
{ "pi", Math.PI },
{ "π", Math.PI },
{ "e", Math.E },
};
@@ -59,6 +59,8 @@ public static class CalculateEngine
input = CalculateHelper.FixHumanMultiplicationExpressions(input);
input = CalculateHelper.UpdateFactorialFunctions(input);
// Get the user selected trigonometry unit
TrigMode trigMode = settings.TrigUnit;
@@ -77,6 +79,13 @@ public static class CalculateEngine
return default;
}
// If we're out of bounds
if (result is "inf" or "-inf")
{
error = Properties.Resources.calculator_not_covert_to_decimal;
return default;
}
if (string.IsNullOrEmpty(result))
{
return default;
@@ -110,15 +119,19 @@ public static class CalculateEngine
/// </summary>
public static decimal FormatMax15Digits(decimal value, CultureInfo cultureInfo)
{
const int maxDisplayDigits = 15;
if (value == 0m)
{
return 0m;
}
var absValue = Math.Abs(value);
var integerDigits = absValue >= 1 ? (int)Math.Floor(Math.Log10((double)absValue)) + 1 : 1;
var maxDecimalDigits = Math.Max(0, 15 - integerDigits);
var maxDecimalDigits = Math.Max(0, maxDisplayDigits - integerDigits);
var rounded = Math.Round(value, maxDecimalDigits, MidpointRounding.AwayFromZero);
var formatted = rounded.ToString("G29", cultureInfo);
return Convert.ToDecimal(formatted, cultureInfo);
return rounded / 1.000000000000000000000000000000000m;
}
}

View File

@@ -3,11 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class CalculateHelper
public static partial class CalculateHelper
{
private static readonly Regex RegValidExpressChar = new Regex(
@"^(" +
@@ -19,7 +20,7 @@ public static class CalculateHelper
@"rad\s*\(|deg\s*\(|grad\s*\(|" + /* trigonometry unit conversion macros */
@"pi|" +
@"==|~=|&&|\|\||" +
@"((-?(\d+(\.\d*)?)|-?(\.\d+))[Ee](-?\d+))|" + /* expression from CheckScientificNotation between parenthesis */
@"((\d+(?:\.\d*)?|\.\d+)[eE](-?\d+))|" + /* expression from CheckScientificNotation between parenthesis */
@"e|[0-9]|0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|[\+\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
@")+$",
RegexOptions.Compiled);
@@ -31,6 +32,94 @@ public static class CalculateHelper
private const string RadToDeg = "(180 / pi) * ";
private const string RadToGrad = "(200 / pi) * ";
// replacements from the user input to displayed query
private static readonly Dictionary<string, string> QueryReplacements = new()
{
{ "", "%" }, { "﹪", "%" },
{ "", "-" }, { "", "-" }, { "—", "-" },
{ "", "!" },
{ "*", "×" }, { "", "×" }, { "·", "×" }, { "⊗", "×" }, { "⋅", "×" }, { "✕", "×" }, { "✖", "×" }, { "\u2062", "×" },
{ "/", "÷" }, { "", "÷" }, { "➗", "÷" }, { ":", "÷" },
};
// replacements from a query to engine input
private static readonly Dictionary<string, string> EngineReplacements = new()
{
{ "×", "*" },
{ "÷", "/" },
};
private static readonly Dictionary<string, string> SuperscriptReplacements = new()
{
{ "²", "^2" }, { "³", "^3" },
};
private static readonly HashSet<char> StandardOperators = [
// binary operators; doesn't make sense for them to be at the end of a query
'+', '-', '*', '/', '%', '^', '=', '&', '|', '\\',
// parentheses
'(', '[',
];
private static readonly HashSet<char> SuffixOperators = [
// unary operators; can appear at the end of a query
')', ']', '!',
];
private static readonly Regex ReplaceScientificNotationRegex = CreateReplaceScientificNotationRegex();
public static char[] GetQueryOperators()
{
var ops = new HashSet<char>(StandardOperators);
ops.ExceptWith(SuffixOperators);
return [.. ops];
}
/// <summary>
/// Normalizes the query for display
/// This replaces standard operators with more visually appealing ones (e.g., '*' -> '×') if enabled.
/// Always applies safe normalizations (standardizing variants like minus, percent, etc.).
/// </summary>
/// <param name="input">The query string to normalize.</param>
public static string NormalizeCharsForDisplayQuery(string input)
{
// 1. Safe/Trivial replacements (Variant -> Standard)
// These are always applied to ensure consistent behavior for non-math symbols (spaces) and
// operator variants like minus, percent, and exclamation mark.
foreach (var (key, value) in QueryReplacements)
{
input = input.Replace(key, value);
}
return input;
}
/// <summary>
/// Normalizes the query for the calculation engine.
/// This replaces all supported operator variants (visual or standard) with the specific
/// ASCII operators required by the engine (e.g., '×' -> '*').
/// It duplicates and expands upon replacements in NormalizeQuery to ensure the engine
/// receives valid input regardless of whether NormalizeQuery was executed.
/// </summary>
public static string NormalizeCharsToEngine(string input)
{
foreach (var (key, value) in EngineReplacements)
{
input = input.Replace(key, value);
}
// Replace superscript characters with their engine equivalents (e.g., '²' -> '^2')
foreach (var (key, value) in SuperscriptReplacements)
{
input = input.Replace(key, value);
}
return input;
}
public static bool InputValid(string input)
{
if (string.IsNullOrWhiteSpace(input))
@@ -50,7 +139,7 @@ public static class CalculateHelper
// If the input ends with a binary operator then it is not a valid input to mages and the Interpret function would throw an exception. Because we expect here that the user has not finished typing we block those inputs.
var trimmedInput = input.TrimEnd();
if (trimmedInput.EndsWith('+') || trimmedInput.EndsWith('-') || trimmedInput.EndsWith('*') || trimmedInput.EndsWith('|') || trimmedInput.EndsWith('\\') || trimmedInput.EndsWith('^') || trimmedInput.EndsWith('=') || trimmedInput.EndsWith('&') || trimmedInput.EndsWith('/') || trimmedInput.EndsWith('%'))
if (EndsWithBinaryOperator(trimmedInput))
{
return false;
}
@@ -58,6 +147,18 @@ public static class CalculateHelper
return true;
}
private static bool EndsWithBinaryOperator(string input)
{
var operators = GetQueryOperators();
if (string.IsNullOrEmpty(input))
{
return false;
}
var lastChar = input[^1];
return Array.Exists(operators, op => op == lastChar);
}
public static string FixHumanMultiplicationExpressions(string input)
{
var output = CheckScientificNotation(input);
@@ -72,18 +173,7 @@ public static class CalculateHelper
private static string CheckScientificNotation(string input)
{
/**
* NOTE: By the time that the expression gets to us, it's already in English format.
*
* Regex explanation:
* (-?(\d+({0}\d*)?)|-?({0}\d+)): Used to capture one of two types:
* -?(\d+({0}\d*)?): Captures a decimal number starting with a number (e.g. "-1.23")
* -?({0}\d+): Captures a decimal number without leading number (e.g. ".23")
* e: Captures 'e' or 'E'
* (-?\d+): Captures an integer number (e.g. "-1" or "23")
*/
var p = @"(-?(\d+(\.\d*)?)|-?(\.\d+))e(-?\d+)";
return Regex.Replace(input, p, "($1 * 10^($5))", RegexOptions.IgnoreCase);
return ReplaceScientificNotationRegex.Replace(input, "($1 * 10^($2))");
}
/*
@@ -292,6 +382,86 @@ public static class CalculateHelper
return modifiedInput;
}
public static string UpdateFactorialFunctions(string input)
{
// Handle n! -> factorial(n)
int startSearch = 0;
while (true)
{
var index = input.IndexOf('!', startSearch);
if (index == -1)
{
break;
}
// Ignore !=
if (index + 1 < input.Length && input[index + 1] == '=')
{
startSearch = index + 2;
continue;
}
if (index == 0)
{
startSearch = index + 1;
continue;
}
// Scan backwards
var endArg = index - 1;
while (endArg >= 0 && char.IsWhiteSpace(input[endArg]))
{
endArg--;
}
if (endArg < 0)
{
startSearch = index + 1;
continue;
}
var startArg = endArg;
if (input[endArg] == ')')
{
// Find matching '('
startArg = FindOpeningBracketIndexInFrontOfIndex(input, endArg);
if (startArg == -1)
{
startSearch = index + 1;
continue;
}
}
else
{
// Scan back for number or word
while (startArg >= 0 && (char.IsLetterOrDigit(input[startArg]) || input[startArg] == '.'))
{
startArg--;
}
startArg++; // Move back to first valid char
}
if (startArg > endArg)
{
// No argument found
startSearch = index + 1;
continue;
}
// Extract argument
var arg = input.Substring(startArg, endArg - startArg + 1);
// Replace <arg><whitespace>! with factorial(<arg>)
input = input.Remove(startArg, index - startArg + 1);
input = input.Insert(startArg, $"factorial({arg})");
startSearch = 0; // Reset search because string changed
}
return input;
}
private static string ModifyMathFunction(string input, string function, string modification)
{
// Create the pattern to match the function, opening bracket, and any spaces in between
@@ -325,4 +495,43 @@ public static class CalculateHelper
return modifiedInput;
}
private static int FindOpeningBracketIndexInFrontOfIndex(string input, int end)
{
var bracketCount = 0;
for (var i = end; i >= 0; i--)
{
switch (input[i])
{
case ')':
bracketCount++;
break;
case '(':
{
bracketCount--;
if (bracketCount == 0)
{
return i;
}
break;
}
}
}
return -1;
}
/*
* NOTE: By the time that the expression gets to us, it's already in English format.
*
* Regex explanation:
* (-?(\d+({0}\d*)?)|-?({0}\d+)): Used to capture one of two types:
* -?(\d+({0}\d*)?): Captures a decimal number starting with a number (e.g. "-1.23")
* -?({0}\d+): Captures a decimal number without leading number (e.g. ".23")
* e: Captures 'e' or 'E'
* (?\d+): Captures an integer number (e.g. "-1" or "23")
*/
[GeneratedRegex(@"(\d+(?:\.\d*)?|\.\d+)e(-?\d+)", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex CreateReplaceScientificNotationRegex();
}

View File

@@ -2,8 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Calc.Helper;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public interface ISettingsInterface
@@ -15,4 +13,8 @@ public interface ISettingsInterface
public bool OutputUseEnglishFormat { get; }
public bool CloseOnEnter { get; }
public bool CopyResultToSearchBarIfQueryEndsWithEqualSign { get; }
public bool AutoFixQuery { get; }
}

View File

@@ -12,7 +12,13 @@ namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static partial class QueryHelper
{
public static ListItem Query(string query, ISettingsInterface settings, bool isFallbackSearch, TypedEventHandler<object, object> handleSave = null)
public static ListItem Query(
string query,
ISettingsInterface settings,
bool isFallbackSearch,
out string displayQuery,
TypedEventHandler<object, object> handleSave = null,
TypedEventHandler<object, object> handleReplace = null)
{
ArgumentNullException.ThrowIfNull(query);
if (!isFallbackSearch)
@@ -20,26 +26,50 @@ public static partial class QueryHelper
ArgumentNullException.ThrowIfNull(handleSave);
}
CultureInfo inputCulture = settings.InputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
CultureInfo outputCulture = settings.OutputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
CultureInfo inputCulture =
settings.InputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
CultureInfo outputCulture =
settings.OutputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
// In case the user pastes a query with a leading =
query = query.TrimStart('=');
query = query.TrimStart('=').TrimStart();
// Enables better looking characters for multiplication and division (e.g., '×' and '÷')
displayQuery = CalculateHelper.NormalizeCharsForDisplayQuery(query);
// Happens if the user has only typed the action key so far
if (string.IsNullOrEmpty(query))
if (string.IsNullOrEmpty(displayQuery))
{
return null;
}
NumberTranslator translator = NumberTranslator.Create(inputCulture, new CultureInfo("en-US"));
var input = translator.Translate(query.Normalize(NormalizationForm.FormKC));
// Normalize query to engine format (e.g., replace '×' with '*', converts superscripts to functions)
// This must be done before any further normalization to avoid losing information
var engineQuery = CalculateHelper.NormalizeCharsToEngine(displayQuery);
// Cleanup rest of the Unicode characters, whitespace
var queryForEngine2 = engineQuery.Normalize(NormalizationForm.FormKC);
// Translate numbers from input culture to en-US culture for the calculation engine
var translator = NumberTranslator.Create(inputCulture, new CultureInfo("en-US"));
// Translate the input query
var input = translator.Translate(queryForEngine2);
if (string.IsNullOrWhiteSpace(input))
{
return ErrorHandler.OnError(isFallbackSearch, query, Properties.Resources.calculator_expression_empty);
}
// normalize again to engine chars after translation
input = CalculateHelper.NormalizeCharsToEngine(input);
// Auto fix incomplete queries (if enabled)
if (settings.AutoFixQuery && TryGetIncompleteQuery(input, out var newInput))
{
input = newInput;
}
if (!CalculateHelper.InputValid(input))
{
return null;
@@ -60,10 +90,10 @@ public static partial class QueryHelper
if (isFallbackSearch)
{
// Fallback search
return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, query);
return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, displayQuery);
}
return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, query, settings, handleSave);
return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, displayQuery, settings, handleSave, handleReplace);
}
catch (OverflowException)
{
@@ -77,4 +107,32 @@ public static partial class QueryHelper
return ErrorHandler.OnError(isFallbackSearch, query, default, e);
}
}
public static bool TryGetIncompleteQuery(string query, out string newQuery)
{
newQuery = query;
var trimmed = query.TrimEnd();
if (string.IsNullOrEmpty(trimmed))
{
return false;
}
// 1. Trim trailing operators
var operators = CalculateHelper.GetQueryOperators();
while (trimmed.Length > 0 && Array.IndexOf(operators, trimmed[^1]) > -1)
{
trimmed = trimmed[..^1].TrimEnd();
}
if (trimmed.Length == 0)
{
return false;
}
// 2. Fix brackets
newQuery = BracketHelper.BalanceBrackets(trimmed);
return true;
}
}

View File

@@ -0,0 +1,26 @@
// 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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public sealed partial class ReplaceQueryCommand : InvokableCommand
{
public event TypedEventHandler<object, object> ReplaceRequested;
public ReplaceQueryCommand()
{
Name = "Replace query";
Icon = new IconInfo("\uE70F"); // Edit icon
}
public override ICommandResult Invoke()
{
ReplaceRequested?.Invoke(this, null);
return CommandResult.KeepOpen();
}
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -13,7 +14,14 @@ namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class ResultHelper
{
public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query, ISettingsInterface settings, TypedEventHandler<object, object> handleSave)
public static ListItem CreateResult(
decimal? roundedResult,
CultureInfo inputCulture,
CultureInfo outputCulture,
string query,
ISettingsInterface settings,
TypedEventHandler<object, object> handleSave,
TypedEventHandler<object, object> handleReplace)
{
// Return null when the expression is not a valid calculator query.
if (roundedResult is null)
@@ -28,6 +36,9 @@ public static class ResultHelper
var saveCommand = new SaveCommand(result);
saveCommand.SaveRequested += handleSave;
var replaceCommand = new ReplaceQueryCommand();
replaceCommand.ReplaceRequested += handleReplace;
var copyCommandItem = CreateResult(roundedResult, inputCulture, outputCulture, query);
// No TextToSuggest on the main save command item. We don't want to keep suggesting what the result is,
@@ -40,6 +51,7 @@ public static class ResultHelper
Subtitle = query,
MoreCommands = [
new CommandContextItem(settings.CloseOnEnter ? saveCommand : copyCommandItem.Command),
new CommandContextItem(replaceCommand) { RequestedShortcut = KeyChords.CopyResultToSearchBox, },
..copyCommandItem.MoreCommands,
],
};
@@ -55,11 +67,15 @@ public static class ResultHelper
var decimalResult = roundedResult?.ToString(outputCulture);
List<CommandContextItem> context = [];
List<IContextItem> context = [];
if (decimal.IsInteger((decimal)roundedResult))
{
context.Add(new Separator());
var i = decimal.ToInt64((decimal)roundedResult);
// hexadecimal
try
{
var hexResult = "0x" + i.ToString("X", outputCulture);
@@ -70,9 +86,10 @@ public static class ResultHelper
}
catch (Exception ex)
{
Logger.LogError("Error parsing hex format", ex);
Logger.LogError("Error converting to hex format", ex);
}
// binary
try
{
var binaryResult = "0b" + i.ToString("B", outputCulture);
@@ -83,7 +100,21 @@ public static class ResultHelper
}
catch (Exception ex)
{
Logger.LogError("Error parsing binary format", ex);
Logger.LogError("Error converting to binary format", ex);
}
// octal
try
{
var octalResult = "0o" + Convert.ToString(i, 8);
context.Add(new CommandContextItem(new CopyTextCommand(octalResult) { Name = Properties.Resources.calculator_copy_octal })
{
Title = octalResult,
});
}
catch (Exception ex)
{
Logger.LogError("Error converting to octal format", ex);
}
}

View File

@@ -45,6 +45,18 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Properties.Resources.calculator_settings_close_on_enter_description,
true);
private readonly ToggleSetting _copyResultToSearchBarIfQueryEndsWithEqualSign = new(
Namespaced(nameof(CopyResultToSearchBarIfQueryEndsWithEqualSign)),
Properties.Resources.calculator_settings_copy_result_to_search_bar,
Properties.Resources.calculator_settings_copy_result_to_search_bar_description,
false);
private readonly ToggleSetting _autoFixQuery = new(
Namespaced(nameof(AutoFixQuery)),
Properties.Resources.calculator_settings_auto_fix_query,
Properties.Resources.calculator_settings_auto_fix_query_description,
true);
public CalculateEngine.TrigMode TrigUnit
{
get
@@ -81,6 +93,10 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
public bool CloseOnEnter => _closeOnEnter.Value;
public bool CopyResultToSearchBarIfQueryEndsWithEqualSign => _copyResultToSearchBarIfQueryEndsWithEqualSign.Value;
public bool AutoFixQuery => _autoFixQuery.Value;
internal static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
@@ -98,6 +114,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_inputUseEnNumberFormat);
Settings.Add(_outputUseEnNumberFormat);
Settings.Add(_closeOnEnter);
Settings.Add(_copyResultToSearchBarIfQueryEndsWithEqualSign);
Settings.Add(_autoFixQuery);
// Load settings from file upon initialization
LoadSettings();

View File

@@ -0,0 +1,13 @@
// 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 Microsoft.CommandPalette.Extensions;
using Windows.System;
namespace Microsoft.CmdPal.Ext.Calc;
internal static class KeyChords
{
internal static KeyChord CopyResultToSearchBox { get; } = new(VirtualKeyModifiers.Control | VirtualKeyModifiers.Shift, (int)VirtualKey.Enter, 0);
}

View File

@@ -25,12 +25,12 @@ public sealed partial class CalculatorListPage : DynamicListPage
private readonly Lock _resultsLock = new();
private readonly ISettingsInterface _settingsManager;
private readonly List<ListItem> _items = [];
private readonly List<ListItem> history = [];
private readonly List<ListItem> _history = [];
private readonly ListItem _emptyItem;
// This is the text that saved when the user click the result.
// We need to avoid the double calculation. This may cause some wierd behaviors.
private string skipQuerySearchText = string.Empty;
private string _skipQuerySearchText = string.Empty;
public CalculatorListPage(ISettingsInterface settings)
{
@@ -54,6 +54,17 @@ public sealed partial class CalculatorListPage : DynamicListPage
UpdateSearchText(string.Empty, string.Empty);
}
private void HandleReplaceQuery(object sender, object args)
{
var lastResult = _items[0].Title;
if (!string.IsNullOrEmpty(lastResult))
{
_skipQuerySearchText = lastResult;
SearchText = lastResult;
OnPropertyChanged(nameof(SearchText));
}
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
if (oldSearch == newSearch)
@@ -61,19 +72,37 @@ public sealed partial class CalculatorListPage : DynamicListPage
return;
}
if (!string.IsNullOrEmpty(skipQuerySearchText) && newSearch == skipQuerySearchText)
if (!string.IsNullOrEmpty(_skipQuerySearchText) && newSearch == _skipQuerySearchText)
{
// only skip once.
skipQuerySearchText = string.Empty;
_skipQuerySearchText = string.Empty;
return;
}
skipQuerySearchText = string.Empty;
var copyResultToSearchText = false;
if (_settingsManager.CopyResultToSearchBarIfQueryEndsWithEqualSign && newSearch.EndsWith('='))
{
newSearch = newSearch.TrimEnd('=').TrimEnd();
copyResultToSearchText = true;
}
_skipQuerySearchText = string.Empty;
_emptyItem.Subtitle = newSearch;
var result = QueryHelper.Query(newSearch, _settingsManager, false, HandleSave);
var result = QueryHelper.Query(newSearch, _settingsManager, isFallbackSearch: false, out var displayQuery, HandleSave, HandleReplaceQuery);
UpdateResult(result);
if (copyResultToSearchText && result is not null)
{
_skipQuerySearchText = result.Title;
SearchText = result.Title;
// LOAD BEARING: The SearchText setter does not raise a PropertyChanged notification,
// so we must raise it explicitly to ensure the UI updates correctly.
OnPropertyChanged(nameof(SearchText));
}
}
private void UpdateResult(ListItem result)
@@ -91,7 +120,7 @@ public sealed partial class CalculatorListPage : DynamicListPage
_items.Add(_emptyItem);
}
this._items.AddRange(history);
this._items.AddRange(_history);
}
RaiseItemsChanged(this._items.Count);
@@ -109,7 +138,7 @@ public sealed partial class CalculatorListPage : DynamicListPage
TextToSuggest = lastResult,
};
history.Insert(0, li);
_history.Insert(0, li);
_items.Insert(1, li);
// Why we need to clean the query record? Removed, but if necessary, please move it back.
@@ -117,9 +146,14 @@ public sealed partial class CalculatorListPage : DynamicListPage
// this change will call the UpdateSearchText again.
// We need to avoid it.
skipQuerySearchText = lastResult;
_skipQuerySearchText = lastResult;
SearchText = lastResult;
this.RaiseItemsChanged(this._items.Count);
// LOAD BEARING: The SearchText setter does not raise a PropertyChanged notification,
// so we must raise it explicitly to ensure the UI updates correctly.
OnPropertyChanged(nameof(SearchText));
RaiseItemsChanged(this._items.Count);
}
}

View File

@@ -27,7 +27,7 @@ public sealed partial class FallbackCalculatorItem : FallbackCommandItem
public override void UpdateQuery(string query)
{
var result = QueryHelper.Query(query, _settings, true, null);
var result = QueryHelper.Query(query, _settings, true, out _);
if (result is null)
{

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -96,6 +96,15 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Copy octal.
/// </summary>
public static string calculator_copy_octal {
get {
return ResourceManager.GetString("calculator_copy_octal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculator.
/// </summary>
@@ -186,6 +195,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Fix incomplete calculations automatically.
/// </summary>
public static string calculator_settings_auto_fix_query {
get {
return ResourceManager.GetString("calculator_settings_auto_fix_query", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Attempt to evaluate incomplete calculations by ignoring extra operators or symbols.
/// </summary>
public static string calculator_settings_auto_fix_query_description {
get {
return ResourceManager.GetString("calculator_settings_auto_fix_query_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Close on Enter.
/// </summary>
@@ -204,6 +231,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Replace query with result on equals.
/// </summary>
public static string calculator_settings_copy_result_to_search_bar {
get {
return ResourceManager.GetString("calculator_settings_copy_result_to_search_bar", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Updates the query to the result when (=) is entered.
/// </summary>
public static string calculator_settings_copy_result_to_search_bar_description {
get {
return ResourceManager.GetString("calculator_settings_copy_result_to_search_bar_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use English (United States) number format for input.
/// </summary>
@@ -222,6 +267,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Handle extra operators and symbols.
/// </summary>
public static string calculator_settings_input_normalization {
get {
return ResourceManager.GetString("calculator_settings_input_normalization", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enable advanced input normalization and extra symbols (e.g. ÷, ×, π).
/// </summary>
public static string calculator_settings_input_normalization_description {
get {
return ResourceManager.GetString("calculator_settings_input_normalization_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use English (United States) number format for output.
/// </summary>

View File

@@ -208,4 +208,25 @@
<data name="calculator_expression_empty" xml:space="preserve">
<value>Please enter an expression</value>
</data>
<data name="calculator_settings_copy_result_to_search_bar" xml:space="preserve">
<value>Replace query with result on equals</value>
</data>
<data name="calculator_settings_copy_result_to_search_bar_description" xml:space="preserve">
<value>Updates the query to the result when (=) is entered</value>
</data>
<data name="calculator_settings_auto_fix_query" xml:space="preserve">
<value>Fix incomplete calculations automatically</value>
</data>
<data name="calculator_settings_auto_fix_query_description" xml:space="preserve">
<value>Attempt to evaluate incomplete calculations by ignoring extra operators or symbols</value>
</data>
<data name="calculator_settings_input_normalization" xml:space="preserve">
<value>Handle extra operators and symbols</value>
</data>
<data name="calculator_settings_input_normalization_description" xml:space="preserve">
<value>Enable advanced input normalization and extra symbols (e.g. ÷, ×, π)</value>
</data>
<data name="calculator_copy_octal" xml:space="preserve">
<value>Copy octal</value>
</data>
</root>