From 0de2af77ac5ab0d75fac923d8fe27dd381d6796e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Thu, 29 Jan 2026 04:23:39 +0100 Subject: [PATCH] CmdPal: Make Calculator Great Again (#44594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 ## PR Checklist - [x] Closes: #43481 - [x] Closes: #43460 - [x] Closes: #42078 - [x] Closes: #41839 - [x] Closes: #39659 - [x] Closes: #40502 - [x] Related to: #41715 - [ ] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .github/actions/spell-check/expect.txt | 1 + .../ExprtkEvaluator.cpp | 21 ++ .../CloseOnEnterTests.cs | 8 +- .../ExtendedCalculatorParserTests.cs | 27 +- .../IncompleteQueryTests.cs | 38 +++ .../QueryHelperTests.cs | 27 ++ .../QueryTests.cs | 3 +- .../Settings.cs | 17 +- .../Helper/BracketHelper.cs | 50 ++++ .../Helper/CalculateEngine.cs | 27 +- .../Helper/CalculateHelper.cs | 239 ++++++++++++++++-- .../Helper/ISettingsInterface.cs | 6 +- .../Helper/QueryHelper.cs | 76 +++++- .../Helper/ReplaceQueryCommand.cs | 26 ++ .../Helper/ResultHelper.cs | 39 ++- .../Helper/SettingsManager.cs | 18 ++ .../Microsoft.CmdPal.Ext.Calc/KeyChords.cs | 13 + .../Pages/CalculatorListPage.cs | 54 +++- .../Pages/FallbackCalculatorItem.cs | 2 +- .../Properties/Resources.Designer.cs | 65 ++++- .../Properties/Resources.resx | 21 ++ 21 files changed, 721 insertions(+), 57 deletions(-) create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/IncompleteQueryTests.cs create mode 100644 src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryHelperTests.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ReplaceQueryCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/KeyChords.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 25c714a03f..fb6af4b35f 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1826,6 +1826,7 @@ TEXTBOXNEWLINE textextractor TEXTINCLUDE tfopen +tgamma tgz THEMECHANGED themeresources diff --git a/src/common/CalculatorEngineCommon/ExprtkEvaluator.cpp b/src/common/CalculatorEngineCommon/ExprtkEvaluator.cpp index dbc2120b24..eb04c18783 100644 --- a/src/common/CalculatorEngineCommon/ExprtkEvaluator.cpp +++ b/src/common/CalculatorEngineCommon/ExprtkEvaluator.cpp @@ -3,9 +3,27 @@ #include #include #include +#include +#include namespace ExprtkCalculator::internal { + static double factorial(const double n) + { + // Only allow non-negative integers + if (n < 0.0 || std::floor(n) != n) + { + return std::numeric_limits::quiet_NaN(); + } + return std::tgamma(n + 1.0); + } + + static double sign(const double n) + { + if (n > 0.0) return 1.0; + if (n < 0.0) return -1.0; + return 0.0; + } std::wstring ToWStringFullPrecision(double value) { @@ -25,6 +43,9 @@ namespace ExprtkCalculator::internal symbol_table.add_constant(name, value); } + symbol_table.add_function("factorial", factorial); + symbol_table.add_function("sign", sign); + exprtk::expression expression; expression.register_symbol_table(symbol_table); diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs index 5c4cf39783..04771fc621 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/CloseOnEnterTests.cs @@ -19,6 +19,7 @@ public class CloseOnEnterTests { var settings = new Settings(closeOnEnter: true); TypedEventHandler handleSave = (s, e) => { }; + TypedEventHandler handleReplace = (s, e) => { }; var item = ResultHelper.CreateResult( 4m, @@ -26,7 +27,8 @@ public class CloseOnEnterTests CultureInfo.CurrentCulture, "2+2", settings, - handleSave); + handleSave, + handleReplace); Assert.IsNotNull(item); Assert.IsInstanceOfType(item.Command, typeof(CopyTextCommand)); @@ -41,6 +43,7 @@ public class CloseOnEnterTests { var settings = new Settings(closeOnEnter: false); TypedEventHandler handleSave = (s, e) => { }; + TypedEventHandler handleReplace = (s, e) => { }; var item = ResultHelper.CreateResult( 4m, @@ -48,7 +51,8 @@ public class CloseOnEnterTests CultureInfo.CurrentCulture, "2+2", settings, - handleSave); + handleSave, + handleReplace); Assert.IsNotNull(item); Assert.IsInstanceOfType(item.Command, typeof(SaveCommand)); diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/ExtendedCalculatorParserTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/ExtendedCalculatorParserTests.cs index b631f59be7..85b712fe20 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/ExtendedCalculatorParserTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/ExtendedCalculatorParserTests.cs @@ -65,6 +65,9 @@ public class ExtendedCalculatorParserTests : CommandPaletteUnitTestBase ["log10(3)", 0.47712125471966M], ["ln(e)", 1M], ["cosh(0)", 1M], + ["1*10^(-5)", 0.00001M], + ["1*10^(-15)", 0.0000000000000001M], + ["1*10^(-16)", 0M], ]; [DataTestMethod] @@ -192,9 +195,11 @@ public class ExtendedCalculatorParserTests : CommandPaletteUnitTestBase private static IEnumerable Interpret_MustReturnExpectedResult_WhenCalled_Data => [ - // ["factorial(5)", 120M], ToDo: this don't support now - // ["sign(-2)", -1M], - // ["sign(2)", +1M], + ["factorial(5)", 120M], + ["5!", 120M], + ["(2+3)!", 120M], + ["sign(-2)", -1M], + ["sign(2)", +1M], ["abs(-2)", 2M], ["abs(2)", 2M], ["0+(1*2)/(0+1)", 2M], // Validate that division by "(0+1)" is not interpret as division by zero. @@ -221,6 +226,9 @@ public class ExtendedCalculatorParserTests : CommandPaletteUnitTestBase [ ["0.2E1", "en-US", 2M], ["0,2E1", "pt-PT", 2M], + ["3.5e3 + 2.5E2", "en-US", 3750M], + ["3,5e3 + 2,5E2", "fr-FR", 3750M], + ["1E3-1E3/1.5", "en-US", 333.333333333333371M], ]; [DataTestMethod] @@ -389,4 +397,17 @@ public class ExtendedCalculatorParserTests : CommandPaletteUnitTestBase Assert.IsNotNull(result); Assert.AreEqual(expectedResult, result); } + + [DataTestMethod] + [DataRow("171!")] + [DataRow("1000!")] + public void Interpret_ReturnsError_WhenValueOverflowsDecimal(string input) + { + var settings = new Settings(); + + CalculateEngine.Interpret(settings, input, CultureInfo.InvariantCulture, out var error); + + Assert.IsFalse(string.IsNullOrEmpty(error)); + Assert.AreNotEqual(null, error); + } } diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/IncompleteQueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/IncompleteQueryTests.cs new file mode 100644 index 0000000000..894660ade0 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/IncompleteQueryTests.cs @@ -0,0 +1,38 @@ +// 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.CmdPal.Ext.Calc.Helper; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Calc.UnitTests; + +[TestClass] +public class IncompleteQueryTests +{ + [DataTestMethod] + [DataRow("2+2+", "2+2")] + [DataRow("2+2*", "2+2")] + [DataRow("sin(30", "sin(30)")] + [DataRow("((1+2)", "((1+2))")] + [DataRow("2*(3+4", "2*(3+4)")] + [DataRow("(1+2", "(1+2)")] + [DataRow("2*(", "2")] + [DataRow("2*(((", "2")] + public void TestTryGetIncompleteQuerySuccess(string input, string expected) + { + var result = QueryHelper.TryGetIncompleteQuery(input, out var newQuery); + Assert.IsTrue(result); + Assert.AreEqual(expected, newQuery); + } + + [DataTestMethod] + [DataRow("")] + [DataRow(" ")] + public void TestTryGetIncompleteQueryFail(string input) + { + var result = QueryHelper.TryGetIncompleteQuery(input, out var newQuery); + Assert.IsFalse(result); + Assert.AreEqual(input, newQuery); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryHelperTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryHelperTests.cs new file mode 100644 index 0000000000..c152dd1f45 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryHelperTests.cs @@ -0,0 +1,27 @@ +// 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.CmdPal.Ext.Calc.Helper; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.Calc.UnitTests; + +[TestClass] +public class QueryHelperTests +{ + [DataTestMethod] + [DataRow("2²", "4")] + [DataRow("2³", "8")] + [DataRow("2!", "2")] + [DataRow("2\u00A0*\u00A02", "4")] // Non-breaking space + [DataRow("20:10", "2")] // Colon as division + public void Interpret_HandlesNormalizedInputs(string input, string expected) + { + var settings = new Settings(); + var result = QueryHelper.Query(input, settings, false, out _, (_, _) => { }); + + Assert.IsNotNull(result); + Assert.AreEqual(expected, result.Title); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs index 73927849a1..fa8a441d43 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/QueryTests.cs @@ -6,7 +6,6 @@ using System.Linq; using Microsoft.CmdPal.Ext.Calc.Helper; using Microsoft.CmdPal.Ext.Calc.Pages; using Microsoft.CmdPal.Ext.UnitTestBase; -using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.CmdPal.Ext.Calc.UnitTests; @@ -72,7 +71,7 @@ public class QueryTests : CommandPaletteUnitTestBase [DataRow("sin(60)", "0.809016", CalculateEngine.TrigMode.Gradians)] public void TrigModeSettingsTest(string input, string expected, CalculateEngine.TrigMode trigMode) { - var settings = new Settings(trigUnit: trigMode); + var settings = new Settings(trigUnit: trigMode, outputUseEnglishFormat: true); var page = new CalculatorListPage(settings); diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/Settings.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/Settings.cs index ccd231767d..767b040fe4 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/Settings.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Calc.UnitTests/Settings.cs @@ -12,17 +12,26 @@ public class Settings : ISettingsInterface private readonly bool inputUseEnglishFormat; private readonly bool outputUseEnglishFormat; private readonly bool closeOnEnter; + private readonly bool copyResultToSearchBarIfQueryEndsWithEqualSign; + private readonly bool autoFixQuery; + private readonly bool inputNormalization; public Settings( CalculateEngine.TrigMode trigUnit = CalculateEngine.TrigMode.Radians, bool inputUseEnglishFormat = false, bool outputUseEnglishFormat = false, - bool closeOnEnter = true) + bool closeOnEnter = true, + bool copyResultToSearchBarIfQueryEndsWithEqualSign = true, + bool autoFixQuery = true, + bool inputNormalization = true) { this.trigUnit = trigUnit; this.inputUseEnglishFormat = inputUseEnglishFormat; this.outputUseEnglishFormat = outputUseEnglishFormat; this.closeOnEnter = closeOnEnter; + this.copyResultToSearchBarIfQueryEndsWithEqualSign = copyResultToSearchBarIfQueryEndsWithEqualSign; + this.autoFixQuery = autoFixQuery; + this.inputNormalization = inputNormalization; } public CalculateEngine.TrigMode TrigUnit => trigUnit; @@ -32,4 +41,10 @@ public class Settings : ISettingsInterface public bool OutputUseEnglishFormat => outputUseEnglishFormat; public bool CloseOnEnter => closeOnEnter; + + public bool CopyResultToSearchBarIfQueryEndsWithEqualSign => copyResultToSearchBarIfQueryEndsWithEqualSign; + + public bool AutoFixQuery => autoFixQuery; + + public bool InputNormalization => inputNormalization; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs index 67d8940ed4..e3e6a3a3fb 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs @@ -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(); + + 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) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs index a927e07499..fea869a497 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs @@ -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 /// 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; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs index ed13acb7b3..0ad44bedd7 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs @@ -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 QueryReplacements = new() + { + { "%", "%" }, { "﹪", "%" }, + { "−", "-" }, { "–", "-" }, { "—", "-" }, + { "!", "!" }, + { "*", "×" }, { "∗", "×" }, { "·", "×" }, { "⊗", "×" }, { "⋅", "×" }, { "✕", "×" }, { "✖", "×" }, { "\u2062", "×" }, + { "/", "÷" }, { "∕", "÷" }, { "➗", "÷" }, { ":", "÷" }, + }; + + // replacements from a query to engine input + private static readonly Dictionary EngineReplacements = new() + { + { "×", "*" }, + { "÷", "/" }, + }; + + private static readonly Dictionary SuperscriptReplacements = new() + { + { "²", "^2" }, { "³", "^3" }, + }; + + private static readonly HashSet StandardOperators = [ + + // binary operators; doesn't make sense for them to be at the end of a query + '+', '-', '*', '/', '%', '^', '=', '&', '|', '\\', + + // parentheses + '(', '[', + ]; + + private static readonly HashSet 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(StandardOperators); + ops.ExceptWith(SuffixOperators); + return [.. ops]; + } + + /// + /// 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.). + /// + /// The query string to normalize. + 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; + } + + /// + /// 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. + /// + 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 ! with factorial() + 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(); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ISettingsInterface.cs index f4b7a50644..f0639aaa29 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ISettingsInterface.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ISettingsInterface.cs @@ -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; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs index 99f782d714..2dc6ff9b3f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs @@ -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 handleSave = null) + public static ListItem Query( + string query, + ISettingsInterface settings, + bool isFallbackSearch, + out string displayQuery, + TypedEventHandler handleSave = null, + TypedEventHandler 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; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ReplaceQueryCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ReplaceQueryCommand.cs new file mode 100644 index 0000000000..2dfb17bd16 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ReplaceQueryCommand.cs @@ -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 ReplaceRequested; + + public ReplaceQueryCommand() + { + Name = "Replace query"; + Icon = new IconInfo("\uE70F"); // Edit icon + } + + public override ICommandResult Invoke() + { + ReplaceRequested?.Invoke(this, null); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs index cd2b811567..0147f73c07 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs @@ -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 handleSave) + public static ListItem CreateResult( + decimal? roundedResult, + CultureInfo inputCulture, + CultureInfo outputCulture, + string query, + ISettingsInterface settings, + TypedEventHandler handleSave, + TypedEventHandler 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 context = []; + List 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); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs index cea59e170f..245af25da7 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs @@ -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(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/KeyChords.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/KeyChords.cs new file mode 100644 index 0000000000..32bf117d90 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/KeyChords.cs @@ -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); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs index 72e5f3db30..70023794e9 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs @@ -25,12 +25,12 @@ public sealed partial class CalculatorListPage : DynamicListPage private readonly Lock _resultsLock = new(); private readonly ISettingsInterface _settingsManager; private readonly List _items = []; - private readonly List history = []; + private readonly List _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); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs index ebf33094f2..935c338a94 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs @@ -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) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Properties/Resources.Designer.cs index 328b57ea96..6dedfe2169 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Properties/Resources.Designer.cs @@ -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 { } } + /// + /// Looks up a localized string similar to Copy octal. + /// + public static string calculator_copy_octal { + get { + return ResourceManager.GetString("calculator_copy_octal", resourceCulture); + } + } + /// /// Looks up a localized string similar to Calculator. /// @@ -186,6 +195,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties { } } + /// + /// Looks up a localized string similar to Fix incomplete calculations automatically. + /// + public static string calculator_settings_auto_fix_query { + get { + return ResourceManager.GetString("calculator_settings_auto_fix_query", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to evaluate incomplete calculations by ignoring extra operators or symbols. + /// + public static string calculator_settings_auto_fix_query_description { + get { + return ResourceManager.GetString("calculator_settings_auto_fix_query_description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Close on Enter. /// @@ -204,6 +231,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties { } } + /// + /// Looks up a localized string similar to Replace query with result on equals. + /// + public static string calculator_settings_copy_result_to_search_bar { + get { + return ResourceManager.GetString("calculator_settings_copy_result_to_search_bar", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updates the query to the result when (=) is entered. + /// + public static string calculator_settings_copy_result_to_search_bar_description { + get { + return ResourceManager.GetString("calculator_settings_copy_result_to_search_bar_description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use English (United States) number format for input. /// @@ -222,6 +267,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties { } } + /// + /// Looks up a localized string similar to Handle extra operators and symbols. + /// + public static string calculator_settings_input_normalization { + get { + return ResourceManager.GetString("calculator_settings_input_normalization", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable advanced input normalization and extra symbols (e.g. ÷, ×, π). + /// + public static string calculator_settings_input_normalization_description { + get { + return ResourceManager.GetString("calculator_settings_input_normalization_description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use English (United States) number format for output. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Properties/Resources.resx index 1da0e6f61e..72e1cc84a0 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Properties/Resources.resx @@ -208,4 +208,25 @@ Please enter an expression + + Replace query with result on equals + + + Updates the query to the result when (=) is entered + + + Fix incomplete calculations automatically + + + Attempt to evaluate incomplete calculations by ignoring extra operators or symbols + + + Handle extra operators and symbols + + + Enable advanced input normalization and extra symbols (e.g. ÷, ×, π) + + + Copy octal + \ No newline at end of file