diff --git a/src/SharpFM.Model/Scripting/Calc/FmCalcCatalog.cs b/src/SharpFM.Model/Scripting/Calc/FmCalcCatalog.cs
new file mode 100644
index 0000000..dfd970c
--- /dev/null
+++ b/src/SharpFM.Model/Scripting/Calc/FmCalcCatalog.cs
@@ -0,0 +1,554 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace SharpFM.Model.Scripting.Calc;
+
+///
+/// Single source of truth for FileMaker calculation built-ins. The TextMate
+/// grammar (fmcalc.tmLanguage.json) is regenerated from this catalog
+/// by SharpFM.Tools.GenerateGrammar; the completion provider reads
+/// from the same lists at runtime. Edit here, regenerate grammar, both stay
+/// in sync.
+///
+/// Categorisation follows FileMaker's calculation dialog. Function lists
+/// are based on the public function reference at help.claris.com and may
+/// trail very recent FM releases; bumps are a one-line append.
+///
+public static class FmCalcCatalog
+{
+ // Enum-value lists referenced from BuildFunctions(). Declared before
+ // Functions so they're initialised first — C# static field init runs
+ // in textual order, and BuildFunctions() reads these.
+ private static readonly IReadOnlyList GetSelectorKeywords = BuildGetSelectorKeywords();
+ private static readonly IReadOnlyList TextStyles = BuildTextStyles();
+ private static readonly IReadOnlyList HashAlgorithms = BuildHashAlgorithms();
+ private static readonly IReadOnlyList JsonElementTypes = BuildJsonElementTypes();
+
+ public static IReadOnlyList Functions { get; } = BuildFunctions();
+ public static IReadOnlyList ControlForms { get; } = BuildControlForms();
+ public static IReadOnlyList Constants { get; } = new ReadOnlyCollection(
+ new[] { "True", "False", "Pi" });
+
+ /// Word operators that must scope as keyword.operator.word, not function names.
+ public static IReadOnlyList WordOperators { get; } = new ReadOnlyCollection(
+ new[] { "and", "or", "not", "xor" });
+
+ private static IReadOnlyList BuildFunctions()
+ {
+ var list = new List();
+
+ void Add(string n, FunctionCategory c, string sig, string desc,
+ params FmCalcFunctionParam[] ps)
+ {
+ // When the caller doesn't pass explicit Params, derive them
+ // from the signature string. Lets every function get tab-stop
+ // completion without hand-authoring a param list per entry.
+ IReadOnlyList paramList = ps.Length > 0
+ ? ps
+ : FmCalcSignatureParser.ParseParams(sig);
+ list.Add(new FmCalcFunction(n, c, sig, desc, paramList));
+ }
+
+ // Text
+ Add("Char", FunctionCategory.Text, "Char(number)", "Returns the character for a Unicode code point.");
+ Add("Code", FunctionCategory.Text, "Code(text)", "Returns the Unicode code points for the characters in text.");
+ Add("Exact", FunctionCategory.Text, "Exact(originalText; comparisonText)", "Returns true if the two values match exactly.");
+ Add("Filter", FunctionCategory.Text, "Filter(textToFilter; filterText)", "Returns characters from textToFilter that also appear in filterText.");
+ Add("FilterValues", FunctionCategory.Text, "FilterValues(textToFilter; filterValues)", "Returns values from textToFilter that match a value list.");
+ Add("GetAsCSS", FunctionCategory.Text, "GetAsCSS(text)", "Returns text marked up with CSS style attributes.");
+ Add("GetAsDate", FunctionCategory.Text, "GetAsDate(text)", "Converts text to a date.");
+ Add("GetAsNumber", FunctionCategory.Text, "GetAsNumber(text)", "Returns only the numeric characters from text.");
+ Add("GetAsSVG", FunctionCategory.Text, "GetAsSVG(text)", "Returns text marked up as SVG.");
+ Add("GetAsText", FunctionCategory.Text, "GetAsText(data)", "Converts a value to text.");
+ Add("GetAsTime", FunctionCategory.Text, "GetAsTime(text)", "Converts text to a time.");
+ Add("GetAsTimestamp", FunctionCategory.Text, "GetAsTimestamp(text)", "Converts text to a timestamp.");
+ Add("GetAsURLEncoded", FunctionCategory.Text, "GetAsURLEncoded(text)", "Returns the URL-encoded form of text.");
+ Add("Hiragana", FunctionCategory.Text, "Hiragana(text)", "Converts katakana to hiragana.");
+ Add("KanaHankaku", FunctionCategory.Text, "KanaHankaku(text)", "Converts full-width katakana to half-width.");
+ Add("KanaZenkaku", FunctionCategory.Text, "KanaZenkaku(text)", "Converts half-width katakana to full-width.");
+ Add("KanjiNumeral", FunctionCategory.Text, "KanjiNumeral(text)", "Converts Arabic numerals to kanji.");
+ Add("KatakanaToRoman", FunctionCategory.Text, "KatakanaToRoman(text)", "Converts katakana to roman characters.");
+ Add("Left", FunctionCategory.Text, "Left(text; numberOfCharacters)", "Returns the leftmost characters of text.");
+ Add("LeftValues", FunctionCategory.Text, "LeftValues(text; numberOfValues)", "Returns the leftmost values from a return-delimited list.");
+ Add("LeftWords", FunctionCategory.Text, "LeftWords(text; numberOfWords)", "Returns the leftmost words from text.");
+ Add("Length", FunctionCategory.Text, "Length(text)", "Returns the number of characters in text.");
+ Add("Lower", FunctionCategory.Text, "Lower(text)", "Returns text in lowercase.");
+ Add("Middle", FunctionCategory.Text, "Middle(text; start; numberOfCharacters)", "Returns characters from text starting at start.");
+ Add("MiddleValues", FunctionCategory.Text, "MiddleValues(text; startingValue; numberOfValues)", "Returns values from a list starting at startingValue.");
+ Add("MiddleWords", FunctionCategory.Text, "MiddleWords(text; startingWord; numberOfWords)", "Returns words from text starting at startingWord.");
+ Add("NumToJText", FunctionCategory.Text, "NumToJText(number; separator; characterType)", "Converts Arabic numerals to Japanese text.");
+ Add("PatternCount", FunctionCategory.Text, "PatternCount(text; searchString)", "Returns how many times searchString appears in text.");
+ Add("Position", FunctionCategory.Text, "Position(text; searchString; start; occurrence)", "Returns the starting position of searchString in text.");
+ Add("Proper", FunctionCategory.Text, "Proper(text)", "Returns text with the first letter of each word capitalized.");
+ Add("Quote", FunctionCategory.Text, "Quote(text)", "Returns text wrapped in quotes with internal quotes escaped.");
+ Add("Replace", FunctionCategory.Text, "Replace(text; start; numberOfCharacters; replacementText)", "Replaces a range of characters in text.");
+ Add("Right", FunctionCategory.Text, "Right(text; numberOfCharacters)", "Returns the rightmost characters of text.");
+ Add("RightValues", FunctionCategory.Text, "RightValues(text; numberOfValues)", "Returns the rightmost values from a return-delimited list.");
+ Add("RightWords", FunctionCategory.Text, "RightWords(text; numberOfWords)", "Returns the rightmost words from text.");
+ Add("RomanHankaku", FunctionCategory.Text, "RomanHankaku(text)", "Converts full-width roman to half-width.");
+ Add("RomanZenkaku", FunctionCategory.Text, "RomanZenkaku(text)", "Converts half-width roman to full-width.");
+ Add("SerialIncrement", FunctionCategory.Text, "SerialIncrement(text; incrementBy)", "Returns text with its trailing digits incremented.");
+ Add("SortValues", FunctionCategory.Text, "SortValues(values; dataType; locale)", "Returns a sorted list of values.");
+ Add("Substitute", FunctionCategory.Text, "Substitute(text; searchString; replaceString)", "Replaces every occurrence of searchString in text.");
+ Add("Trim", FunctionCategory.Text, "Trim(text)", "Removes leading and trailing spaces from text.");
+ Add("TrimAll", FunctionCategory.Text, "TrimAll(text; trimSpaces; trimType)", "Removes spaces with finer control than Trim.");
+ Add("UniqueValues", FunctionCategory.Text, "UniqueValues(values; fieldType; locale)", "Returns the unique values from a list.");
+ Add("Upper", FunctionCategory.Text, "Upper(text)", "Returns text in uppercase.");
+ Add("ValueCount", FunctionCategory.Text, "ValueCount(text)", "Returns the number of values in a return-delimited list.");
+ Add("VerifyID", FunctionCategory.Text, "VerifyID(id)", "Returns whether an ID has a valid checksum.");
+ Add("WordCount", FunctionCategory.Text, "WordCount(text)", "Returns the number of words in text.");
+
+ // Text formatting
+ Add("RGB", FunctionCategory.TextFormatting, "RGB(red; green; blue)", "Returns a numeric color value.");
+ Add("TextColor", FunctionCategory.TextFormatting, "TextColor(text; rgb)", "Returns text with the given color applied.");
+ Add("TextColorRemove", FunctionCategory.TextFormatting, "TextColorRemove(text; rgb)", "Removes color from text.");
+ Add("TextFont", FunctionCategory.TextFormatting, "TextFont(text; fontName; fontScript)", "Returns text with the given font applied.");
+ Add("TextFontRemove", FunctionCategory.TextFormatting, "TextFontRemove(text; fontName; fontScript)", "Removes font from text.");
+ Add("TextFormatRemove", FunctionCategory.TextFormatting, "TextFormatRemove(text)", "Removes all formatting from text.");
+ Add("TextSize", FunctionCategory.TextFormatting, "TextSize(text; size)", "Returns text at the given size.");
+ Add("TextSizeRemove", FunctionCategory.TextFormatting, "TextSizeRemove(text; size)", "Removes size from text.");
+ Add("TextStyleAdd", FunctionCategory.TextFormatting, "TextStyleAdd(text; style)",
+ "Returns text with the given style applied.",
+ new FmCalcFunctionParam("text"),
+ new FmCalcFunctionParam("style", "Style flag", TextStyles));
+ Add("TextStyleRemove", FunctionCategory.TextFormatting, "TextStyleRemove(text; style)",
+ "Removes style from text.",
+ new FmCalcFunctionParam("text"),
+ new FmCalcFunctionParam("style", "Style flag", TextStyles));
+
+ // Number
+ Add("Abs", FunctionCategory.Number, "Abs(number)", "Returns the absolute value of number.");
+ Add("Ceiling", FunctionCategory.Number, "Ceiling(number)", "Rounds number up to the next integer.");
+ Add("Combination", FunctionCategory.Number, "Combination(setSize; numberOfChoices)", "Returns the number of combinations.");
+ Add("Div", FunctionCategory.Number, "Div(number; divisor)", "Returns the integer part of number divided by divisor.");
+ Add("Exp", FunctionCategory.Number, "Exp(number)", "Returns e raised to the power of number.");
+ Add("Factorial", FunctionCategory.Number, "Factorial(number; numberOfFactors)", "Returns the factorial of number.");
+ Add("Floor", FunctionCategory.Number, "Floor(number)", "Rounds number down to the previous integer.");
+ Add("Int", FunctionCategory.Number, "Int(number)", "Returns the integer part of number.");
+ Add("Lg", FunctionCategory.Number, "Lg(number)", "Returns the base-2 logarithm of number.");
+ Add("Ln", FunctionCategory.Number, "Ln(number)", "Returns the natural logarithm of number.");
+ Add("Log", FunctionCategory.Number, "Log(number)", "Returns the base-10 logarithm of number.");
+ Add("Mod", FunctionCategory.Number, "Mod(number; divisor)", "Returns the remainder of number divided by divisor.");
+ Add("Random", FunctionCategory.Number, "Random", "Returns a random number between 0 and 1.");
+ Add("Round", FunctionCategory.Number, "Round(number; precision)", "Rounds number to precision decimal places.");
+ Add("SetPrecision", FunctionCategory.Number, "SetPrecision(expression; precision)", "Returns expression evaluated with extended precision.");
+ Add("Sign", FunctionCategory.Number, "Sign(number)", "Returns -1, 0, or 1 depending on the sign of number.");
+ Add("Sqrt", FunctionCategory.Number, "Sqrt(number)", "Returns the square root of number.");
+ Add("Truncate", FunctionCategory.Number, "Truncate(number; precision)", "Truncates number to precision decimal places.");
+
+ // Date
+ Add("Date", FunctionCategory.Date, "Date(month; day; year)", "Returns a date value.");
+ Add("Day", FunctionCategory.Date, "Day(date)", "Returns the day of the month from date.");
+ Add("DayName", FunctionCategory.Date, "DayName(date)", "Returns the weekday name for date.");
+ Add("DayNameJ", FunctionCategory.Date, "DayNameJ(date)", "Returns the Japanese weekday name for date.");
+ Add("DayOfWeek", FunctionCategory.Date, "DayOfWeek(date)", "Returns the day of the week (1-7) for date.");
+ Add("DayOfYear", FunctionCategory.Date, "DayOfYear(date)", "Returns the day of the year (1-366) for date.");
+ Add("Month", FunctionCategory.Date, "Month(date)", "Returns the month number for date.");
+ Add("MonthName", FunctionCategory.Date, "MonthName(date)", "Returns the month name for date.");
+ Add("MonthNameJ", FunctionCategory.Date, "MonthNameJ(date)", "Returns the Japanese month name for date.");
+ Add("WeekOfYear", FunctionCategory.Date, "WeekOfYear(date)", "Returns the week number of the year for date.");
+ Add("WeekOfYearFiscal", FunctionCategory.Date, "WeekOfYearFiscal(date; startingDay)", "Returns the fiscal week of year for date.");
+ Add("Year", FunctionCategory.Date, "Year(date)", "Returns the year for date.");
+ Add("YearName", FunctionCategory.Date, "YearName(date; format)", "Returns the Japanese era year name for date.");
+
+ // Time
+ Add("Hour", FunctionCategory.Time, "Hour(time)", "Returns the hour for time.");
+ Add("Minute", FunctionCategory.Time, "Minute(time)", "Returns the minute for time.");
+ Add("Seconds", FunctionCategory.Time, "Seconds(time)", "Returns the seconds for time.");
+ Add("Time", FunctionCategory.Time, "Time(hours; minutes; seconds)", "Returns a time value.");
+ Add("Timestamp", FunctionCategory.Time, "Timestamp(date; time)", "Returns a timestamp value.");
+
+ // Aggregate
+ Add("Average", FunctionCategory.Aggregate, "Average(field {; field...})", "Returns the average of non-blank values.");
+ Add("Count", FunctionCategory.Aggregate, "Count(field {; field...})", "Returns the count of non-blank values.");
+ Add("List", FunctionCategory.Aggregate, "List(field {; field...})", "Returns a return-delimited list of non-blank values.");
+ Add("Max", FunctionCategory.Aggregate, "Max(field {; field...})", "Returns the maximum of non-blank values.");
+ Add("Min", FunctionCategory.Aggregate, "Min(field {; field...})", "Returns the minimum of non-blank values.");
+ Add("StDev", FunctionCategory.Aggregate, "StDev(field {; field...})", "Returns the sample standard deviation.");
+ Add("StDevP", FunctionCategory.Aggregate, "StDevP(field {; field...})", "Returns the population standard deviation.");
+ Add("Sum", FunctionCategory.Aggregate, "Sum(field {; field...})", "Returns the sum of non-blank values.");
+ Add("Variance", FunctionCategory.Aggregate, "Variance(field {; field...})", "Returns the sample variance.");
+ Add("VarianceP", FunctionCategory.Aggregate, "VarianceP(field {; field...})", "Returns the population variance.");
+
+ // Summary / repeating
+ Add("GetSummary", FunctionCategory.Summary, "GetSummary(summaryField; breakField)", "Returns a summary value broken on breakField.");
+ Add("GetNthRecord", FunctionCategory.Summary, "GetNthRecord(field; recordNumber)", "Returns field's value from the Nth record.");
+ Add("Last", FunctionCategory.Summary, "Last(field)", "Returns the last non-blank value in a related set.");
+ Add("GetRepetition", FunctionCategory.Summary, "GetRepetition(repeatingField; number)", "Returns the Nth repetition of a repeating field.");
+ Add("Extend", FunctionCategory.Summary, "Extend(nonRepeatingField)", "Allows a non-repeating field to apply to all repetitions.");
+
+ // Financial
+ Add("FV", FunctionCategory.Financial, "FV(payment; interestRate; periods)", "Returns future value.");
+ Add("NPV", FunctionCategory.Financial, "NPV(payment; interestRate)", "Returns net present value.");
+ Add("PMT", FunctionCategory.Financial, "PMT(principal; interestRate; term)", "Returns payment amount.");
+ Add("PV", FunctionCategory.Financial, "PV(payment; interestRate; periods)", "Returns present value.");
+
+ // Trigonometric
+ Add("Acos", FunctionCategory.Trigonometric, "Acos(number)", "Returns the arc cosine of number.");
+ Add("Asin", FunctionCategory.Trigonometric, "Asin(number)", "Returns the arc sine of number.");
+ Add("Atan", FunctionCategory.Trigonometric, "Atan(number)", "Returns the arc tangent of number.");
+ Add("Cos", FunctionCategory.Trigonometric, "Cos(angleInRadians)", "Returns the cosine of angle.");
+ Add("Degrees", FunctionCategory.Trigonometric, "Degrees(angleInRadians)", "Converts radians to degrees.");
+ Add("Radians", FunctionCategory.Trigonometric, "Radians(angleInDegrees)", "Converts degrees to radians.");
+ Add("Sin", FunctionCategory.Trigonometric, "Sin(angleInRadians)", "Returns the sine of angle.");
+ Add("Tan", FunctionCategory.Trigonometric, "Tan(angleInRadians)", "Returns the tangent of angle.");
+
+ // Logical
+ Add("Evaluate", FunctionCategory.Logical, "Evaluate(expression {; [fields]})", "Evaluates an expression provided as text.");
+ Add("EvaluationError", FunctionCategory.Logical, "EvaluationError(expression)", "Returns the error number from an evaluation.");
+ Add("GetAsBoolean", FunctionCategory.Logical, "GetAsBoolean(data)", "Returns 0 if data is empty/zero, otherwise 1.");
+ Add("GetField", FunctionCategory.Logical, "GetField(fieldName)", "Returns the value of the field whose name is fieldName.");
+ Add("GetFieldName", FunctionCategory.Logical, "GetFieldName(field)", "Returns the fully qualified name of field.");
+ Add("IsEmpty", FunctionCategory.Logical, "IsEmpty(expression)", "Returns true if expression is empty.");
+ Add("IsValid", FunctionCategory.Logical, "IsValid(field)", "Returns true if field is valid and references a real field.");
+ Add("IsValidExpression", FunctionCategory.Logical, "IsValidExpression(expression)", "Returns true if expression is syntactically valid.");
+ Add("Lookup", FunctionCategory.Logical, "Lookup(sourceField {; failExpression})", "Returns a looked-up value through a relationship.");
+ Add("LookupNext", FunctionCategory.Logical, "LookupNext(sourceField; lower-/higher-flag)", "Returns the next looked-up value.");
+ Add("Self", FunctionCategory.Logical, "Self", "Refers to the object whose property is being evaluated.");
+ Add("SetField", FunctionCategory.Logical, "SetField(fieldName; value)", "Sets the field whose name is fieldName.");
+
+ // Get
+ Add("Get", FunctionCategory.Get, "Get(parameter)",
+ "Returns information about the FileMaker environment.",
+ new FmCalcFunctionParam("parameter", "Selector keyword", GetSelectorKeywords));
+
+ // Container
+ Add("Base64Decode", FunctionCategory.Container, "Base64Decode(text {; fileNameWithExtension})", "Decodes base64 to container or text.");
+ Add("Base64Encode", FunctionCategory.Container, "Base64Encode(data)", "Encodes data as base64.");
+ Add("Base64EncodeRFC", FunctionCategory.Container, "Base64EncodeRFC(rfcNumber; data)", "Encodes data as base64 per the given RFC.");
+ Add("CryptAuthCode", FunctionCategory.Container, "CryptAuthCode(data; algorithm; key)", "Returns an HMAC authentication code.");
+ Add("CryptDecrypt", FunctionCategory.Container, "CryptDecrypt(data; key)", "Decrypts data with key.");
+ Add("CryptDecryptBase64", FunctionCategory.Container, "CryptDecryptBase64(text; key)", "Decrypts base64-encoded data with key.");
+ Add("CryptDigest", FunctionCategory.Container, "CryptDigest(data; algorithm)",
+ "Returns a cryptographic digest of data.",
+ new FmCalcFunctionParam("data"),
+ new FmCalcFunctionParam("algorithm", "Hash algorithm", HashAlgorithms));
+ Add("CryptEncrypt", FunctionCategory.Container, "CryptEncrypt(data; key)", "Encrypts data with key.");
+ Add("CryptEncryptBase64", FunctionCategory.Container, "CryptEncryptBase64(data; key)", "Encrypts data with key and returns base64.");
+ Add("CryptGenerateSignature", FunctionCategory.Container, "CryptGenerateSignature(data; algorithm; privateRSAKey; password)", "Generates an RSA signature.");
+ Add("CryptVerifySignature", FunctionCategory.Container, "CryptVerifySignature(data; algorithm; publicRSAKey; signature)", "Verifies an RSA signature.");
+ Add("GetContainerAttribute", FunctionCategory.Container, "GetContainerAttribute(field; attribute)", "Returns metadata about a container value.");
+ Add("GetHeight", FunctionCategory.Container, "GetHeight(field)", "Returns the height of an image container.");
+ Add("GetThumbnail", FunctionCategory.Container, "GetThumbnail(field; fitToWidth; fitToHeight)", "Returns a thumbnail of a container.");
+ Add("GetWidth", FunctionCategory.Container, "GetWidth(field)", "Returns the width of an image container.");
+ Add("HexDecode", FunctionCategory.Container, "HexDecode(text {; fileNameWithExtension})", "Decodes hex to container or text.");
+ Add("HexEncode", FunctionCategory.Container, "HexEncode(data)", "Encodes data as hex.");
+ Add("VerifyContainer", FunctionCategory.Container, "VerifyContainer(field)", "Returns whether a container's checksum verifies.");
+
+ // JSON
+ Add("JSONDeleteElement", FunctionCategory.Json, "JSONDeleteElement(json; keyOrIndexOrPath)", "Deletes an element at the given path.");
+ Add("JSONFormatElements", FunctionCategory.Json, "JSONFormatElements(json)", "Returns json formatted with indentation.");
+ Add("JSONGetElement", FunctionCategory.Json, "JSONGetElement(json; keyOrIndexOrPath)", "Returns an element at the given path.");
+ Add("JSONListKeys", FunctionCategory.Json, "JSONListKeys(json; keyOrIndexOrPath)", "Returns the keys of an object element.");
+ Add("JSONListValues", FunctionCategory.Json, "JSONListValues(json; keyOrIndexOrPath)", "Returns the values of an object/array element.");
+ Add("JSONSetElement", FunctionCategory.Json, "JSONSetElement(json; keyOrIndexOrPath; value; type)",
+ "Sets an element at the given path.",
+ new FmCalcFunctionParam("json"),
+ new FmCalcFunctionParam("keyOrIndexOrPath"),
+ new FmCalcFunctionParam("value"),
+ new FmCalcFunctionParam("type", "JSON value type", JsonElementTypes));
+
+ // SQL
+ Add("ExecuteSQL", FunctionCategory.Sql, "ExecuteSQL(sql; fieldSeparator; rowSeparator {; arguments...})", "Executes an SQL query against the open database.");
+
+ // External
+ Add("GetSensor", FunctionCategory.External, "GetSensor(sensorType {; options})", "Returns a value from a device sensor (FileMaker Go).");
+ Add("GetLiveRemoteCallResult", FunctionCategory.External, "GetLiveRemoteCallResult(callID)", "Returns the result of a live remote call.");
+ Add("GetLiveRemoteCallStatus", FunctionCategory.External, "GetLiveRemoteCallStatus(callID)", "Returns the status of a live remote call.");
+
+ // Design
+ Add("DatabaseNames", FunctionCategory.Design, "DatabaseNames", "Returns names of open databases.");
+ Add("FieldBounds", FunctionCategory.Design, "FieldBounds(fileName; layoutName; fieldName)", "Returns layout coordinates of a field.");
+ Add("FieldComment", FunctionCategory.Design, "FieldComment(fileName; fieldName)", "Returns the comment for a field.");
+ Add("FieldIDs", FunctionCategory.Design, "FieldIDs(fileName; layoutName)", "Returns field IDs.");
+ Add("FieldNames", FunctionCategory.Design, "FieldNames(fileName; layoutName)", "Returns field names.");
+ Add("FieldRepetitions", FunctionCategory.Design, "FieldRepetitions(fileName; layoutName; fieldName)", "Returns repetitions for a field.");
+ Add("FieldStyle", FunctionCategory.Design, "FieldStyle(fileName; layoutName; fieldName)", "Returns the style applied to a field.");
+ Add("FieldType", FunctionCategory.Design, "FieldType(fileName; fieldName)", "Returns the type of a field.");
+ Add("GetNextSerialValue", FunctionCategory.Design, "GetNextSerialValue(fileName; fieldName)", "Returns the next auto-enter serial value.");
+ Add("LayoutIDs", FunctionCategory.Design, "LayoutIDs(fileName)", "Returns layout IDs.");
+ Add("LayoutNames", FunctionCategory.Design, "LayoutNames(fileName)", "Returns layout names.");
+ Add("LayoutObjectNames", FunctionCategory.Design, "LayoutObjectNames(fileName; layoutName)", "Returns named objects on a layout.");
+ Add("RelationInfo", FunctionCategory.Design, "RelationInfo(fileName; tableOccurrenceName)", "Returns information about relationships.");
+ Add("ScriptIDs", FunctionCategory.Design, "ScriptIDs(fileName)", "Returns script IDs.");
+ Add("ScriptNames", FunctionCategory.Design, "ScriptNames(fileName)", "Returns script names.");
+ Add("TableIDs", FunctionCategory.Design, "TableIDs(fileName)", "Returns table occurrence IDs.");
+ Add("TableNames", FunctionCategory.Design, "TableNames(fileName)", "Returns table occurrence names.");
+ Add("ValueListIDs", FunctionCategory.Design, "ValueListIDs(fileName)", "Returns value list IDs.");
+ Add("ValueListItems", FunctionCategory.Design, "ValueListItems(fileName; valueListName)", "Returns the items of a value list.");
+ Add("ValueListNames", FunctionCategory.Design, "ValueListNames(fileName)", "Returns value list names.");
+ Add("WindowNames", FunctionCategory.Design, "WindowNames({fileName})", "Returns names of open windows.");
+
+ return new ReadOnlyCollection(list);
+ }
+
+ private static IReadOnlyList BuildControlForms()
+ {
+ return new ReadOnlyCollection(new[]
+ {
+ new FmCalcControlForm(
+ "Let",
+ "Let([var = expr; ...]; result)",
+ "Binds variables and evaluates result with them in scope.",
+ "Let ( [ ${1:var} = ${2:value} ] ; ${3:result} )"),
+ new FmCalcControlForm(
+ "Case",
+ "Case(test1; result1 {; test2; result2 ...} {; defaultResult})",
+ "Returns result for the first true test; otherwise default.",
+ "Case ( ${1:test} ; ${2:result} ; ${3:default} )"),
+ new FmCalcControlForm(
+ "If",
+ "If(test; resultIfTrue; resultIfFalse)",
+ "Branches between two results based on test.",
+ "If ( ${1:test} ; ${2:trueResult} ; ${3:falseResult} )"),
+ new FmCalcControlForm(
+ "While",
+ "While([initialVars]; condition; [updateVars]; result)",
+ "Iterates while condition is true and returns result.",
+ "While ( [ ${1:counter} = 0 ] ; ${2:condition} ; [ ${3:counter} = counter + 1 ] ; ${4:result} )"),
+ new FmCalcControlForm(
+ "Choose",
+ "Choose(test; result0 {; result1 ...})",
+ "Returns the Nth result based on test (0-indexed).",
+ "Choose ( ${1:test} ; ${2:result0} ; ${3:result1} )"),
+ });
+ }
+
+ ///
+ /// JSON value-type keywords accepted by JSONSetElement's last argument.
+ ///
+ private static IReadOnlyList BuildJsonElementTypes() => new ReadOnlyCollection(new[]
+ {
+ new FmCalcEnumValue("JSONString", "Quoted string."),
+ new FmCalcEnumValue("JSONNumber", "Numeric value."),
+ new FmCalcEnumValue("JSONObject", "Object literal."),
+ new FmCalcEnumValue("JSONArray", "Array literal."),
+ new FmCalcEnumValue("JSONBoolean", "true or false."),
+ new FmCalcEnumValue("JSONNull", "Null literal."),
+ new FmCalcEnumValue("JSONRaw", "Raw, unquoted JSON fragment."),
+ });
+
+ ///
+ /// Style keywords accepted by TextStyleAdd / TextStyleRemove.
+ ///
+ private static IReadOnlyList BuildTextStyles() => new ReadOnlyCollection(new[]
+ {
+ new FmCalcEnumValue("Plain"),
+ new FmCalcEnumValue("Bold"),
+ new FmCalcEnumValue("Italic"),
+ new FmCalcEnumValue("Underline"),
+ new FmCalcEnumValue("Condense"),
+ new FmCalcEnumValue("Extend"),
+ new FmCalcEnumValue("Strikethrough"),
+ new FmCalcEnumValue("SmallCaps"),
+ new FmCalcEnumValue("Superscript"),
+ new FmCalcEnumValue("Subscript"),
+ new FmCalcEnumValue("Uppercase"),
+ new FmCalcEnumValue("Lowercase"),
+ new FmCalcEnumValue("Titlecase"),
+ new FmCalcEnumValue("WordUnderline"),
+ new FmCalcEnumValue("DoubleUnderline"),
+ new FmCalcEnumValue("AllStyles"),
+ });
+
+ ///
+ /// Hash algorithm keywords accepted by CryptDigest.
+ ///
+ private static IReadOnlyList BuildHashAlgorithms() => new ReadOnlyCollection(new[]
+ {
+ new FmCalcEnumValue("MD5"),
+ new FmCalcEnumValue("SHA1"),
+ new FmCalcEnumValue("SHA256"),
+ new FmCalcEnumValue("SHA512"),
+ new FmCalcEnumValue("SHA224"),
+ new FmCalcEnumValue("SHA384"),
+ });
+
+ ///
+ /// Selector keywords accepted by Get(...). Common subset from
+ /// FileMaker's Get function reference; rare/edge keywords can be appended
+ /// without further wiring.
+ ///
+ private static IReadOnlyList BuildGetSelectorKeywords()
+ {
+ var list = new List();
+ void Add(string n, string d) => list.Add(new FmCalcEnumValue(n, d));
+
+ // Account / privileges
+ Add("AccountName", "Name of the account used to log in.");
+ Add("AccountPrivilegeSetName", "Privilege set name for the current account.");
+ Add("AccountExtendedPrivileges", "Extended privileges for the current account.");
+ Add("AccountGroupName", "Group name of the current externally authenticated account.");
+
+ // Active selection / field
+ Add("ActiveFieldName", "Name of the field with focus.");
+ Add("ActiveFieldContents", "Contents of the field with focus.");
+ Add("ActiveFieldTableName", "Table occurrence of the field with focus.");
+ Add("ActiveLayoutObjectName", "Object name of the layout object with focus.");
+ Add("ActiveModifierKeys", "Bitmask of modifier keys currently held.");
+ Add("ActivePortalRowNumber", "Row number of the active portal.");
+ Add("ActiveRecordNumber", "Record number of the active record in the found set.");
+ Add("ActiveRepetitionNumber", "Active repetition of a repeating field.");
+ Add("ActiveSelectionSize", "Size of the current text selection.");
+ Add("ActiveSelectionStart", "Start position of the current text selection.");
+
+ // App
+ Add("ApplicationArchitecture", "Architecture of the running FileMaker (x86_64, arm64, etc.).");
+ Add("ApplicationLanguage", "Language of the running FileMaker.");
+ Add("ApplicationVersion", "Version string of the running FileMaker.");
+
+ // Calculation / scripting
+ Add("CalculationRepetitionNumber", "Current repetition during calculation evaluation.");
+ Add("ScriptName", "Name of the currently running script.");
+ Add("ScriptParameter", "Parameter passed to the running script.");
+ Add("ScriptResult", "Result returned by the most recently completed sub-script.");
+ Add("ScriptAnimationState", "Whether script animations are currently allowed.");
+
+ // Connection
+ Add("ConnectionAttributes", "Attributes of the current network connection.");
+ Add("ConnectionState", "Encrypted/unencrypted state of the current connection.");
+
+ // Date / time
+ Add("CurrentDate", "Current date.");
+ Add("CurrentTime", "Current time.");
+ Add("CurrentTimestamp", "Current timestamp.");
+ Add("CurrentHostTimestamp", "Current timestamp from the host (sync-safe).");
+ Add("CurrentTimeUTCMilliseconds", "Current UTC time in milliseconds.");
+ Add("CurrentExtendedPrivileges", "Extended privileges for the current session.");
+ Add("CurrentPrivilegeSetName", "Privilege set name in effect for the current session.");
+
+ // Custom menus
+ Add("CustomMenuSetName", "Name of the active custom menu set.");
+
+ // Device / paths
+ Add("Device", "Type of the device running FileMaker.");
+ Add("DocumentsPath", "Path to the user's Documents folder.");
+ Add("DocumentsPathListing", "Listing of files in the Documents folder.");
+ Add("DesktopPath", "Path to the user's Desktop folder.");
+ Add("FileMakerPath", "Path to the running FileMaker application.");
+ Add("PreferencesPath", "Path to the FileMaker preferences folder.");
+ Add("TemporaryPath", "Path to a temporary folder unique to this session.");
+
+ // Encryption
+ Add("EncryptionState", "Encryption state of the current file.");
+
+ // Errors
+ Add("LastError", "Most recent script-step error number.");
+ Add("LastErrorDetail", "Detail string for the most recent error.");
+ Add("LastErrorLocation", "File/script/line of the most recent error.");
+ Add("LastExternalErrorDetail", "Detail from the most recent external error.");
+ Add("LastODBCError", "Most recent ODBC error.");
+ Add("LastMessageChoice", "User's choice from the most recent Show Custom Dialog.");
+ Add("ErrorCaptureState", "Whether error capture is enabled.");
+
+ // File
+ Add("FileName", "Name of the current file.");
+ Add("FilePath", "Full path to the current file.");
+ Add("FileSize", "Size of the current file in bytes.");
+ Add("FileLocaleName", "Locale name for the current file.");
+ Add("FileLocaleElements", "Locale elements for the current file.");
+
+ // Found set / records
+ Add("FoundCount", "Number of records in the current found set.");
+ Add("ModifiedCount", "Number of modified records in the current found set.");
+ Add("RecordID", "Internal record ID of the current record.");
+ Add("RecordNumber", "Record number of the current record.");
+ Add("RecordOpenCount", "Number of open records.");
+ Add("RecordOpenState", "Open state of the current record (0/1/2).");
+ Add("RecordModificationCount", "Number of times the current record has been modified.");
+ Add("RecordAccess", "Access privilege for the current record.");
+ Add("RequestCount", "Number of find requests in the current found set.");
+ Add("RequestOmitState", "Whether the current find request has the omit flag set.");
+ Add("TotalRecordCount", "Total record count for the current table.");
+
+ // Host / network
+ Add("HostApplicationVersion", "Version of the host FileMaker application.");
+ Add("HostIPAddress", "IP address of the host.");
+ Add("HostName", "Name of the host.");
+ Add("MultiUserState", "Multi-user (network sharing) state.");
+ Add("NetworkProtocol", "Network protocol in use.");
+ Add("NetworkType", "Type of network connection.");
+
+ // Layouts
+ Add("LayoutAccess", "Access privilege for the current layout.");
+ Add("LayoutCount", "Number of layouts in the file.");
+ Add("LayoutName", "Name of the current layout.");
+ Add("LayoutNumber", "Number of the current layout.");
+ Add("LayoutTableName", "Table occurrence of the current layout.");
+ Add("LayoutViewState", "Current view state (form/list/table).");
+
+ // Plugins
+ Add("InstalledFMPlugins", "List of installed FileMaker plugins.");
+
+ // Quick find
+ Add("QuickFindText", "Most recent quick find text.");
+
+ // Screen / printer / display
+ Add("ScreenDepth", "Color depth of the screen.");
+ Add("ScreenHeight", "Pixel height of the screen.");
+ Add("ScreenWidth", "Pixel width of the screen.");
+ Add("ScreenScaleFactor", "Scale factor of the screen.");
+ Add("HighContrastColor", "Current high-contrast color.");
+ Add("HighContrastState", "Whether high-contrast mode is enabled.");
+ Add("PrinterName", "Name of the default printer.");
+
+ // System
+ Add("SystemDrive", "Drive letter the OS is installed on.");
+ Add("SystemIPAddress", "IP address of the local machine.");
+ Add("SystemLanguage", "Language code of the OS.");
+ Add("SystemNICAddress", "NIC (MAC) address of the local machine.");
+ Add("SystemPlatform", "OS platform (1=Mac, -2=Windows, etc.).");
+ Add("SystemTimeZoneOffset", "Time zone offset from UTC, in seconds.");
+ Add("SystemVersion", "OS version string.");
+ Add("PersistentID", "Persistent device identifier.");
+ Add("UUID", "A new UUID string.");
+ Add("UUIDNumber", "A new UUID expressed as a number.");
+
+ // Sort / preview / status
+ Add("SortState", "Whether the found set is sorted.");
+ Add("StatusAreaState", "Visibility/locked state of the status area.");
+ Add("PreviewState", "Whether preview mode is active.");
+ Add("PageNumber", "Current page in preview/print.");
+
+ // Triggers
+ Add("TriggerCurrentPanel", "Index of the current panel for OnPanelSwitch triggers.");
+ Add("TriggerTargetPanel", "Index of the target panel for OnPanelSwitch triggers.");
+ Add("TriggerCurrentTabPanel", "Current tab panel index.");
+ Add("TriggerTargetTabPanel", "Target tab panel index.");
+ Add("TriggerKeystroke", "Keystroke that fired the trigger.");
+ Add("TriggerModifierKeys", "Modifiers held when the trigger fired.");
+ Add("TriggerExternalEvent", "External event that fired the trigger.");
+ Add("TriggerGestureInfo", "Gesture info for the trigger.");
+
+ // Window
+ Add("WindowContentHeight", "Height of the window content area.");
+ Add("WindowContentWidth", "Width of the window content area.");
+ Add("WindowDesktopHeight", "Height of the desktop area.");
+ Add("WindowDesktopWidth", "Width of the desktop area.");
+ Add("WindowHeight", "Height of the current window.");
+ Add("WindowWidth", "Width of the current window.");
+ Add("WindowLeft", "Left coordinate of the current window.");
+ Add("WindowTop", "Top coordinate of the current window.");
+ Add("WindowMode", "Mode of the current window.");
+ Add("WindowName", "Name of the current window.");
+ Add("WindowOrientation", "Orientation of the current window.");
+ Add("WindowStyle", "Style of the current window.");
+ Add("WindowTitle", "Title of the current window.");
+ Add("WindowVisible", "Whether the window is visible.");
+ Add("WindowZoomLevel", "Zoom level of the current window.");
+
+ // Misc
+ Add("AllowAbortState", "Whether script abort is currently allowed.");
+ Add("AllowFormattingBarState", "Whether the formatting bar is allowed.");
+ Add("AllowToolbarState", "Whether toolbars are allowed.");
+ Add("OpenDataFileInfo", "Information about open data files.");
+ Add("RegionMonitorEvents", "Region-monitor event flags.");
+ Add("TextRulerVisible", "Whether the text ruler is visible.");
+ Add("TimerEventsCount", "Count of timer events.");
+ Add("TouchKeyboardState", "Whether the touch keyboard is shown.");
+ Add("UserCount", "Count of users connected to the file.");
+ Add("UserName", "Name of the current user (FileMaker preferences).");
+
+ return new ReadOnlyCollection(list);
+ }
+}
diff --git a/src/SharpFM.Model/Scripting/Calc/FmCalcControlForm.cs b/src/SharpFM.Model/Scripting/Calc/FmCalcControlForm.cs
new file mode 100644
index 0000000..340f3e4
--- /dev/null
+++ b/src/SharpFM.Model/Scripting/Calc/FmCalcControlForm.cs
@@ -0,0 +1,13 @@
+namespace SharpFM.Model.Scripting.Calc;
+
+///
+/// One of the FileMaker calculation control forms (Let, Case,
+/// etc.). uses Monaco-style ${N:placeholder}
+/// tab-stops so completion accept inserts the full form with the first slot
+/// pre-selected.
+///
+public sealed record FmCalcControlForm(
+ string Name,
+ string Signature,
+ string Description,
+ string Snippet);
diff --git a/src/SharpFM.Model/Scripting/Calc/FmCalcEnumValue.cs b/src/SharpFM.Model/Scripting/Calc/FmCalcEnumValue.cs
new file mode 100644
index 0000000..5a0a81d
--- /dev/null
+++ b/src/SharpFM.Model/Scripting/Calc/FmCalcEnumValue.cs
@@ -0,0 +1,8 @@
+namespace SharpFM.Model.Scripting.Calc;
+
+///
+/// One valid value for a function parameter that takes an enumerated keyword.
+/// is optional and surfaces in completion tooltips
+/// when present (e.g. each Get(...) selector has its own one-liner).
+///
+public sealed record FmCalcEnumValue(string Name, string? Description = null);
diff --git a/src/SharpFM.Model/Scripting/Calc/FmCalcFunction.cs b/src/SharpFM.Model/Scripting/Calc/FmCalcFunction.cs
new file mode 100644
index 0000000..992d495
--- /dev/null
+++ b/src/SharpFM.Model/Scripting/Calc/FmCalcFunction.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace SharpFM.Model.Scripting.Calc;
+
+///
+/// One built-in FileMaker calculation function. is a
+/// human-readable form (e.g. Length(text)) shown in completion
+/// tooltips; is a one-line summary.
+///
+/// describes each positional parameter. When a
+/// parameter has a list, the
+/// completion provider offers those keywords when the caret is inside that
+/// argument position. Functions whose params are open-ended (numbers,
+/// fields, expressions) leave empty — the catalog only
+/// models keyword arguments.
+///
+public sealed record FmCalcFunction(
+ string Name,
+ FunctionCategory Category,
+ string Signature,
+ string Description,
+ IReadOnlyList Params);
diff --git a/src/SharpFM.Model/Scripting/Calc/FmCalcFunctionParam.cs b/src/SharpFM.Model/Scripting/Calc/FmCalcFunctionParam.cs
new file mode 100644
index 0000000..add4005
--- /dev/null
+++ b/src/SharpFM.Model/Scripting/Calc/FmCalcFunctionParam.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace SharpFM.Model.Scripting.Calc;
+
+///
+/// One parameter of a built-in calculation function.
+/// is the keyword set the parameter accepts (e.g. Get(parameter)'s
+/// selectors, JSONSetElement's type values); null when
+/// the parameter is open-ended (a number, string, field, expression, …).
+///
+public sealed record FmCalcFunctionParam(
+ string Name,
+ string? Description = null,
+ IReadOnlyList? ValidValues = null);
diff --git a/src/SharpFM.Model/Scripting/Calc/FmCalcGrammarBuilder.cs b/src/SharpFM.Model/Scripting/Calc/FmCalcGrammarBuilder.cs
new file mode 100644
index 0000000..2e313ed
--- /dev/null
+++ b/src/SharpFM.Model/Scripting/Calc/FmCalcGrammarBuilder.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Linq;
+using System.Text.Json.Nodes;
+
+namespace SharpFM.Model.Scripting.Calc;
+
+///
+/// Builds the source.fmcalc TextMate grammar JSON from
+/// . Called at runtime by the registry — there
+/// is no committed grammar file. The catalog is the single source of truth;
+/// the completion provider and the grammar both read from it.
+///
+public static class FmCalcGrammarBuilder
+{
+ public static string Build()
+ {
+ var root = new JsonObject
+ {
+ ["name"] = "FileMaker Calculation",
+ ["scopeName"] = "source.fmcalc",
+ ["fileTypes"] = new JsonArray("fmcalc"),
+ ["patterns"] = new JsonArray(Include("#expression")),
+ ["repository"] = BuildRepository(),
+ };
+
+ var options = new System.Text.Json.JsonSerializerOptions
+ {
+ WriteIndented = true,
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ };
+ return root.ToJsonString(options);
+ }
+
+ private static JsonObject BuildRepository() => new()
+ {
+ ["expression"] = new JsonObject
+ {
+ ["patterns"] = new JsonArray(
+ Include("#comment-block"),
+ Include("#comment-line"),
+ Include("#string"),
+ Include("#number"),
+ Include("#constant"),
+ Include("#control-form"),
+ Include("#operator-word"),
+ Include("#variable"),
+ Include("#field-reference"),
+ Include("#builtin-function"),
+ Include("#custom-function"),
+ Include("#operator-symbol"),
+ Include("#punctuation")),
+ },
+ ["comment-block"] = new JsonObject
+ {
+ ["name"] = "comment.block.fmcalc",
+ ["begin"] = @"/\*",
+ ["beginCaptures"] = NamedCapture("0", "punctuation.definition.comment.fmcalc"),
+ ["end"] = @"\*/",
+ ["endCaptures"] = NamedCapture("0", "punctuation.definition.comment.fmcalc"),
+ },
+ ["comment-line"] = new JsonObject
+ {
+ ["name"] = "comment.line.double-slash.fmcalc",
+ ["begin"] = "//",
+ ["beginCaptures"] = NamedCapture("0", "punctuation.definition.comment.fmcalc"),
+ ["end"] = "$",
+ },
+ ["string"] = new JsonObject
+ {
+ ["name"] = "string.quoted.double.fmcalc",
+ ["begin"] = "\"",
+ ["beginCaptures"] = NamedCapture("0", "punctuation.definition.string.begin.fmcalc"),
+ ["end"] = "\"",
+ ["endCaptures"] = NamedCapture("0", "punctuation.definition.string.end.fmcalc"),
+ ["patterns"] = new JsonArray(new JsonObject
+ {
+ ["name"] = "constant.character.escape.fmcalc",
+ ["match"] = @"\\(""|\\|n|r|t)",
+ }),
+ },
+ ["number"] = new JsonObject
+ {
+ ["name"] = "constant.numeric.fmcalc",
+ ["match"] = @"\b\d+(\.\d+)?([eE][+-]?\d+)?\b",
+ },
+ ["constant"] = new JsonObject
+ {
+ ["name"] = "constant.language.fmcalc",
+ ["match"] = $@"\b({string.Join("|", FmCalcCatalog.Constants)})\b",
+ },
+ ["control-form"] = new JsonObject
+ {
+ ["name"] = "keyword.control.fmcalc",
+ ["match"] = $@"\b({string.Join("|", FmCalcCatalog.ControlForms.Select(c => c.Name))})(?=\s*\()",
+ },
+ ["operator-word"] = new JsonObject
+ {
+ ["name"] = "keyword.operator.word.fmcalc",
+ ["match"] = $@"\b({string.Join("|", FmCalcCatalog.WordOperators)})\b",
+ },
+ ["variable"] = new JsonObject
+ {
+ ["name"] = "variable.other.fmcalc",
+ ["match"] = @"\${1,2}[A-Za-z_][A-Za-z0-9_.]*",
+ },
+ ["field-reference"] = new JsonObject
+ {
+ ["match"] = @"\b([A-Za-z_][A-Za-z0-9_ ]*?)(::)([A-Za-z_][A-Za-z0-9_]*)",
+ ["captures"] = new JsonObject
+ {
+ ["1"] = ScopeNode("entity.name.type.fmcalc"),
+ ["2"] = ScopeNode("punctuation.separator.field.fmcalc"),
+ ["3"] = ScopeNode("variable.other.member.fmcalc"),
+ },
+ },
+ ["builtin-function"] = BuildBuiltinFunctions(),
+ ["custom-function"] = new JsonObject
+ {
+ ["match"] = @"\b([A-Za-z_][A-Za-z0-9_]*)\s*(?=\()",
+ ["captures"] = new JsonObject
+ {
+ ["1"] = ScopeNode("entity.name.function.fmcalc"),
+ },
+ },
+ ["operator-symbol"] = new JsonObject
+ {
+ ["name"] = "keyword.operator.fmcalc",
+ ["match"] = @"(\^|\*|/|\+|-|&|=|≠|<>|≤|<=|≥|>=|<|>)",
+ },
+ ["punctuation"] = new JsonObject
+ {
+ ["patterns"] = new JsonArray(
+ new JsonObject { ["match"] = @"\(", ["name"] = "punctuation.section.parens.begin.fmcalc" },
+ new JsonObject { ["match"] = @"\)", ["name"] = "punctuation.section.parens.end.fmcalc" },
+ new JsonObject { ["match"] = ";", ["name"] = "punctuation.separator.fmcalc" },
+ new JsonObject { ["match"] = ",", ["name"] = "punctuation.separator.fmcalc" },
+ new JsonObject { ["match"] = @"\[", ["name"] = "punctuation.section.brackets.begin.fmcalc" },
+ new JsonObject { ["match"] = @"\]", ["name"] = "punctuation.section.brackets.end.fmcalc" }),
+ },
+ };
+
+ private static JsonObject BuildBuiltinFunctions()
+ {
+ var patterns = new JsonArray();
+
+ // One alternation per category so each can carry its own
+ // support.function..fmcalc scope. Within a category,
+ // sort length-desc then alphabetical — the standard TextMate idiom
+ // that ensures longer alternatives win when one name prefixes another.
+ var groups = FmCalcCatalog.Functions
+ .GroupBy(f => f.Category)
+ .OrderBy(g => (int)g.Key);
+
+ foreach (var group in groups)
+ {
+ var names = group
+ .Select(f => f.Name)
+ .OrderByDescending(n => n.Length)
+ .ThenBy(n => n, StringComparer.Ordinal)
+ .ToList();
+
+ patterns.Add(new JsonObject
+ {
+ ["match"] = $@"\b({string.Join("|", names)})\s*(?=\()",
+ ["captures"] = new JsonObject
+ {
+ ["1"] = ScopeNode($"support.function.{ScopeSegment(group.Key)}.fmcalc"),
+ },
+ });
+ }
+
+ return new JsonObject { ["patterns"] = patterns };
+ }
+
+ ///
+ /// Map a category enum to its TextMate scope segment. Hyphenated for
+ /// multi-word categories, matching VS Code grammar conventions.
+ ///
+ public static string ScopeSegment(FunctionCategory category) => category switch
+ {
+ FunctionCategory.Text => "text",
+ FunctionCategory.TextFormatting => "text-formatting",
+ FunctionCategory.Number => "number",
+ FunctionCategory.Date => "date",
+ FunctionCategory.Time => "time",
+ FunctionCategory.Aggregate => "aggregate",
+ FunctionCategory.Summary => "summary",
+ FunctionCategory.Financial => "financial",
+ FunctionCategory.Trigonometric => "trigonometric",
+ FunctionCategory.Logical => "logical",
+ FunctionCategory.Get => "get",
+ FunctionCategory.Container => "container",
+ FunctionCategory.Json => "json",
+ FunctionCategory.Sql => "sql",
+ FunctionCategory.External => "external",
+ FunctionCategory.Design => "design",
+ _ => throw new ArgumentOutOfRangeException(nameof(category), category, null),
+ };
+
+ private static JsonObject Include(string reference) => new() { ["include"] = reference };
+ private static JsonObject ScopeNode(string name) => new() { ["name"] = name };
+
+ private static JsonObject NamedCapture(string group, string scope) => new()
+ {
+ [group] = ScopeNode(scope),
+ };
+}
diff --git a/src/SharpFM.Model/Scripting/Calc/FmCalcSignatureParser.cs b/src/SharpFM.Model/Scripting/Calc/FmCalcSignatureParser.cs
new file mode 100644
index 0000000..bf598bb
--- /dev/null
+++ b/src/SharpFM.Model/Scripting/Calc/FmCalcSignatureParser.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+
+namespace SharpFM.Model.Scripting.Calc;
+
+///
+/// Parses the param list out of a function signature string like
+/// JSONGetElement(json; keyOrIndexOrPath) so completion items can
+/// tab through placeholders without us hand-authoring a per-function param
+/// list. Variadic markers like {; field...} are dropped — anything
+/// before the first { is taken as the named portion.
+///
+public static class FmCalcSignatureParser
+{
+ public static IReadOnlyList ParseParams(string signature)
+ {
+ if (string.IsNullOrWhiteSpace(signature)) return System.Array.Empty();
+
+ var openParen = signature.IndexOf('(');
+ var closeParen = signature.LastIndexOf(')');
+ if (openParen < 0 || closeParen < openParen) return System.Array.Empty();
+
+ var inner = signature.Substring(openParen + 1, closeParen - openParen - 1);
+
+ // Drop any variadic / optional region. Catalog signatures use
+ // {; ...} for "and more like this" — we don't model that as a
+ // separate stop, so trim it off.
+ var brace = inner.IndexOf('{');
+ if (brace >= 0) inner = inner.Substring(0, brace);
+
+ if (string.IsNullOrWhiteSpace(inner)) return System.Array.Empty();
+
+ var result = new List();
+ foreach (var part in inner.Split(';'))
+ {
+ var name = part.Trim().TrimStart('[').TrimEnd(']').Trim();
+ if (name.Length == 0) continue;
+ result.Add(new FmCalcFunctionParam(name));
+ }
+ return result;
+ }
+}
diff --git a/src/SharpFM.Model/Scripting/Calc/FunctionCategory.cs b/src/SharpFM.Model/Scripting/Calc/FunctionCategory.cs
new file mode 100644
index 0000000..0b12330
--- /dev/null
+++ b/src/SharpFM.Model/Scripting/Calc/FunctionCategory.cs
@@ -0,0 +1,26 @@
+namespace SharpFM.Model.Scripting.Calc;
+
+///
+/// FileMaker calculation function categories. Mirrors the grouping in
+/// FileMaker's calculation dialog so completion menus and TextMate scopes
+/// (support.function.<category>.fmcalc) line up.
+///
+public enum FunctionCategory
+{
+ Text,
+ TextFormatting,
+ Number,
+ Date,
+ Time,
+ Aggregate,
+ Summary,
+ Financial,
+ Trigonometric,
+ Logical,
+ Get,
+ Container,
+ Json,
+ Sql,
+ External,
+ Design,
+}
diff --git a/src/SharpFM/Editors/ScriptTextEditor.cs b/src/SharpFM/Editors/ScriptTextEditor.cs
index 4ee6c71..02e8905 100644
--- a/src/SharpFM/Editors/ScriptTextEditor.cs
+++ b/src/SharpFM/Editors/ScriptTextEditor.cs
@@ -32,7 +32,7 @@ public class ScriptTextEditor : TextEditor, IDisposable
private static readonly RegistryOptions SharedRegistry =
new((ThemeName)(int)ThemeName.DarkPlus);
- private static readonly FmScriptRegistryOptions SharedFmRegistry =
+ private static readonly FmLanguageRegistryOptions SharedFmRegistry =
new(SharedRegistry);
private readonly TextMate.Installation _textMate;
@@ -41,7 +41,7 @@ public class ScriptTextEditor : TextEditor, IDisposable
public ScriptTextEditor()
{
_textMate = this.InstallTextMate(SharedFmRegistry);
- _textMate.SetGrammar(FmScriptRegistryOptions.ScopeName);
+ _textMate.SetGrammar(FmLanguageRegistryOptions.ScriptScopeName);
_controller = new ScriptEditorController(this);
_controller.StatusMessageRaised += OnStatusMessageRaised;
diff --git a/src/SharpFM/Schema/Editor/CalculationEditorWindow.axaml.cs b/src/SharpFM/Schema/Editor/CalculationEditorWindow.axaml.cs
index 1affbd8..916c015 100644
--- a/src/SharpFM/Schema/Editor/CalculationEditorWindow.axaml.cs
+++ b/src/SharpFM/Schema/Editor/CalculationEditorWindow.axaml.cs
@@ -1,10 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
+using Avalonia.Input;
using Avalonia.Interactivity;
using AvaloniaEdit;
+using AvaloniaEdit.CodeCompletion;
using AvaloniaEdit.TextMate;
using SharpFM.Model.Schema;
using SharpFM.Scripting;
+using SharpFM.Scripting.Editor;
using TextMateSharp.Grammars;
namespace SharpFM.Schema.Editor;
@@ -14,48 +17,130 @@ public partial class CalculationEditorWindow : Window
{
private readonly FmField _field;
private readonly TextMate.Installation _textMateInstallation;
+ private readonly TextEditor _editor;
+ private readonly CalcCompletionContextProvider _completionContext;
+ private CompletionWindow? _completionWindow;
public bool Saved { get; private set; }
// Required by XAML loader
- public CalculationEditorWindow() : this(new FmField()) { }
+ public CalculationEditorWindow() : this(new FmField(), null) { }
- public CalculationEditorWindow(FmField field)
+ public CalculationEditorWindow(FmField field) : this(field, null) { }
+
+ public CalculationEditorWindow(FmField field, FmTable? currentTable)
{
InitializeComponent();
_field = field;
- // Set up FM script syntax highlighting for calculations
+ // Set up FM calculation syntax highlighting
var registryOptions = new RegistryOptions((ThemeName)(int)ThemeName.DarkPlus);
- var fmRegistry = new FmScriptRegistryOptions(registryOptions);
- var editor = this.FindControl("calcEditor")!;
- _textMateInstallation = editor.InstallTextMate(fmRegistry);
- _textMateInstallation.SetGrammar(FmScriptRegistryOptions.ScopeName);
+ var fmRegistry = new FmLanguageRegistryOptions(registryOptions);
+ _editor = this.FindControl("calcEditor")!;
+ _textMateInstallation = _editor.InstallTextMate(fmRegistry);
+ _textMateInstallation.SetGrammar(FmLanguageRegistryOptions.CalcScopeName);
// Populate fields
- editor.Text = field.Calculation ?? "";
+ _editor.Text = field.Calculation ?? "";
var contextBox = this.FindControl("contextTableBox")!;
contextBox.Text = field.CalculationContext ?? "";
var alwaysCheck = this.FindControl("alwaysEvaluateCheck")!;
alwaysCheck.IsChecked = field.AlwaysEvaluate;
+ _completionContext = new CalcCompletionContextProvider(
+ getDocumentText: () => _editor.Document.Text,
+ currentTable: currentTable);
+
+ // Completion: pop the menu on every text input. Same trigger model
+ // the script editor uses, kept inline rather than introducing a
+ // controller — calc editor has no other adornments to coordinate.
+ _editor.TextArea.TextEntered += OnTextEntered;
+ _editor.TextArea.TextEntering += OnTextEntering;
+
// Wire buttons
this.FindControl