From 6cf73ce839b16ea0ade07bc0b8052d63e77bea9b Mon Sep 17 00:00:00 2001 From: Yu Leng <42196638+moooyo@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:59:10 +0800 Subject: [PATCH] [cmdpal] Port v1 calculator extension (#38629) * init * update * Remove duplicated cp command * Change the long desc * Update notice.md * Use the same icon for fallback item * Add Rappl to expect list * update notice.md * Move the original order back. * Make Radians become default choice * Fix empty result * Remove unused settings. Move history back. Refactory the query logic * fix typo * merge main * CmdPal: minor calc updates (#38914) A bunch of calc updates * maintain the visibility of the history * add other formats to the context menu #38708 * some other icon tidying --------- Co-authored-by: Yu Leng (from Dev Box) Co-authored-by: Mike Griese --- .github/actions/spell-check/expect.txt | 1 + NOTICE.md | 34 ++ .../CalculatorCommandProvider.cs | 166 +-------- .../Helper/BracketHelper.cs | 86 +++++ .../Helper/CalculateEngine.cs | 127 +++++++ .../Helper/CalculateHelper.cs | 328 ++++++++++++++++++ .../Helper/CalculateResult.cs | 39 +++ .../Helper/CalculatorIcons.cs | 18 + .../Helper/ErrorHandler.cs | 53 +++ .../Helper/NumberTranslator.cs | 144 ++++++++ .../Helper/QueryHelper.cs | 77 ++++ .../Helper/ResultHelper.cs | 103 ++++++ .../Helper/SaveCommand.cs | 31 ++ .../Helper/SettingsManager.cs | 98 ++++++ .../Microsoft.CmdPal.Ext.Calc.csproj | 5 + .../Pages/CalculatorListPage.cs | 127 +++++++ .../Pages/FallbackCalculatorItem.cs | 52 +++ .../Properties/Resources.Designer.cs | 171 +++++++++ .../Properties/Resources.resx | 59 ++++ 19 files changed, 1565 insertions(+), 154 deletions(-) create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateResult.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculatorIcons.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ErrorHandler.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SaveCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 8eba793e1b..46432105d0 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1299,6 +1299,7 @@ QUNS QXZ RAII RAlt +Rappl randi Rasterization Rasterize diff --git a/NOTICE.md b/NOTICE.md index 83a24ef185..92bdf5fe39 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -75,6 +75,40 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ``` +## Utility: Command Palette Built-in Extensions + +### Calculator + +#### Mages + +We use the Mages NuGet package for calculating the result of expression. + +**Source**: [https://github.com/FlorianRappl/Mages](https://github.com/FlorianRappl/Mages) + +``` +The MIT License (MIT) + +Copyright (c) 2016 - 2025 Florian Rappl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + ## Utility: File Explorer Add-ins ### Monaco Editor diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs index 04d474a31a..1478dbbd45 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/CalculatorCommandProvider.cs @@ -2,176 +2,34 @@ // 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.Data; -using System.Globalization; -using System.Text; +using Microsoft.CmdPal.Ext.Calc.Helper; +using Microsoft.CmdPal.Ext.Calc.Pages; using Microsoft.CmdPal.Ext.Calc.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -using Windows.Foundation; namespace Microsoft.CmdPal.Ext.Calc; public partial class CalculatorCommandProvider : CommandProvider { - private readonly ListItem _listItem = new(new CalculatorListPage()) { Subtitle = Resources.calculator_top_level_subtitle }; - private readonly FallbackCalculatorItem _fallback = new(); + private readonly ListItem _listItem = new(new CalculatorListPage(settings)) + { + Subtitle = Resources.calculator_top_level_subtitle, + MoreCommands = [new CommandContextItem(settings.Settings.SettingsPage)], + }; + + private readonly FallbackCalculatorItem _fallback = new(settings); + private static SettingsManager settings = new(); public CalculatorCommandProvider() { Id = "Calculator"; DisplayName = Resources.calculator_display_name; - Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg"); + Icon = CalculatorIcons.ProviderIcon; + Settings = settings.Settings; } public override ICommandItem[] TopLevelCommands() => [_listItem]; public override IFallbackCommandItem[] FallbackCommands() => [_fallback]; } - -// The calculator page is a dynamic list page -// * The first command is where we display the results. Title=result, Subtitle=query -// - The default command is `SaveCommand`. -// - When you save, insert into list at spot 1 -// - change SearchText to the result -// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard -// * The rest of the items are previously saved results -// - Command is a CopyCommand -// - Each item also sets the TextToSuggest to the result -[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")] -public sealed partial class CalculatorListPage : DynamicListPage -{ - private readonly List _items = []; - private readonly SaveCommand _saveCommand = new(); - private readonly CopyTextCommand _copyContextCommand; - private readonly CommandContextItem _copyContextMenuItem; - private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.calculator_error); - - public CalculatorListPage() - { - Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg"); - Name = Resources.calculator_title; - PlaceholderText = Resources.calculator_placeholder_text; - Id = "com.microsoft.cmdpal.calculator"; - - _copyContextCommand = new CopyTextCommand(string.Empty); - _copyContextMenuItem = new CommandContextItem(_copyContextCommand); - - _items.Add(new(_saveCommand) { Icon = new IconInfo("\uE94E") }); - - UpdateSearchText(string.Empty, string.Empty); - - _saveCommand.SaveRequested += HandleSave; - } - - private void HandleSave(object sender, object args) - { - var lastResult = _items[0].Title; - if (!string.IsNullOrEmpty(lastResult)) - { - var li = new ListItem(new CopyTextCommand(lastResult)) - { - Title = _items[0].Title, - Subtitle = _items[0].Subtitle, - TextToSuggest = lastResult, - }; - _items.Insert(1, li); - _items[0].Subtitle = string.Empty; - SearchText = lastResult; - this.RaiseItemsChanged(this._items.Count); - } - } - - public override void UpdateSearchText(string oldSearch, string newSearch) - { - var firstItem = _items[0]; - if (string.IsNullOrEmpty(newSearch)) - { - firstItem.Title = Resources.calculator_placeholder_text; - firstItem.Subtitle = string.Empty; - firstItem.MoreCommands = []; - } - else - { - _copyContextCommand.Text = ParseQuery(newSearch, out var result) ? result : string.Empty; - firstItem.Title = result; - firstItem.Subtitle = newSearch; - firstItem.MoreCommands = [_copyContextMenuItem]; - } - } - - internal static bool ParseQuery(string equation, out string result) - { - try - { - var resultNumber = new DataTable().Compute(equation, null); - result = resultNumber.ToString() ?? string.Empty; - return true; - } - catch (Exception e) - { - result = string.Format(CultureInfo.CurrentCulture, ErrorMessage, e.Message); - return false; - } - } - - public override IListItem[] GetItems() => _items.ToArray(); -} - -[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")] -public sealed partial class SaveCommand : InvokableCommand -{ - public event TypedEventHandler SaveRequested; - - public SaveCommand() - { - Name = Resources.calculator_save_command_name; - } - - public override ICommandResult Invoke() - { - SaveRequested?.Invoke(this, this); - return CommandResult.KeepOpen(); - } -} - -[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")] -internal sealed partial class FallbackCalculatorItem : FallbackCommandItem -{ - private readonly CopyTextCommand _copyCommand = new(string.Empty); - private static readonly IconInfo _cachedIcon = IconHelpers.FromRelativePath("Assets\\Calculator.svg"); - - public FallbackCalculatorItem() - : base(new NoOpCommand(), Resources.calculator_title) - { - Command = _copyCommand; - _copyCommand.Name = string.Empty; - Title = string.Empty; - Subtitle = Resources.calculator_placeholder_text; - Icon = _cachedIcon; - } - - public override void UpdateQuery(string query) - { - if (CalculatorListPage.ParseQuery(query, out var result)) - { - _copyCommand.Text = result; - _copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name; - Title = result; - - // we have to make the subtitle the equation, - // so that we will still string match the original query - // Otherwise, something like 1+2 will have a title of "3" and not match - Subtitle = query; - } - else - { - _copyCommand.Text = string.Empty; - _copyCommand.Name = string.Empty; - Title = string.Empty; - Subtitle = string.Empty; - } - } -} 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 new file mode 100644 index 0000000000..67d8940ed4 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/BracketHelper.cs @@ -0,0 +1,86 @@ +// 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.Linq; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public static class BracketHelper +{ + public static bool IsBracketComplete(string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + return true; + } + + var valueTuples = query + .Select(BracketTrail) + .Where(r => r != default); + + var trailTest = new Stack(); + + foreach (var (direction, type) in valueTuples) + { + switch (direction) + { + case TrailDirection.Open: + trailTest.Push(type); + break; + case TrailDirection.Close: + // Try to get item out of stack + if (!trailTest.TryPop(out var popped)) + { + return false; + } + + if (type != popped) + { + return false; + } + + continue; + default: + { + throw new ArgumentOutOfRangeException($"Can't process value (Parameter direction: {direction})"); + } + } + } + + return trailTest.Count == 0; + } + + private static (TrailDirection Direction, TrailType Type) BracketTrail(char @char) + { + switch (@char) + { + case '(': + return (TrailDirection.Open, TrailType.Round); + case ')': + return (TrailDirection.Close, TrailType.Round); + case '[': + return (TrailDirection.Open, TrailType.Bracket); + case ']': + return (TrailDirection.Close, TrailType.Bracket); + default: + return default; + } + } + + private enum TrailDirection + { + None, + Open, + Close, + } + + private enum TrailType + { + None, + Bracket, + Round, + } +} 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 new file mode 100644 index 0000000000..0d6f9536db --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateEngine.cs @@ -0,0 +1,127 @@ +// 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 Mages.Core; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public static class CalculateEngine +{ + private static readonly Engine _magesEngine = new Engine(new Configuration + { + Scope = new Dictionary + { + { "e", Math.E }, // e is not contained in the default mages engine + }, + }); + + public const int RoundingDigits = 10; + + public enum TrigMode + { + Radians, + Degrees, + Gradians, + } + + /// + /// Interpret + /// + /// Use CultureInfo.CurrentCulture if something is user facing + public static CalculateResult Interpret(SettingsManager settings, string input, CultureInfo cultureInfo, out string error) + { + error = default; + + if (!CalculateHelper.InputValid(input)) + { + return default; + } + + // check for division by zero + // We check if the string contains a slash followed by space (optional) and zero. Whereas the zero must not be followed by a dot, comma, 'b', 'o' or 'x' as these indicate a number with decimal digits or a binary/octal/hexadecimal value respectively. The zero must also not be followed by other digits. + if (new Regex("\\/\\s*0(?!(?:[,\\.0-9]|[box]0*[1-9a-f]))", RegexOptions.IgnoreCase).Match(input).Success) + { + error = Properties.Resources.calculator_division_by_zero; + return default; + } + + // mages has quirky log representation + // mage has log == ln vs log10 + input = input. + Replace("log(", "log10(", true, CultureInfo.CurrentCulture). + Replace("ln(", "log(", true, CultureInfo.CurrentCulture); + + input = CalculateHelper.FixHumanMultiplicationExpressions(input); + + // Get the user selected trigonometry unit + TrigMode trigMode = settings.TrigUnit; + + // Modify trig functions depending on angle unit setting + input = CalculateHelper.UpdateTrigFunctions(input, trigMode); + + // Expand conversions between trig units + input = CalculateHelper.ExpandTrigConversions(input, trigMode); + + var result = _magesEngine.Interpret(input); + + // This could happen for some incorrect queries, like pi(2) + if (result == null) + { + error = Properties.Resources.calculator_expression_not_complete; + return default; + } + + result = TransformResult(result); + if (result is string) + { + error = result as string; + return default; + } + + if (string.IsNullOrEmpty(result?.ToString())) + { + return default; + } + + var decimalResult = Convert.ToDecimal(result, cultureInfo); + var roundedResult = Round(decimalResult); + + return new CalculateResult() + { + Result = decimalResult, + RoundedResult = roundedResult, + }; + } + + public static decimal Round(decimal value) + { + return Math.Round(value, RoundingDigits, MidpointRounding.AwayFromZero); + } + + private static dynamic TransformResult(object result) + { + if (result.ToString() == "NaN") + { + return Properties.Resources.calculator_not_a_number; + } + + if (result is Function) + { + return Properties.Resources.calculator_expression_not_complete; + } + + if (result is double[,]) + { + // '[10,10]' is interpreted as array by mages engine + return Properties.Resources.calculator_double_array_returned; + } + + return result; + } +} 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 new file mode 100644 index 0000000000..acab3d7b96 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculateHelper.cs @@ -0,0 +1,328 @@ +// 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.Text.RegularExpressions; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public static class CalculateHelper +{ + private static readonly Regex RegValidExpressChar = new Regex( + @"^(" + + @"%|" + + @"ceil\s*\(|floor\s*\(|exp\s*\(|max\s*\(|min\s*\(|abs\s*\(|log(?:2|10)?\s*\(|ln\s*\(|sqrt\s*\(|pow\s*\(|" + + @"factorial\s*\(|sign\s*\(|round\s*\(|rand\s*\(\)|randi\s*\([^\)]|" + + @"sin\s*\(|cos\s*\(|tan\s*\(|arcsin\s*\(|arccos\s*\(|arctan\s*\(|" + + @"sinh\s*\(|cosh\s*\(|tanh\s*\(|arsinh\s*\(|arcosh\s*\(|artanh\s*\(|" + + @"rad\s*\(|deg\s*\(|grad\s*\(|" + /* trigonometry unit conversion macros */ + @"pi|" + + @"==|~=|&&|\|\||" + + @"((-?(\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); + + private const string DegToRad = "(pi / 180) * "; + private const string DegToGrad = "(10 / 9) * "; + private const string GradToRad = "(pi / 200) * "; + private const string GradToDeg = "(9 / 10) * "; + private const string RadToDeg = "(180 / pi) * "; + private const string RadToGrad = "(200 / pi) * "; + + public static bool InputValid(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + throw new ArgumentNullException(paramName: nameof(input)); + } + + if (!RegValidExpressChar.IsMatch(input)) + { + return false; + } + + if (!BracketHelper.IsBracketComplete(input)) + { + return false; + } + + // 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('%')) + { + return false; + } + + return true; + } + + public static string FixHumanMultiplicationExpressions(string input) + { + var output = CheckScientificNotation(input); + output = CheckNumberOrConstantThenParenthesisExpr(output); + output = CheckNumberOrConstantThenFunc(output); + output = CheckParenthesisExprThenFunc(output); + output = CheckParenthesisExprThenParenthesisExpr(output); + output = CheckNumberThenConstant(output); + output = CheckConstantThenConstant(output); + return output; + } + + private static string CheckScientificNotation(string input) + { + /** + * NOTE: By the time 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); + } + + /* + * num (exp) + * const (exp) + */ + private static string CheckNumberOrConstantThenParenthesisExpr(string input) + { + var output = input; + do + { + input = output; + output = Regex.Replace(input, @"(\d+|pi|e)\s*(\()", m => + { + if (m.Index > 0 && char.IsLetter(input[m.Index - 1])) + { + return m.Value; + } + + return $"{m.Groups[1].Value} * {m.Groups[2].Value}"; + }); + } + while (output != input); + + return output; + } + + /* + * num func + * const func + */ + private static string CheckNumberOrConstantThenFunc(string input) + { + var output = input; + do + { + input = output; + output = Regex.Replace(input, @"(\d+|pi|e)\s*([a-zA-Z]+[0-9]*\s*\()", m => + { + if (input[m.Index] == 'e' && input[m.Index + 1] == 'x' && input[m.Index + 2] == 'p') + { + return m.Value; + } + + if (m.Index > 0 && char.IsLetter(input[m.Index - 1])) + { + return m.Value; + } + + return $"{m.Groups[1].Value} * {m.Groups[2].Value}"; + }); + } + while (output != input); + + return output; + } + + /* + * (exp) func + * func func + */ + private static string CheckParenthesisExprThenFunc(string input) + { + var p = @"(\))\s*([a-zA-Z]+[0-9]*\s*\()"; + var r = "$1 * $2"; + return Regex.Replace(input, p, r); + } + + /* + * (exp) (exp) + * func (exp) + */ + private static string CheckParenthesisExprThenParenthesisExpr(string input) + { + var p = @"(\))\s*(\()"; + var r = "$1 * $2"; + return Regex.Replace(input, p, r); + } + + /* + * num const + */ + private static string CheckNumberThenConstant(string input) + { + var output = input; + do + { + input = output; + output = Regex.Replace(input, @"(\d+)\s*(pi|e)", m => + { + if (m.Index > 0 && char.IsLetter(input[m.Index - 1])) + { + return m.Value; + } + + return $"{m.Groups[1].Value} * {m.Groups[2].Value}"; + }); + } + while (output != input); + + return output; + } + + /* + * const const + */ + private static string CheckConstantThenConstant(string input) + { + var output = input; + do + { + input = output; + output = Regex.Replace(input, @"(pi|e)\s*(pi|e)", m => + { + if (m.Index > 0 && char.IsLetter(input[m.Index - 1])) + { + return m.Value; + } + + return $"{m.Groups[1].Value} * {m.Groups[2].Value}"; + }); + } + while (output != input); + + return output; + } + + // Gets the index of the closing bracket of a function + private static int FindClosingBracketIndex(string input, int start) + { + var bracketCount = 0; // Set count to zero + for (var i = start; i < input.Length; i++) + { + if (input[i] == '(') + { + bracketCount++; + } + else if (input[i] == ')') + { + bracketCount--; + if (bracketCount == 0) + { + return i; + } + } + } + + return -1; // Unmatched brackets + } + + private static string ModifyTrigFunction(string input, string function, string modification) + { + // Get the RegEx pattern to match, depending on whether the function is inverse or normal + var pattern = function.StartsWith("arc", StringComparison.Ordinal) ? string.Empty : @"(? +{ + public decimal? Result { get; set; } + + public decimal? RoundedResult { get; set; } + + public bool Equals(CalculateResult other) + { + return Result == other.Result && RoundedResult == other.RoundedResult; + } + + public override bool Equals(object obj) + { + return obj is CalculateResult other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(Result, RoundedResult); + } + + public static bool operator ==(CalculateResult left, CalculateResult right) + { + return left.Equals(right); + } + + public static bool operator !=(CalculateResult left, CalculateResult right) + { + return !(left == right); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculatorIcons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculatorIcons.cs new file mode 100644 index 0000000000..e3be5f2149 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/CalculatorIcons.cs @@ -0,0 +1,18 @@ +// 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.Toolkit; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public static class CalculatorIcons +{ + public static IconInfo ResultIcon => new("\uE94E"); + + public static IconInfo SaveIcon => new("\uE74E"); + + public static IconInfo ErrorIcon => new("\uE783"); + + public static IconInfo ProviderIcon => IconHelpers.FromRelativePath("Assets\\Calculator.svg"); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ErrorHandler.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ErrorHandler.cs new file mode 100644 index 0000000000..b3948dc854 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ErrorHandler.cs @@ -0,0 +1,53 @@ +// 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 ManagedCommon; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +internal static class ErrorHandler +{ + /// + /// Method to handles errors while calculating + /// + /// Bool to indicate if it is a fallback query. + /// User input as string including the action keyword. + /// Error message if applicable. + /// Exception if applicable. + /// List of results to show. Either an error message or an empty list. + /// Thrown if and are both filled with their default values. + internal static ListItem OnError(bool isFallbackSearch, string queryInput, string errorMessage, Exception exception = default) + { + string userMessage; + + if (errorMessage != default) + { + Logger.LogError($"Failed to calculate <{queryInput}>: {errorMessage}"); + userMessage = errorMessage; + } + else if (exception != default) + { + Logger.LogError($"Exception when query for <{queryInput}>", exception); + userMessage = exception.Message; + } + else + { + throw new ArgumentException("The arguments error and exception have default values. One of them has to be filled with valid error data (error message/exception)!"); + } + + return isFallbackSearch ? null : CreateErrorResult(userMessage); + } + + private static ListItem CreateErrorResult(string errorMessage) + { + return new ListItem(new NoOpCommand()) + { + Title = Properties.Resources.calculator_calculation_failed_title, + Subtitle = errorMessage, + Icon = CalculatorIcons.ErrorIcon, + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs new file mode 100644 index 0000000000..8de77ebdae --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/NumberTranslator.cs @@ -0,0 +1,144 @@ +// 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.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +/// +/// Tries to convert all numbers in a text from one culture format to another. +/// +public class NumberTranslator +{ + private readonly CultureInfo sourceCulture; + private readonly CultureInfo targetCulture; + private readonly Regex splitRegexForSource; + private readonly Regex splitRegexForTarget; + + private NumberTranslator(CultureInfo sourceCulture, CultureInfo targetCulture) + { + this.sourceCulture = sourceCulture; + this.targetCulture = targetCulture; + + splitRegexForSource = GetSplitRegex(this.sourceCulture); + splitRegexForTarget = GetSplitRegex(this.targetCulture); + } + + /// + /// Create a new . + /// + /// source culture + /// target culture + /// Number translator for target culture + public static NumberTranslator Create(CultureInfo sourceCulture, CultureInfo targetCulture) + { + ArgumentNullException.ThrowIfNull(sourceCulture); + + ArgumentNullException.ThrowIfNull(targetCulture); + + return new NumberTranslator(sourceCulture, targetCulture); + } + + /// + /// Translate from source to target culture. + /// + /// input string to translate + /// translated string + public string Translate(string input) + { + return Translate(input, sourceCulture, targetCulture, splitRegexForSource); + } + + /// + /// Translate from target to source culture. + /// + /// input string to translate back to source culture + /// source culture string + public string TranslateBack(string input) + { + return Translate(input, targetCulture, sourceCulture, splitRegexForTarget); + } + + private static string Translate(string input, CultureInfo cultureFrom, CultureInfo cultureTo, Regex splitRegex) + { + var outputBuilder = new StringBuilder(); + var hexRegex = new Regex(@"(?:(0x[\da-fA-F]+))"); + + var hexTokens = hexRegex.Split(input); + + foreach (var hexToken in hexTokens) + { + if (hexToken.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)) + { + // Mages engine has issues processing large hex number (larger than 7 hex digits + 0x prefix = 9 characters). So we convert it to decimal and pass it to the engine. + if (hexToken.Length > 9) + { + try + { + var num = Convert.ToInt64(hexToken, 16); + var numStr = num.ToString(cultureFrom); + outputBuilder.Append(numStr); + } + catch (Exception) + { + outputBuilder.Append(hexToken); + } + } + else + { + outputBuilder.Append(hexToken); + } + + continue; + } + + var tokens = splitRegex.Split(hexToken); + foreach (var token in tokens) + { + var leadingZeroCount = 0; + + // Count leading zero characters. + foreach (var c in token) + { + if (c != '0') + { + break; + } + + leadingZeroCount++; + } + + // number is all zero characters. no need to add zero characters at the end. + if (token.Length == leadingZeroCount) + { + leadingZeroCount = 0; + } + + decimal number; + + outputBuilder.Append( + decimal.TryParse(token, NumberStyles.Number, cultureFrom, out number) + ? (new string('0', leadingZeroCount) + number.ToString(cultureTo)) + : token.Replace(cultureFrom.TextInfo.ListSeparator, cultureTo.TextInfo.ListSeparator)); + } + } + + return outputBuilder.ToString(); + } + + private static Regex GetSplitRegex(CultureInfo culture) + { + var splitPattern = $"((?:\\d|{Regex.Escape(culture.NumberFormat.NumberDecimalSeparator)}"; + if (!string.IsNullOrEmpty(culture.NumberFormat.NumberGroupSeparator)) + { + splitPattern += $"|{Regex.Escape(culture.NumberFormat.NumberGroupSeparator)}"; + } + + splitPattern += ")+)"; + return new Regex(splitPattern); + } +} 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 new file mode 100644 index 0000000000..6b1e62ae03 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/QueryHelper.cs @@ -0,0 +1,77 @@ +// 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.Globalization; +using System.Text; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public static partial class QueryHelper +{ + public static ListItem Query(string query, SettingsManager settings, bool isFallbackSearch, TypedEventHandler handleSave = null) + { + ArgumentNullException.ThrowIfNull(query); + if (!isFallbackSearch) + { + ArgumentNullException.ThrowIfNull(handleSave); + } + + CultureInfo inputCulture = settings.InputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture; + CultureInfo outputCulture = settings.OutputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture; + + // Happens if the user has only typed the action key so far + if (string.IsNullOrEmpty(query)) + { + return null; + } + + NumberTranslator translator = NumberTranslator.Create(inputCulture, new CultureInfo("en-US")); + var input = translator.Translate(query.Normalize(NormalizationForm.FormKC)); + + if (!CalculateHelper.InputValid(input)) + { + return null; + } + + try + { + // Using CurrentUICulture since this is user facing + var result = CalculateEngine.Interpret(settings, input, outputCulture, out var errorMessage); + + // This could happen for some incorrect queries, like pi(2) + if (result.Equals(default(CalculateResult))) + { + // If errorMessage is not default then do error handling + return errorMessage == default ? null : ErrorHandler.OnError(isFallbackSearch, query, errorMessage); + } + + if (isFallbackSearch) + { + // Fallback search + return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, query); + } + + return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, query, handleSave); + } + catch (Mages.Core.ParseException) + { + // Invalid input + return ErrorHandler.OnError(isFallbackSearch, query, Properties.Resources.calculator_expression_not_complete); + } + catch (OverflowException) + { + // Result to big to convert to decimal + return ErrorHandler.OnError(isFallbackSearch, query, Properties.Resources.calculator_not_covert_to_decimal); + } + catch (Exception e) + { + // Any other crash occurred + // We want to keep the process alive if any the mages library throws any exceptions. + return ErrorHandler.OnError(isFallbackSearch, query, default, e); + } + } +} 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 new file mode 100644 index 0000000000..0fab0a8245 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/ResultHelper.cs @@ -0,0 +1,103 @@ +// 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 ManagedCommon; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public static class ResultHelper +{ + public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query, TypedEventHandler handleSave) + { + // Return null when the expression is not a valid calculator query. + if (roundedResult == null) + { + return null; + } + + var result = roundedResult?.ToString(outputCulture); + + // Create a SaveCommand and subscribe to the SaveRequested event + // This can append the result to the history list. + var saveCommand = new SaveCommand(result); + saveCommand.SaveRequested += handleSave; + + var copyCommandItem = CreateResult(roundedResult, inputCulture, outputCulture, query); + + return new ListItem(saveCommand) + { + // Using CurrentCulture since this is user facing + Icon = CalculatorIcons.ResultIcon, + Title = result, + Subtitle = query, + TextToSuggest = result, + MoreCommands = [ + new CommandContextItem(copyCommandItem.Command) + { + Icon = copyCommandItem.Icon, + Title = copyCommandItem.Title, + Subtitle = copyCommandItem.Subtitle, + }, + ..copyCommandItem.MoreCommands, + ], + }; + } + + public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query) + { + // Return null when the expression is not a valid calculator query. + if (roundedResult == null) + { + return null; + } + + var decimalResult = roundedResult?.ToString(outputCulture); + + List context = []; + + if (decimal.IsInteger((decimal)roundedResult)) + { + var i = decimal.ToInt64((decimal)roundedResult); + try + { + var hexResult = "0x" + i.ToString("X", outputCulture); + context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex }) + { + Title = hexResult, + }); + } + catch (Exception ex) + { + Logger.LogError("Error parsing hex format", ex); + } + + try + { + var binaryResult = "0b" + i.ToString("B", outputCulture); + context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary }) + { + Title = binaryResult, + }); + } + catch (Exception ex) + { + Logger.LogError("Error parsing binary format", ex); + } + } + + return new ListItem(new CopyTextCommand(decimalResult)) + { + // Using CurrentCulture since this is user facing + Title = decimalResult, + Subtitle = query, + TextToSuggest = decimalResult, + MoreCommands = context.ToArray(), + }; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SaveCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SaveCommand.cs new file mode 100644 index 0000000000..850f8511e3 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SaveCommand.cs @@ -0,0 +1,31 @@ +// 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.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public sealed partial class SaveCommand : InvokableCommand +{ + private readonly string _result; + + public event TypedEventHandler SaveRequested; + + public SaveCommand(string result) + { + Name = Resources.calculator_save_command_name; + Icon = CalculatorIcons.SaveIcon; + _result = result; + } + + public override ICommandResult Invoke() + { + SaveRequested?.Invoke(this, this); + ClipboardHelper.SetText(_result); + return CommandResult.KeepOpen(); + } +} 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 new file mode 100644 index 0000000000..e106123f5e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Helper/SettingsManager.cs @@ -0,0 +1,98 @@ +// 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.Generic; +using System.IO; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Calc.Helper; + +public class SettingsManager : JsonSettingsManager +{ + private static readonly string _namespace = "calculator"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private static readonly List _trigUnitChoices = new() + { + new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_radians, "0"), + new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_degrees, "1"), + new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_gradians, "2"), + }; + + private readonly ChoiceSetSetting _trigUnit = new( + Namespaced(nameof(TrigUnit)), + Properties.Resources.calculator_settings_trig_unit_mode, + Properties.Resources.calculator_settings_trig_unit_mode_description, + _trigUnitChoices); + + private readonly ToggleSetting _inputUseEnNumberFormat = new( + Namespaced(nameof(InputUseEnglishFormat)), + Properties.Resources.calculator_settings_in_en_format, + Properties.Resources.calculator_settings_in_en_format_description, + false); + + private readonly ToggleSetting _outputUseEnNumberFormat = new( + Namespaced(nameof(OutputUseEnglishFormat)), + Properties.Resources.calculator_settings_out_en_format, + Properties.Resources.calculator_settings_out_en_format_description, + false); + + public CalculateEngine.TrigMode TrigUnit + { + get + { + if (_trigUnit.Value == null || string.IsNullOrEmpty(_trigUnit.Value)) + { + return CalculateEngine.TrigMode.Radians; + } + + var success = int.TryParse(_trigUnit.Value, out var result); + + if (!success) + { + return CalculateEngine.TrigMode.Radians; + } + + switch (result) + { + case 0: + return CalculateEngine.TrigMode.Radians; + case 1: + return CalculateEngine.TrigMode.Degrees; + case 2: + return CalculateEngine.TrigMode.Gradians; + default: + return CalculateEngine.TrigMode.Radians; + } + } + } + + public bool InputUseEnglishFormat => _inputUseEnNumberFormat.Value; + + public bool OutputUseEnglishFormat => _outputUseEnNumberFormat.Value; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + // now, the state is just next to the exe + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + Settings.Add(_trigUnit); + Settings.Add(_inputUseEnNumberFormat); + Settings.Add(_outputUseEnNumberFormat); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj index 68a38c1ca2..85f06768ce 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj @@ -9,9 +9,14 @@ Microsoft.CmdPal.Ext.Calc.pri + + + + + Resources.resx 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 new file mode 100644 index 0000000000..24f26646c5 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/CalculatorListPage.cs @@ -0,0 +1,127 @@ +// 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.Generic; +using System.Threading; +using Microsoft.CmdPal.Ext.Calc.Helper; +using Microsoft.CmdPal.Ext.Calc.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Calc.Pages; + +// The calculator page is a dynamic list page +// * The first command is where we display the results. Title=result, Subtitle=query +// - The default command is `SaveCommand`. +// - When you save, insert into list at spot 1 +// - change SearchText to the result +// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard +// * The rest of the items are previously saved results +// - Command is a CopyCommand +// - Each item also sets the TextToSuggest to the result +public sealed partial class CalculatorListPage : DynamicListPage +{ + private readonly Lock _resultsLock = new(); + private readonly SettingsManager _settingsManager; + private readonly List _items = []; + 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; + + public CalculatorListPage(SettingsManager settings) + { + _settingsManager = settings; + Icon = CalculatorIcons.ProviderIcon; + Name = Resources.calculator_title; + PlaceholderText = Resources.calculator_placeholder_text; + Id = "com.microsoft.cmdpal.calculator"; + + _emptyItem = new ListItem(new NoOpCommand()) + { + Title = Resources.calculator_placeholder_text, + Icon = CalculatorIcons.ResultIcon, + }; + EmptyContent = new CommandItem(new NoOpCommand()) + { + Icon = CalculatorIcons.ProviderIcon, + Title = Resources.calculator_placeholder_text, + }; + + UpdateSearchText(string.Empty, string.Empty); + } + + public override void UpdateSearchText(string oldSearch, string newSearch) + { + if (oldSearch == newSearch) + { + return; + } + + if (!string.IsNullOrEmpty(skipQuerySearchText) && newSearch == skipQuerySearchText) + { + // only skip once. + skipQuerySearchText = string.Empty; + return; + } + + skipQuerySearchText = string.Empty; + + _emptyItem.Subtitle = newSearch; + + var result = QueryHelper.Query(newSearch, _settingsManager, false, HandleSave); + UpdateResult(result); + } + + private void UpdateResult(ListItem result) + { + lock (_resultsLock) + { + this._items.Clear(); + + if (result != null) + { + this._items.Add(result); + } + else + { + _items.Add(_emptyItem); + } + + this._items.AddRange(history); + } + + RaiseItemsChanged(this._items.Count); + } + + private void HandleSave(object sender, object args) + { + var lastResult = _items[0].Title; + if (!string.IsNullOrEmpty(lastResult)) + { + var li = new ListItem(new CopyTextCommand(lastResult)) + { + Title = _items[0].Title, + Subtitle = _items[0].Subtitle, + TextToSuggest = lastResult, + }; + + history.Insert(0, li); + _items.Insert(1, li); + + // Why we need to clean the query record? Removed, but if necessary, please move it back. + // _items[0].Subtitle = string.Empty; + + // this change will call the UpdateSearchText again. + // We need to avoid it. + skipQuerySearchText = lastResult; + SearchText = lastResult; + this.RaiseItemsChanged(this._items.Count); + } + } + + public override IListItem[] GetItems() => _items.ToArray(); +} 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 new file mode 100644 index 0000000000..b309f4ed3a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Pages/FallbackCalculatorItem.cs @@ -0,0 +1,52 @@ +// 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.CmdPal.Ext.Calc.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Calc.Pages; + +public sealed partial class FallbackCalculatorItem : FallbackCommandItem +{ + private readonly CopyTextCommand _copyCommand = new(string.Empty); + private readonly SettingsManager _settings; + + public FallbackCalculatorItem(SettingsManager settings) + : base(new NoOpCommand(), Resources.calculator_title) + { + Command = _copyCommand; + _copyCommand.Name = string.Empty; + Title = string.Empty; + Subtitle = Resources.calculator_placeholder_text; + Icon = CalculatorIcons.ProviderIcon; + _settings = settings; + } + + public override void UpdateQuery(string query) + { + var result = QueryHelper.Query(query, _settings, true, null); + + if (result == null) + { + _copyCommand.Text = string.Empty; + _copyCommand.Name = string.Empty; + Title = string.Empty; + Subtitle = string.Empty; + MoreCommands = []; + return; + } + + _copyCommand.Text = result.Title; + _copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name; + Title = result.Title; + + // we have to make the subtitle the equation, + // so that we will still string match the original query + // Otherwise, something like 1+2 will have a title of "3" and not match + Subtitle = query; + + MoreCommands = result.MoreCommands; + } +} 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 72fdb55d3b..8cd385be32 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 @@ -60,6 +60,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties { } } + /// + /// Looks up a localized string similar to Failed to calculate the input. + /// + public static string calculator_calculation_failed_title { + get { + return ResourceManager.GetString("calculator_calculation_failed_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy binary. + /// + public static string calculator_copy_binary { + get { + return ResourceManager.GetString("calculator_copy_binary", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copy. /// @@ -69,6 +87,15 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties { } } + /// + /// Looks up a localized string similar to Copy hexadecimal. + /// + public static string calculator_copy_hex { + get { + return ResourceManager.GetString("calculator_copy_hex", resourceCulture); + } + } + /// /// Looks up a localized string similar to Calculator. /// @@ -78,6 +105,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties { } } + /// + /// Looks up a localized string similar to Expression contains division by zero. + /// + public static string calculator_division_by_zero { + get { + return ResourceManager.GetString("calculator_division_by_zero", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unsupported use of square brackets. + /// + public static string calculator_double_array_returned { + get { + return ResourceManager.GetString("calculator_double_array_returned", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error: {0}. /// @@ -87,6 +132,33 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties { } } + /// + /// Looks up a localized string similar to Expression wrong or incomplete. + /// + public static string calculator_expression_not_complete { + get { + return ResourceManager.GetString("calculator_expression_not_complete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Calculation result is not a valid number (NaN). + /// + public static string calculator_not_a_number { + get { + return ResourceManager.GetString("calculator_not_a_number", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Result value was either too large or too small for a decimal number. + /// + public static string calculator_not_covert_to_decimal { + get { + return ResourceManager.GetString("calculator_not_covert_to_decimal", resourceCulture); + } + } + /// /// Looks up a localized string similar to Type an equation.... /// @@ -105,6 +177,105 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties { } } + /// + /// Looks up a localized string similar to Use English (United States) number format for input. + /// + public static string calculator_settings_in_en_format { + get { + return ResourceManager.GetString("calculator_settings_in_en_format", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ignores your system setting and expects numbers in the format '{0}'.. + /// + public static string calculator_settings_in_en_format_description { + get { + return ResourceManager.GetString("calculator_settings_in_en_format_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use English (United States) number format for output. + /// + public static string calculator_settings_out_en_format { + get { + return ResourceManager.GetString("calculator_settings_out_en_format", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ignores your system setting and returns numbers in the format '{0}'.. + /// + public static string calculator_settings_out_en_format_description { + get { + return ResourceManager.GetString("calculator_settings_out_en_format_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace input if query ends with '='. + /// + public static string calculator_settings_replace_input { + get { + return ResourceManager.GetString("calculator_settings_replace_input", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When using direct activation, appending '=' to the expression will replace the input with the calculated result (e.g. '=5*3-2=' will change the query to '=13').. + /// + public static string calculator_settings_replace_input_description { + get { + return ResourceManager.GetString("calculator_settings_replace_input_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Degrees. + /// + public static string calculator_settings_trig_unit_degrees { + get { + return ResourceManager.GetString("calculator_settings_trig_unit_degrees", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gradians. + /// + public static string calculator_settings_trig_unit_gradians { + get { + return ResourceManager.GetString("calculator_settings_trig_unit_gradians", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Trigonometry Unit. + /// + public static string calculator_settings_trig_unit_mode { + get { + return ResourceManager.GetString("calculator_settings_trig_unit_mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Specifies the angle unit to use for trigonometry operations. + /// + public static string calculator_settings_trig_unit_mode_description { + get { + return ResourceManager.GetString("calculator_settings_trig_unit_mode_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Radians. + /// + public static string calculator_settings_trig_unit_radians { + get { + return ResourceManager.GetString("calculator_settings_trig_unit_radians", resourceCulture); + } + } + /// /// Looks up a localized string similar to Calculator. /// 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 580f704433..3c50d3a1c5 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 @@ -140,4 +140,63 @@ Copy + + Failed to calculate the input + + + Expression contains division by zero + + + Expression wrong or incomplete + + + Calculation result is not a valid number (NaN) + + + Unsupported use of square brackets + + + Gradians + + + Degrees + + + Radians + + + Trigonometry Unit + + + Specifies the angle unit to use for trigonometry operations + + + Use English (United States) number format for output + + + Ignores your system setting and returns numbers in the format '{0}'. + {0} is a placeholder and will be replaced in code. + + + Use English (United States) number format for input + + + Ignores your system setting and expects numbers in the format '{0}'. + {0} is a placeholder and will be replaced in code. + + + Replace input if query ends with '=' + + + When using direct activation, appending '=' to the expression will replace the input with the calculated result (e.g. '=5*3-2=' will change the query to '=13'). + + + Result value was either too large or too small for a decimal number + + + Copy hexadecimal + + + Copy binary + \ No newline at end of file