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