From b250220a5b8f27321d3198b2f748652234d74df0 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 22 Sep 2025 20:40:17 +0100 Subject: [PATCH 1/9] Add support for member auto completion The essence of the approach is the declaring type analyzer used to extract the semantic expression closest to the caret position. Auto-completion is performed by looking for all public members of the declaring type. --- ...Bonsai.Scripting.Expressions.Design.csproj | 3 + .../CaretExpressionAnalyzer.cs | 724 ++++++++++++++++++ .../ExpressionScriptEditor.cs | 48 +- .../ExpressionScriptEditorDialog.Designer.cs | 2 +- .../ExpressionScriptEditorDialog.cs | 118 ++- .../Properties/Resources.Designer.cs | 93 +++ .../Properties/Resources.resx | 150 ++++ 7 files changed, 1119 insertions(+), 19 deletions(-) create mode 100644 src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs create mode 100644 src/Bonsai.Scripting.Expressions.Design/Properties/Resources.Designer.cs create mode 100644 src/Bonsai.Scripting.Expressions.Design/Properties/Resources.resx diff --git a/src/Bonsai.Scripting.Expressions.Design/Bonsai.Scripting.Expressions.Design.csproj b/src/Bonsai.Scripting.Expressions.Design/Bonsai.Scripting.Expressions.Design.csproj index 06238b0..010ab27 100644 --- a/src/Bonsai.Scripting.Expressions.Design/Bonsai.Scripting.Expressions.Design.csproj +++ b/src/Bonsai.Scripting.Expressions.Design/Bonsai.Scripting.Expressions.Design.csproj @@ -5,6 +5,9 @@ true net472;net8.0-windows + + + diff --git a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs new file mode 100644 index 0000000..dbe4aa1 --- /dev/null +++ b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs @@ -0,0 +1,724 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core.Config; +using System.Linq.Dynamic.Core.Exceptions; +using System.Linq.Dynamic.Core.Tokenizer; +using System.Linq.Expressions; +using System.Text.RegularExpressions; + +namespace Bonsai.Scripting.Expressions.Design +{ + /// + /// Analyzes the expression at selected caret position to determine the nearest declaring type. + /// Some parts of the code is based on https://github.com/zzzprojects/System.Linq.Dynamic.Core + /// and licensed under the Apache 2.0 license. + /// + internal class CaretExpressionAnalyzer + { + private static readonly string[] OutKeywords = ["out", "$out"]; + private const string DiscardVariable = "_"; + + readonly ParsingConfig _parsingConfig; + readonly TextParser _textParser; + readonly string _text; + int _startPos; + + public CaretExpressionAnalyzer(ParsingConfig config, string text, int position) + { + _parsingConfig = config; + _text = position > 0 ? text.Substring(0, position) : string.Empty; + _textParser = new TextParser(_parsingConfig, _text); + } + + public LambdaExpression? Parse(Type? itType = null) + { + _startPos = 0; + try { ParseConditionalOperator(); } + catch (ParseException) { } + + var text = _text.Substring(_startPos); + if (string.IsNullOrEmpty(text)) + return null; + + return DynamicExpressionParser.ParseLambda(_parsingConfig, itType, null, text); + } + + // out keyword + private void ParseOutKeyword() + { + if (_textParser.CurrentToken.Id == TokenId.Identifier && OutKeywords.Contains(_textParser.CurrentToken.Text)) + { + // Go to next token (which should be a '_') + _textParser.NextToken(); + + var variableName = _textParser.CurrentToken.Text; + if (variableName != DiscardVariable) + { + throw ParseError("OutKeywordRequiresDiscard"); + } + + // Advance to next token + _textParser.NextToken(); + } + + ParseConditionalOperator(); + } + + // ?: operator + private void ParseConditionalOperator() + { + int errorPos = _textParser.CurrentToken.Pos; + ParseNullCoalescingOperator(); + if (_textParser.CurrentToken.Id == TokenId.Question) + { + _textParser.NextToken(); + ParseConditionalOperator(); + _textParser.ValidateToken(TokenId.Colon, "ColonExpected"); + _textParser.NextToken(); + ParseConditionalOperator(); + } + } + + // ?? (null-coalescing) operator + private void ParseNullCoalescingOperator() + { + ParseLambdaOperator(); + if (_textParser.CurrentToken.Id == TokenId.NullCoalescing) + { + _textParser.NextToken(); + ParseConditionalOperator(); + } + } + + // => operator - Added Support for projection operator + private void ParseLambdaOperator() + { + ParseOrOperator(); + + if (_textParser.CurrentToken.Id == TokenId.Lambda) + { + _textParser.NextToken(); + if (_textParser.CurrentToken.Id is TokenId.Identifier or TokenId.OpenParen) + { + ParseConditionalOperator(); + } + _textParser.ValidateToken(TokenId.OpenParen, "OpenParenExpected"); + } + } + + // Or operator + // - || + // - Or + // - OrElse + private void ParseOrOperator() + { + ParseAndOperator(); + while (_textParser.CurrentToken.Id == TokenId.DoubleBar) + { + Token op = _textParser.CurrentToken; + _textParser.NextToken(); + ParseAndOperator(); + } + } + + // And operator + // - && + // - And + // - AndAlso + private void ParseAndOperator() + { + ParseIn(); + while (_textParser.CurrentToken.Id == TokenId.DoubleAmpersand) + { + Token op = _textParser.CurrentToken; + _textParser.NextToken(); + ParseIn(); + } + } + + // "in" / "not in" / "not_in" operator for literals - example: "x in (1,2,3,4)" + // "in" / "not in" / "not_in" operator to mimic contains - example: "x in @0", compare to @0.Contains(x) + private void ParseIn() + { + ParseLogicalAndOrOperator(); + while (_textParser.TryGetToken(["in", "not_in", "not"], [TokenId.Exclamation], out var token)) + { + if (token.Text == "not" || token.Id == TokenId.Exclamation) + { + _textParser.NextToken(); + + if (!TokenIsIdentifier("in")) + { + throw ParseError(token.Pos, "TokenExpected", "in"); + } + } + + _textParser.NextToken(); + + if (_textParser.CurrentToken.Id == TokenId.OpenParen) // literals (or other inline list) + { + while (_textParser.CurrentToken.Id != TokenId.CloseParen) + { + _textParser.NextToken(); + + // we need to parse unary expressions because otherwise 'in' clause will fail in use cases like 'in (-1, -1)' or 'in (!true)' + ParseUnary(); + + if (_textParser.CurrentToken.Id == TokenId.End) + { + throw ParseError(token.Pos, "CloseParenOrCommaExpected"); + } + } + + // Since this started with an open paren, make sure to move off the close + _textParser.NextToken(); + } + else if (_textParser.CurrentToken.Id == TokenId.Identifier) // a single argument + { + ParsePrimary(); + } + else + { + throw ParseError(token.Pos, "OpenParenOrIdentifierExpected"); + } + } + } + + // &, | bitwise operators + private void ParseLogicalAndOrOperator() + { + ParseComparisonOperator(); + + while (_textParser.CurrentToken.Id is TokenId.Ampersand or TokenId.Bar) + { + Token op = _textParser.CurrentToken; + _textParser.NextToken(); + ParseComparisonOperator(); + } + } + + // =, ==, !=, <>, >, >=, <, <= operators + private void ParseComparisonOperator() + { + ParseShiftOperator(); + while (_textParser.CurrentToken.Id.IsComparisonOperator()) + { + Token op = _textParser.CurrentToken; + _textParser.NextToken(); + ParseShiftOperator(); + } + } + + // <<, >> operators + private void ParseShiftOperator() + { + ParseAdditive(); + while (_textParser.CurrentToken.Id == TokenId.DoubleLessThan || _textParser.CurrentToken.Id == TokenId.DoubleGreaterThan) + { + Token op = _textParser.CurrentToken; + _textParser.NextToken(); + ParseAdditive(); + } + } + + // +, - operators + private void ParseAdditive() + { + ParseArithmetic(); + while (_textParser.CurrentToken.Id is TokenId.Plus or TokenId.Minus) + { + Token op = _textParser.CurrentToken; + _textParser.NextToken(); + ParseArithmetic(); + } + } + + // *, /, %, mod operators + private void ParseArithmetic() + { + ParseUnary(); + while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIsIdentifier("mod")) + { + Token op = _textParser.CurrentToken; + _textParser.NextToken(); + ParseUnary(); + } + } + + // -, !, not unary operators + private void ParseUnary() + { + if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIsIdentifier("not")) + { + Token op = _textParser.CurrentToken; + _textParser.NextToken(); + if (op.Id == TokenId.Minus && _textParser.CurrentToken.Id is TokenId.IntegerLiteral or TokenId.RealLiteral) + { + _textParser.CurrentToken.Text = "-" + _textParser.CurrentToken.Text; + _textParser.CurrentToken.Pos = op.Pos; + ParsePrimary(); + } + + ParseUnary(); + } + + ParsePrimary(); + } + + // primary elements + private void ParsePrimary() + { + ParsePrimaryStart(); + + while (true) + { + if (_textParser.CurrentToken.Id == TokenId.Dot) + { + _textParser.NextToken(); + ParseMemberAccess(); + } + else if (_textParser.CurrentToken.Id == TokenId.NullPropagation) + { + throw new NotSupportedException("An expression tree lambda may not contain a null propagating operator. Use the 'np()' or 'np(...)' (null-propagation) function instead."); + } + else if (_textParser.CurrentToken.Id == TokenId.OpenBracket) + { + ParseElementAccess(); + } + else + { + break; + } + } + } + + private void ParsePrimaryStart() + { + _startPos = _textParser.CurrentToken.Pos; + switch (_textParser.CurrentToken.Id) + { + case TokenId.Identifier: + ParseIdentifier(); + break; + + case TokenId.StringLiteral: + ParseStringLiteralAsStringExpressionOrTypeExpression(); + break; + + case TokenId.IntegerLiteral: + ParseIntegerLiteral(); + break; + + case TokenId.RealLiteral: + ParseRealLiteral(); + break; + + case TokenId.OpenParen: + ParseParenExpression(); + break; + + default: + throw ParseError("ExpressionExpected"); + } + } + + private void ParseStringLiteralAsStringExpressionOrTypeExpression() + { + var clonedTextParser = _textParser.Clone(); + clonedTextParser.NextToken(); + + // Check if next token is a "(" or a "?(". + // Used for casting like $"\"System.DateTime\"(Abc)" or $"\"System.DateTime\"?(Abc)". + // In that case, the string value is NOT forced to stay a string. + bool forceParseAsString = true; + if (clonedTextParser.CurrentToken.Id == TokenId.OpenParen) + { + forceParseAsString = false; + } + else if (clonedTextParser.CurrentToken.Id == TokenId.Question) + { + clonedTextParser.NextToken(); + if (clonedTextParser.CurrentToken.Id == TokenId.OpenParen) + { + forceParseAsString = false; + } + } + + ParseStringLiteral(forceParseAsString); + } + + private void ParseStringLiteral(bool forceParseAsString) + { + _textParser.ValidateToken(TokenId.StringLiteral); + + var text = _textParser.CurrentToken.Text; + var parsedStringValue = ParseStringAndEscape(text); + + if (_textParser.CurrentToken.Text[0] == '\'') + { + if (parsedStringValue.Length > 1) + { + throw ParseError("InvalidCharacterLiteral"); + } + + _textParser.NextToken(); + return; + } + + _textParser.NextToken(); + + // While the next token is also a string, keep concatenating these strings and get next token + while (_textParser.CurrentToken.Id == TokenId.StringLiteral) + { + text += _textParser.CurrentToken.Text; + _textParser.NextToken(); + } + + parsedStringValue = ParseStringAndEscape(text); + } + + private string ParseStringAndEscape(string text) + { + return _parsingConfig.StringLiteralParsing == StringLiteralParsingType.EscapeDoubleQuoteByTwoDoubleQuotes ? + StringParser.ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(text, _textParser.CurrentToken.Pos) : + StringParser.ParseStringAndUnescape(text, _textParser.CurrentToken.Pos); + } + + private void ParseIntegerLiteral() + { + _textParser.ValidateToken(TokenId.IntegerLiteral); + string text = _textParser.CurrentToken.Text; + var tokenPosition = _textParser.CurrentToken.Pos; + _textParser.NextToken(); + } + + private void ParseRealLiteral() + { + _textParser.ValidateToken(TokenId.RealLiteral); + string text = _textParser.CurrentToken.Text; + _textParser.NextToken(); + } + + private void ParseParenExpression() + { + _textParser.ValidateToken(TokenId.OpenParen, "OpenParenExpected"); + _textParser.NextToken(); + ParseConditionalOperator(); + _textParser.ValidateToken(TokenId.CloseParen, "CloseParenOrOperatorExpected"); + _textParser.NextToken(); + } + + private void ParseIdentifier() + { + _textParser.ValidateToken(TokenId.Identifier); + if (TokenIsIdentifier("new")) + ParseNew(); + else + ParseMemberAccess(); + } + + // new (...) function + private void ParseNew() + { + _textParser.NextToken(); + if (_textParser.CurrentToken.Id != TokenId.OpenParen && + _textParser.CurrentToken.Id != TokenId.OpenCurlyParen && + _textParser.CurrentToken.Id != TokenId.OpenBracket && + _textParser.CurrentToken.Id != TokenId.Identifier) + { + throw ParseError("OpenParenOrIdentifierExpected"); + } + + if (_textParser.CurrentToken.Id == TokenId.Identifier) + { + var newTypeName = _textParser.CurrentToken.Text; + + _textParser.NextToken(); + + while (_textParser.CurrentToken.Id is TokenId.Dot or TokenId.Plus) + { + var sep = _textParser.CurrentToken.Text; + _textParser.NextToken(); + if (_textParser.CurrentToken.Id != TokenId.Identifier) + { + throw ParseError("IdentifierExpected"); + } + newTypeName += sep + _textParser.CurrentToken.Text; + _textParser.NextToken(); + } + + if (_textParser.CurrentToken.Id != TokenId.OpenParen && + _textParser.CurrentToken.Id != TokenId.OpenBracket && + _textParser.CurrentToken.Id != TokenId.OpenCurlyParen) + { + throw ParseError("OpenParenExpected"); + } + } + + bool arrayInitializer = false; + if (_textParser.CurrentToken.Id == TokenId.OpenBracket) + { + _textParser.NextToken(); + _textParser.ValidateToken(TokenId.CloseBracket, "CloseBracketExpected"); + _textParser.NextToken(); + _textParser.ValidateToken(TokenId.OpenCurlyParen, "OpenCurlyParenExpected"); + arrayInitializer = true; + } + + _textParser.NextToken(); + + var properties = new List(); + var expressions = new List(); + + while (_textParser.CurrentToken.Id != TokenId.CloseParen && _textParser.CurrentToken.Id != TokenId.CloseCurlyParen) + { + int exprPos = _textParser.CurrentToken.Pos; + ParseConditionalOperator(); + if (!arrayInitializer) + { + string? propName; + if (TokenIsIdentifier("as")) + { + _textParser.NextToken(); + propName = GetIdentifierAs(); + } + } + + if (_textParser.CurrentToken.Id != TokenId.Comma) + { + break; + } + + _textParser.NextToken(); + } + + if (_textParser.CurrentToken.Id != TokenId.CloseParen && _textParser.CurrentToken.Id != TokenId.CloseCurlyParen) + { + throw ParseError("CloseParenOrCommaExpected"); + } + _textParser.NextToken(); + } + + private void ParseLambdaInvocation() + { + int errorPos = _textParser.CurrentToken.Pos; + _textParser.NextToken(); + ParseArgumentList(); + } + + private void ParseMemberAccess() + { + var errorPos = _textParser.CurrentToken.Pos; + var id = GetIdentifier(); + _textParser.NextToken(); + + // Parse as Lambda + if (_textParser.CurrentToken.Id == TokenId.Lambda) + { + ParseAsLambda(); + return; + } + + // This could be enum like "A.B.C.MyEnum.Value1" or "A.B.C+MyEnum.Value1". + // + // Or it's a nested (static) class with a + // - static property like "NestedClass.MyProperty" + // - static method like "NestedClass.MyMethod" + if (_textParser.CurrentToken.Id is TokenId.Dot or TokenId.Plus) + { + ParseAsEnumOrNestedClass(); + } + + if (_textParser.CurrentToken.Id == TokenId.OpenParen) + { + ParseArgumentList(); + } + } + + private void ParseAsLambda() + { + // next + _textParser.NextToken(); + ParseConditionalOperator(); + } + + private void ParseAsEnumOrNestedClass() + { + while (_textParser.CurrentToken.Id is TokenId.Dot or TokenId.Plus) + { + if (_textParser.CurrentToken.Id is TokenId.Dot or TokenId.Plus) + { + _textParser.NextToken(); + } + + if (_textParser.CurrentToken.Id == TokenId.Identifier) + { + _textParser.NextToken(); + } + } + + if (_textParser.CurrentToken.Id == TokenId.Identifier) + ParseMemberAccess(); + } + + private void ParseArgumentList() + { + _textParser.ValidateToken(TokenId.OpenParen, "OpenParenExpected"); + _textParser.NextToken(); + + if (_textParser.CurrentToken.Id != TokenId.CloseParen) + ParseArguments(); + + _textParser.ValidateToken(TokenId.CloseParen, "CloseParenOrCommaExpected"); + _textParser.NextToken(); + } + + private void ParseArguments() + { + while (true) + { + ParseOutKeyword(); + if (_textParser.CurrentToken.Id != TokenId.Comma) + { + break; + } + + _textParser.NextToken(); + } + } + + private void ParseElementAccess() + { + int errorPos = _textParser.CurrentToken.Pos; + _textParser.ValidateToken(TokenId.OpenBracket, "OpenParenExpected"); + _textParser.NextToken(); + + ParseArguments(); + _textParser.ValidateToken(TokenId.CloseBracket, "CloseBracketOrCommaExpected"); + _textParser.NextToken(); + } + + private bool TokenIsIdentifier(string id) + { + return _textParser.TokenIsIdentifier(id); + } + + private string GetIdentifier() + { + _textParser.ValidateToken(TokenId.Identifier, "IdentifierExpected"); + return SanitizeId(_textParser.CurrentToken.Text); + } + + private string GetIdentifierAs() + { + _textParser.ValidateToken(TokenId.Identifier, "IdentifierExpected"); + + if (!_parsingConfig.SupportDotInPropertyNames) + { + var id = SanitizeId(_textParser.CurrentToken.Text); + _textParser.NextToken(); + return id; + } + + var parts = new List(); + while (_textParser.CurrentToken.Id is TokenId.Dot or TokenId.Identifier) + { + parts.Add(_textParser.CurrentToken.Text); + _textParser.NextToken(); + } + + return SanitizeId(string.Concat(parts)); + } + + private static string SanitizeId(string id) + { + if (id.Length > 1 && id[0] == '@') + { + id = id.Substring(1); + } + + return id; + } + + private Exception ParseError(string format, params object[] args) + { + return ParseError(_textParser.CurrentToken.Pos, format, args); + } + + private static Exception ParseError(int pos, string format, params object[] args) + { + return new ParseException(string.Format(CultureInfo.CurrentCulture, format, args), pos); + } + } + + internal static class TokenIdExtensions + { + internal static bool IsEqualityOperator(this TokenId tokenId) + { + return tokenId is TokenId.Equal or TokenId.DoubleEqual or TokenId.ExclamationEqual or TokenId.LessGreater; + } + + internal static bool IsComparisonOperator(this TokenId tokenId) + { + return tokenId is TokenId.Equal or TokenId.DoubleEqual or TokenId.ExclamationEqual or TokenId.LessGreater or TokenId.GreaterThan or TokenId.GreaterThanEqual or TokenId.LessThan or TokenId.LessThanEqual; + } + } + + /// + /// Parse a Double and Single Quoted string. + /// Some parts of the code is based on https://github.com/zzzprojects/Eval-Expression.NET + /// + internal static class StringParser + { + private const string TwoDoubleQuotes = "\"\""; + private const string SingleDoubleQuote = "\""; + + internal static string ParseStringAndUnescape(string s, int pos = default) + { + if (s == null || s.Length < 2) + { + throw new ParseException(string.Format(CultureInfo.CurrentCulture, "InvalidStringLength", s, 2), pos); + } + + if (s[0] != '"' && s[0] != '\'') + { + throw new ParseException(string.Format(CultureInfo.CurrentCulture, "InvalidStringQuoteCharacter"), pos); + } + + char quote = s[0]; // This can be single or a double quote + if (s.Last() != quote) + { + throw new ParseException(string.Format(CultureInfo.CurrentCulture, "UnexpectedUnclosedString", s.Length, s), pos); + } + + try + { + return Regex.Unescape(s.Substring(1, s.Length - 2)); + } + catch (Exception ex) + { + throw new ParseException(ex.Message, pos, ex); + } + } + + internal static string ParseStringAndUnescapeTwoDoubleQuotesByASingleDoubleQuote(string input, int position = default) + { + return ReplaceTwoDoubleQuotesByASingleDoubleQuote(ParseStringAndUnescape(input, position), position); + } + + private static string ReplaceTwoDoubleQuotesByASingleDoubleQuote(string input, int position) + { + try + { + return Regex.Replace(input, TwoDoubleQuotes, SingleDoubleQuote); + } + catch (Exception ex) + { + throw new ParseException(ex.Message, position, ex); + } + } + } +} diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditor.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditor.cs index 55e80cf..308e8b1 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditor.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditor.cs @@ -15,29 +15,53 @@ public class ExpressionScriptEditor : RichTextEditor { static readonly bool IsRunningOnMono = Type.GetType("Mono.Runtime") != null; + /// + public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) + { + if (provider != null && !IsRunningOnMono) + { + var scintillaEditor = new ScintillaExpressionScriptEditor(this); + return scintillaEditor.EditValue(context, provider, value); + } + + return base.EditValue(context, provider, value); + } + } + + internal class ScintillaExpressionScriptEditor : DataSourceTypeEditor + { + /// + /// Initializes a new instance of the class. + /// + public ScintillaExpressionScriptEditor(UITypeEditor baseEditor) + : base(DataSource.Input) + { + BaseEditor = baseEditor; + } + + private UITypeEditor BaseEditor { get; } + /// public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) { return UITypeEditorEditStyle.Modal; } - /// public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { - if (provider != null && !IsRunningOnMono) + var workflowBuilder = (WorkflowBuilder)provider.GetService(typeof(WorkflowBuilder)); + var editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); + if (editorService != null) { - var editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); - if (editorService != null) - { - using var editorDialog = new ExpressionScriptEditorDialog(); - editorDialog.Script = (string)value; - return editorService.ShowDialog(editorDialog) == DialogResult.OK - ? editorDialog.Script - : value; - } + using var editorDialog = new ExpressionScriptEditorDialog(); + editorDialog.Script = (string)value; + editorDialog.ItType = GetDataSource(context, provider)?.ObservableType; + return editorService.ShowDialog(editorDialog) == DialogResult.OK + ? editorDialog.Script + : value; } - return base.EditValue(context, provider, value); + return BaseEditor.EditValue(context, provider, value); } } } diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.Designer.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.Designer.cs index 948b613..723b2b3 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.Designer.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.Designer.cs @@ -45,7 +45,7 @@ private void InitializeComponent() this.scintilla.TabWidth = 2; this.scintilla.UseTabs = false; this.scintilla.WrapMode = ScintillaNET.WrapMode.Word; - this.scintilla.KeyDown += new System.Windows.Forms.KeyEventHandler(this.scintilla_KeyDown); + this.scintilla.CharAdded += new System.EventHandler(this.scintilla_CharAdded); this.scintilla.TextChanged += new System.EventHandler(this.scintilla_TextChanged); // // okButton diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs index 22f1022..2e91111 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs @@ -1,12 +1,20 @@ -using ScintillaNET; -using System; +using System; +using System.Collections.Generic; using System.Drawing; +using System.Linq; +using System.Linq.Dynamic.Core.Exceptions; +using System.Reflection; +using System.Text; using System.Windows.Forms; +using Bonsai.Scripting.Expressions.Design.Properties; +using ScintillaNET; namespace Bonsai.Scripting.Expressions.Design { internal partial class ExpressionScriptEditorDialog : Form { + readonly StringBuilder autoCompleteList = new(); + public ExpressionScriptEditorDialog() { InitializeComponent(); @@ -28,8 +36,17 @@ public ExpressionScriptEditorDialog() var types = "Object Boolean Char String SByte Byte Int16 UInt16 Int32 UInt32 Int64 UInt64 Single Double Decimal DateTime DateTimeOffset TimeSpan Guid Math Convert"; scintilla.SetKeywords(0, "it iif new outerIt as true false null"); scintilla.SetKeywords(1, string.Join(" ", types, types.ToLowerInvariant())); + + scintilla.AutoCSeparator = ';'; + scintilla.AutoCTypeSeparator = '?'; + scintilla.AutoCDropRestOfWord = true; + scintilla.RegisterRgbaImage(0, Resources.FieldIcon); + scintilla.RegisterRgbaImage(1, Resources.PropertyIcon); + scintilla.RegisterRgbaImage(2, Resources.MethodIcon); } + public Type ItType { get; set; } + public string Script { get; set; } protected override void OnLoad(EventArgs e) @@ -47,20 +64,33 @@ protected override void OnLoad(EventArgs e) protected override void OnKeyDown(KeyEventArgs e) { - if (e.KeyCode == Keys.Escape && !e.Handled) + if (e.KeyData == Keys.Escape && !e.Handled) { Close(); e.Handled = true; } + if (e.Modifiers == Keys.Control && e.KeyCode == Keys.Enter) + { + okButton.PerformClick(); + } + + if (e.Modifiers == Keys.Control && e.KeyCode == Keys.Space && + TryGetAutoCompleteList(ItType, out string list, out int lenEntered)) + { + scintilla.AutoCShow(lenEntered, list); + e.SuppressKeyPress = true; + } + base.OnKeyDown(e); } - private void scintilla_KeyDown(object sender, KeyEventArgs e) + private void scintilla_CharAdded(object sender, CharAddedEventArgs e) { - if (e.KeyCode == Keys.Enter && e.Modifiers == Keys.Control) + autoCompleteList.Clear(); + if (e.Char == '.' && TryGetAutoCompleteList(ItType, out string list, out int lenEntered)) { - okButton.PerformClick(); + scintilla.AutoCShow(lenEntered, list); } } @@ -68,5 +98,81 @@ private void scintilla_TextChanged(object sender, EventArgs e) { Script = scintilla.Text; } + + private bool TryGetAutoCompleteList(Type itType, out string list, out int lenEntered) + { + if (itType is not null) + { + try + { + var currentPos = scintilla.CurrentPosition; + var config = ParsingConfigHelper.CreateParsingConfig(itType); + var wordStartPos = scintilla.WordStartPosition(currentPos, true); + scintilla.CurrentPosition = wordStartPos; + lenEntered = currentPos - wordStartPos; + var analyzer = new CaretExpressionAnalyzer(config, scintilla.Text, wordStartPos - 1); + + autoCompleteList.Clear(); + var bindingFlags = BindingFlags.Public | BindingFlags.Instance; + if (selectedType is null) + { + AppendMember("it", -1, autoCompleteList); + selectedType = itType; + } + + if (!selectedType.IsEnum) + AppendFields(selectedType, bindingFlags, autoCompleteList); + AppendProperties(selectedType, bindingFlags, autoCompleteList); + AppendMethods(selectedType, bindingFlags, autoCompleteList); + list = autoCompleteList.ToString(); + return true; + } + catch (ParseException) { } + } + + lenEntered = default; + list = default; + return false; + } + + private void AppendFields(Type type, BindingFlags bindingFlags, StringBuilder sb) + { + foreach (var field in type.GetFields(bindingFlags) + .OrderBy(f => f.Name)) + { + AppendMember(field.Name, 0, sb); + } + } + + private void AppendProperties(Type type, BindingFlags bindingFlags, StringBuilder sb) + { + foreach (var property in type.GetProperties(bindingFlags) + .Except(type.GetDefaultMembers()) + .OrderBy(p => p.Name)) + { + AppendMember(property.Name, 1, sb); + } + } + + private void AppendMethods(Type type, BindingFlags bindingFlags, StringBuilder sb) + { + var nameSet = new HashSet(); + foreach (var method in type.GetMethods(bindingFlags) + .OrderBy(m => m.Name)) + { + if (!method.IsSpecialName && nameSet.Add(method.Name)) + AppendMember(method.Name, 2, sb); + } + } + + private void AppendMember(string name, int type, StringBuilder sb) + { + if (sb.Length > 0) + sb.Append(scintilla.AutoCSeparator); + + sb.Append(name); + sb.Append(scintilla.AutoCTypeSeparator); + sb.Append(type); + } } } diff --git a/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.Designer.cs b/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.Designer.cs new file mode 100644 index 0000000..711ba05 --- /dev/null +++ b/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.Designer.cs @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Bonsai.Scripting.Expressions.Design.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Bonsai.Scripting.Expressions.Design.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap FieldIcon { + get { + object obj = ResourceManager.GetObject("FieldIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap MethodIcon { + get { + object obj = ResourceManager.GetObject("MethodIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap PropertyIcon { + get { + object obj = ResourceManager.GetObject("PropertyIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.resx b/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.resx new file mode 100644 index 0000000..73b7ee6 --- /dev/null +++ b/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAACbSURBVDhPxZLBDYQgFEQpxVrogpu9aB9bBNXoHraEdUPg + HzFDQsJO/KJ72UmGfNT3PIAxJxGRMaWUUcz8Xk0FvffZOVeKuSticJrmvC7PUsyqSAO39+erLAohvAyW + HsgCa20pfmqw1AeagEFVwCIN7Aqu9n+C9jiLoJ4Cf8htwRjjQ0QG3IEBmzPRIcg5El0COa3oFsgB9BN4 + NztZNuzE+aYHsgAAAABJRU5ErkJggg== + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAADNSURBVDhPY2AYVOD79+8KX79+3f/t27f/IBrER1eDE3z9 + +rXh7csP/5e07v2fqjsBTIP4IHF0tSjg+/fvDt++fTt/cvu1/+Vuc//PLNvy//71p2AaxAeJg+RB6tD1 + ggHIuZNyNvwvc53z/+z+m/8/vP8IxyA+SBwkD1KHrhcMQBIgJ3fErcBqAEgcJE/QgOePX/9f3XcQxQsg + PkicKANgtl47fR/MB9EwMZIMgGlA5+M1AD0QYQYQFYjYohFkANHRCANkJyRkQFFSHjAAAM/jZxD0N56g + AAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAADmSURBVDhPtZIhDoQwEEU5wh5lj7DBcwcEB1iNWoXHk6ys + ROBZjecAOASihpnKkt+kBIaSzW7CT0a0mfcZ5jeKrpAx5kFEH2a2vnCe5/kuew8yxqTTNNmiKGwcx66S + JLG4gxHMJbNKa30bx1FnWbaD+75fJ2FmjT7JOhHRUyl1CjdNY7uuwxSpZJ2Yuc7zPGgAGHdVVWEfL8k6 + SQNvAsifvxqUZbkzkNW2bfgXiOjtxzwrLDe4xBCM0bdnxIqEDm/hDBbRoeoDDOGBbL/mYRjL3qCGYfgf + htCMx4F4foa9EAuyDcZztRYsmlywkj8K+QAAAABJRU5ErkJggg== + + + \ No newline at end of file From 97c424f60bd3677147e7ad856f012205d37d2bc6 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 25 Sep 2025 14:29:18 +0100 Subject: [PATCH 2/9] Add support for type auto completion --- .../CaretExpressionAnalyzer.cs | 8 +- .../ExpressionScriptEditorDialog.cs | 92 +++++++++++++++---- .../Properties/Resources.Designer.cs | 10 ++ .../Properties/Resources.resx | 40 ++++---- 4 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs index dbe4aa1..ec865b4 100644 --- a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs +++ b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs @@ -33,17 +33,13 @@ public CaretExpressionAnalyzer(ParsingConfig config, string text, int position) _textParser = new TextParser(_parsingConfig, _text); } - public LambdaExpression? Parse(Type? itType = null) + public string GetCaretExpression(Type? itType = null) { _startPos = 0; try { ParseConditionalOperator(); } catch (ParseException) { } - var text = _text.Substring(_startPos); - if (string.IsNullOrEmpty(text)) - return null; - - return DynamicExpressionParser.ParseLambda(_parsingConfig, itType, null, text); + return _text.Substring(_startPos); } // out keyword diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs index 2e91111..6138d92 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs @@ -1,19 +1,44 @@ -using System; +using Bonsai.Scripting.Expressions.Design.Properties; +using ScintillaNET; +using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core.Exceptions; using System.Reflection; using System.Text; using System.Windows.Forms; -using Bonsai.Scripting.Expressions.Design.Properties; -using ScintillaNET; namespace Bonsai.Scripting.Expressions.Design { internal partial class ExpressionScriptEditorDialog : Form { readonly StringBuilder autoCompleteList = new(); + static readonly Type[] defaultTypes = new[] + { + typeof(Object), + typeof(Boolean), + typeof(Char), + typeof(String), + typeof(SByte), + typeof(Byte), + typeof(Int16), + typeof(UInt16), + typeof(Int32), + typeof(UInt32), + typeof(Int64), + typeof(UInt64), + typeof(Single), + typeof(Double), + typeof(Decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(Guid), + typeof(Math), + typeof(Convert) + }; public ExpressionScriptEditorDialog() { @@ -33,7 +58,7 @@ public ExpressionScriptEditorDialog() scintilla.Styles[Style.Cpp.Word2].ForeColor = ColorTranslator.FromHtml("#2b91af"); scintilla.Lexer = Lexer.Cpp; - var types = "Object Boolean Char String SByte Byte Int16 UInt16 Int32 UInt32 Int64 UInt64 Single Double Decimal DateTime DateTimeOffset TimeSpan Guid Math Convert"; + var types = string.Join(" ", defaultTypes.Select(type => type.Name)); scintilla.SetKeywords(0, "it iif new outerIt as true false null"); scintilla.SetKeywords(1, string.Join(" ", types, types.ToLowerInvariant())); @@ -43,6 +68,7 @@ public ExpressionScriptEditorDialog() scintilla.RegisterRgbaImage(0, Resources.FieldIcon); scintilla.RegisterRgbaImage(1, Resources.PropertyIcon); scintilla.RegisterRgbaImage(2, Resources.MethodIcon); + scintilla.RegisterRgbaImage(3, Resources.TypeIcon); } public Type ItType { get; set; } @@ -111,21 +137,44 @@ private bool TryGetAutoCompleteList(Type itType, out string list, out int lenEnt scintilla.CurrentPosition = wordStartPos; lenEntered = currentPos - wordStartPos; var analyzer = new CaretExpressionAnalyzer(config, scintilla.Text, wordStartPos - 1); - - autoCompleteList.Clear(); - var bindingFlags = BindingFlags.Public | BindingFlags.Instance; - if (selectedType is null) + var text = analyzer.GetCaretExpression(itType); + try { - AppendMember("it", -1, autoCompleteList); - selectedType = itType; + var selectedType = !string.IsNullOrEmpty(text) + ? DynamicExpressionParser.ParseLambda(config, itType, null, text).ReturnType + : null; + + autoCompleteList.Clear(); + var bindingFlags = BindingFlags.Public | BindingFlags.Instance; + if (selectedType is null) + { + AppendMember("it", -1, autoCompleteList); + selectedType = itType; + } + + if (!selectedType.IsEnum) + AppendFields(selectedType, bindingFlags, autoCompleteList); + AppendProperties(selectedType, bindingFlags, autoCompleteList); + AppendMethods(selectedType, bindingFlags, autoCompleteList); + AppendTypes(itType, autoCompleteList); + list = autoCompleteList.ToString(); + return true; + } + catch (ParseException) + { + var matchingType = defaultTypes.FirstOrDefault( + type => text.Equals(type.Name, StringComparison.OrdinalIgnoreCase)); + if (matchingType is not null) + { + autoCompleteList.Clear(); + var bindingFlags = BindingFlags.Public | BindingFlags.Static; + AppendFields(matchingType, bindingFlags, autoCompleteList); + AppendProperties(matchingType, bindingFlags, autoCompleteList); + AppendMethods(matchingType, bindingFlags, autoCompleteList); + list = autoCompleteList.ToString(); + return true; + } } - - if (!selectedType.IsEnum) - AppendFields(selectedType, bindingFlags, autoCompleteList); - AppendProperties(selectedType, bindingFlags, autoCompleteList); - AppendMethods(selectedType, bindingFlags, autoCompleteList); - list = autoCompleteList.ToString(); - return true; } catch (ParseException) { } } @@ -165,6 +214,15 @@ private void AppendMethods(Type type, BindingFlags bindingFlags, StringBuilder s } } + private void AppendTypes(Type itType, StringBuilder sb) + { + foreach (var type in defaultTypes.Append(itType) + .OrderBy(t => t.Name)) + { + AppendMember(type.Name, 3, sb); + } + } + private void AppendMember(string name, int type, StringBuilder sb) { if (sb.Length > 0) diff --git a/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.Designer.cs b/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.Designer.cs index 711ba05..aa65bea 100644 --- a/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.Designer.cs +++ b/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.Designer.cs @@ -89,5 +89,15 @@ internal static System.Drawing.Bitmap PropertyIcon { return ((System.Drawing.Bitmap)(obj)); } } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap TypeIcon { + get { + object obj = ResourceManager.GetObject("TypeIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } } } diff --git a/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.resx b/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.resx index 73b7ee6..b675004 100644 --- a/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.resx +++ b/src/Bonsai.Scripting.Expressions.Design/Properties/Resources.resx @@ -120,31 +120,39 @@ - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAACbSURBVDhPxZLBDYQgFEQpxVrogpu9aB9bBNXoHraEdUPg - HzFDQsJO/KJ72UmGfNT3PIAxJxGRMaWUUcz8Xk0FvffZOVeKuSticJrmvC7PUsyqSAO39+erLAohvAyW - HsgCa20pfmqw1AeagEFVwCIN7Aqu9n+C9jiLoJ4Cf8htwRjjQ0QG3IEBmzPRIcg5El0COa3oFsgB9BN4 - NztZNuzE+aYHsgAAAABJRU5ErkJggg== + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wQAADsEBuJFr7QAAAJtJREFUOE/FksENhCAURCnFWuiCm71oH1sE1egetoR1Q+AfMUNCwk78onvZSYZ8 + 1Pc8gDEnEZExpZRRzPxeTQW999k5V4q5K2Jwmua8Ls9SzKpIA7f356ssCiG8DJYeyAJrbSl+arDUB5qA + QVXAIg3sCq72f4L2OIugngJ/yG3BGONDRAbcgQGbM9EhyDkSXQI5regWyAH0E3g3O1k27MT5pgeyAAAA + AElFTkSuQmCC - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAADNSURBVDhPY2AYVOD79+8KX79+3f/t27f/IBrER1eDE3z9 - +rXh7csP/5e07v2fqjsBTIP4IHF0tSjg+/fvDt++fTt/cvu1/+Vuc//PLNvy//71p2AaxAeJg+RB6tD1 - ggHIuZNyNvwvc53z/+z+m/8/vP8IxyA+SBwkD1KHrhcMQBIgJ3fErcBqAEgcJE/QgOePX/9f3XcQxQsg - PkicKANgtl47fR/MB9EwMZIMgGlA5+M1AD0QYQYQFYjYohFkANHRCANkJyRkQFFSHjAAAM/jZxD0N56g - AAAAAElFTkSuQmCC + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wQAADsEBuJFr7QAAAM1JREFUOE9jYBhU4Pv37wpfv37d/+3bt/8gGsRHV4MTfP36teHtyw//l7Tu/Z+q + OwFMg/ggcXS1KOD79+8O3759O39y+7X/5W5z/88s2/L//vWnYBrEB4mD5EHq0PWCAci5k3I2/C9znfP/ + 7P6b/z+8/wjHID5IHCQPUoeuFwxAEiAnd8StwGoASBwkT9CA549f/1/ddxDFCyA+SJwoA2C2Xjt9H8wH + 0TAxkgyAaUDn4zUAPRBhBhAViNiiEWQA0dEIA2QnJGRAUVIeMAAAz+NnEPQ3nqAAAAAASUVORK5CYII= + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wQAADsEBuJFr7QAAAOZJREFUOE+1kiEOhDAQRTnCHmWPsMFzBwQHWI1ahceTrKxE4FmN5wA4BKKGmcqS + 36QEhpLNbsJPRrSZ9xnmN4qukDHmQUQfZra+cJ7n+S57DzLGpNM02aIobBzHrpIksbiDEcwls0prfRvH + UWdZtoP7vl8nYWaNPsk6EdFTKXUKN01ju67DFKlknZi5zvM8aAAYd1VVYR8vyTpJA28CyJ+/GpRluTOQ + 1bZt+BeI6O3HPCssN7jEEIzRt2fEioQOb+EMFtGh6gMM4YFsv+ZhGMveoIZh+B+G0IzHgXh+hr0QC7IN + xnO1FiyaXLCSPwr5AAAAAElFTkSuQmCC + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAADmSURBVDhPtZIhDoQwEEU5wh5lj7DBcwcEB1iNWoXHk6ys - ROBZjecAOASihpnKkt+kBIaSzW7CT0a0mfcZ5jeKrpAx5kFEH2a2vnCe5/kuew8yxqTTNNmiKGwcx66S - JLG4gxHMJbNKa30bx1FnWbaD+75fJ2FmjT7JOhHRUyl1CjdNY7uuwxSpZJ2Yuc7zPGgAGHdVVWEfL8k6 - SQNvAsifvxqUZbkzkNW2bfgXiOjtxzwrLDe4xBCM0bdnxIqEDm/hDBbRoeoDDOGBbL/mYRjL3qCGYfgf - htCMx4F4foa9EAuyDcZztRYsmlywkj8K+QAAAABJRU5ErkJggg== + YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAACWSURBVDhPlZHRDYAwCEQ7h1M4kgs4ggs5hkO4hj9N+1mD + CQZOwPYSYk29hxwpgXLOcynloqIz3odi87mvjWoIIs3HNj3VDbHMHoSeChiZEWDm02FW9cknAkTFEHME + 2ZneubMJwBzk73kADFZB5IUEwPyt1rq8ZgmRVASYIUZigFUqA0/YeRhACrb0PwLL2xJ+F8raEukGVF3/ + 7l5SVIkAAAAASUVORK5CYII= \ No newline at end of file From 7afafc4d1e8f0bf64c204343c7bb11a22d2ff62c Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 25 Sep 2025 15:06:48 +0100 Subject: [PATCH 3/9] Add stack to track primary expression --- .../CaretExpressionAnalyzer.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs index ec865b4..29d6e1c 100644 --- a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs +++ b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs @@ -23,23 +23,24 @@ internal class CaretExpressionAnalyzer readonly ParsingConfig _parsingConfig; readonly TextParser _textParser; + readonly Stack _primaryStack; readonly string _text; - int _startPos; public CaretExpressionAnalyzer(ParsingConfig config, string text, int position) { _parsingConfig = config; _text = position > 0 ? text.Substring(0, position) : string.Empty; _textParser = new TextParser(_parsingConfig, _text); + _primaryStack = new Stack(); } public string GetCaretExpression(Type? itType = null) { - _startPos = 0; + _primaryStack.Clear(); try { ParseConditionalOperator(); } catch (ParseException) { } - return _text.Substring(_startPos); + return _text.Substring(_primaryStack.FirstOrDefault()); } // out keyword @@ -267,6 +268,7 @@ private void ParseUnary() // primary elements private void ParsePrimary() { + _primaryStack.Push(_textParser.CurrentToken.Pos); ParsePrimaryStart(); while (true) @@ -289,11 +291,13 @@ private void ParsePrimary() break; } } + + if (_textParser.CurrentToken.Id != TokenId.End) + _primaryStack.Pop(); } private void ParsePrimaryStart() { - _startPos = _textParser.CurrentToken.Pos; switch (_textParser.CurrentToken.Id) { case TokenId.Identifier: From 00b93ed5b6d6aee145b694b36fdc1318e61b5865 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 25 Sep 2025 15:52:41 +0100 Subject: [PATCH 4/9] Move class identifier parsing inside analyzer --- .../CaretExpressionAnalyzer.cs | 42 ++++++++- .../ExpressionScriptEditorDialog.cs | 85 +++++-------------- 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs index 29d6e1c..53cf811 100644 --- a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs +++ b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs @@ -20,6 +20,30 @@ internal class CaretExpressionAnalyzer { private static readonly string[] OutKeywords = ["out", "$out"]; private const string DiscardVariable = "_"; + internal static readonly Type[] DefaultTypes = new[] + { + typeof(Object), + typeof(Boolean), + typeof(Char), + typeof(String), + typeof(SByte), + typeof(Byte), + typeof(Int16), + typeof(UInt16), + typeof(Int32), + typeof(UInt32), + typeof(Int64), + typeof(UInt64), + typeof(Single), + typeof(Double), + typeof(Decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(Guid), + typeof(Math), + typeof(Convert) + }; readonly ParsingConfig _parsingConfig; readonly TextParser _textParser; @@ -34,13 +58,27 @@ public CaretExpressionAnalyzer(ParsingConfig config, string text, int position) _primaryStack = new Stack(); } - public string GetCaretExpression(Type? itType = null) + public Type ParseExpressionType(Type itType, out bool isClassIdentifier) { _primaryStack.Clear(); try { ParseConditionalOperator(); } catch (ParseException) { } - return _text.Substring(_primaryStack.FirstOrDefault()); + isClassIdentifier = false; + var primaryText = _text.Substring(_primaryStack.FirstOrDefault()); + try + { + return !string.IsNullOrEmpty(primaryText) + ? DynamicExpressionParser.ParseLambda(_parsingConfig, itType, null, primaryText).ReturnType + : null; + } + catch (ParseException pex) + { + isClassIdentifier = true; + return DefaultTypes.Append(itType).FirstOrDefault( + type => primaryText.Equals(type.Name, StringComparison.OrdinalIgnoreCase)) + ?? throw pex; + } } // out keyword diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs index 6138d92..bfcdbe9 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs @@ -15,30 +15,6 @@ namespace Bonsai.Scripting.Expressions.Design internal partial class ExpressionScriptEditorDialog : Form { readonly StringBuilder autoCompleteList = new(); - static readonly Type[] defaultTypes = new[] - { - typeof(Object), - typeof(Boolean), - typeof(Char), - typeof(String), - typeof(SByte), - typeof(Byte), - typeof(Int16), - typeof(UInt16), - typeof(Int32), - typeof(UInt32), - typeof(Int64), - typeof(UInt64), - typeof(Single), - typeof(Double), - typeof(Decimal), - typeof(DateTime), - typeof(DateTimeOffset), - typeof(TimeSpan), - typeof(Guid), - typeof(Math), - typeof(Convert) - }; public ExpressionScriptEditorDialog() { @@ -58,7 +34,7 @@ public ExpressionScriptEditorDialog() scintilla.Styles[Style.Cpp.Word2].ForeColor = ColorTranslator.FromHtml("#2b91af"); scintilla.Lexer = Lexer.Cpp; - var types = string.Join(" ", defaultTypes.Select(type => type.Name)); + var types = string.Join(" ", CaretExpressionAnalyzer.DefaultTypes.Select(type => type.Name)); scintilla.SetKeywords(0, "it iif new outerIt as true false null"); scintilla.SetKeywords(1, string.Join(" ", types, types.ToLowerInvariant())); @@ -137,44 +113,27 @@ private bool TryGetAutoCompleteList(Type itType, out string list, out int lenEnt scintilla.CurrentPosition = wordStartPos; lenEntered = currentPos - wordStartPos; var analyzer = new CaretExpressionAnalyzer(config, scintilla.Text, wordStartPos - 1); - var text = analyzer.GetCaretExpression(itType); - try - { - var selectedType = !string.IsNullOrEmpty(text) - ? DynamicExpressionParser.ParseLambda(config, itType, null, text).ReturnType - : null; - - autoCompleteList.Clear(); - var bindingFlags = BindingFlags.Public | BindingFlags.Instance; - if (selectedType is null) - { - AppendMember("it", -1, autoCompleteList); - selectedType = itType; - } - - if (!selectedType.IsEnum) - AppendFields(selectedType, bindingFlags, autoCompleteList); - AppendProperties(selectedType, bindingFlags, autoCompleteList); - AppendMethods(selectedType, bindingFlags, autoCompleteList); - AppendTypes(itType, autoCompleteList); - list = autoCompleteList.ToString(); - return true; - } - catch (ParseException) + var primaryType = analyzer.ParseExpressionType(itType, out bool isClassIdentifier); + + autoCompleteList.Clear(); + var appendTypes = false; + var bindingFlags = BindingFlags.Public; + bindingFlags |= isClassIdentifier ? BindingFlags.Static : BindingFlags.Instance; + if (primaryType is null && !isClassIdentifier) { - var matchingType = defaultTypes.FirstOrDefault( - type => text.Equals(type.Name, StringComparison.OrdinalIgnoreCase)); - if (matchingType is not null) - { - autoCompleteList.Clear(); - var bindingFlags = BindingFlags.Public | BindingFlags.Static; - AppendFields(matchingType, bindingFlags, autoCompleteList); - AppendProperties(matchingType, bindingFlags, autoCompleteList); - AppendMethods(matchingType, bindingFlags, autoCompleteList); - list = autoCompleteList.ToString(); - return true; - } + AppendMember("it", -1, autoCompleteList); + primaryType = itType; + appendTypes = true; } + + if (!primaryType.IsEnum) + AppendFields(primaryType, bindingFlags, autoCompleteList); + AppendProperties(primaryType, bindingFlags, autoCompleteList); + AppendMethods(primaryType, bindingFlags, autoCompleteList); + if (appendTypes) + AppendTypes(itType, autoCompleteList); + list = autoCompleteList.ToString(); + return true; } catch (ParseException) { } } @@ -216,8 +175,8 @@ private void AppendMethods(Type type, BindingFlags bindingFlags, StringBuilder s private void AppendTypes(Type itType, StringBuilder sb) { - foreach (var type in defaultTypes.Append(itType) - .OrderBy(t => t.Name)) + foreach (var type in CaretExpressionAnalyzer.DefaultTypes.Append(itType) + .OrderBy(t => t.Name)) { AppendMember(type.Name, 3, sb); } From 50e86bfd20fa152df3cd0af28a833d0e018da715 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 25 Sep 2025 15:59:57 +0100 Subject: [PATCH 5/9] Add support for enum names and static constants --- .../ExpressionScriptEditorDialog.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs index bfcdbe9..b20c74b 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs @@ -118,7 +118,10 @@ private bool TryGetAutoCompleteList(Type itType, out string list, out int lenEnt autoCompleteList.Clear(); var appendTypes = false; var bindingFlags = BindingFlags.Public; - bindingFlags |= isClassIdentifier ? BindingFlags.Static : BindingFlags.Instance; + bindingFlags |= isClassIdentifier + ? BindingFlags.Static | BindingFlags.FlattenHierarchy + : BindingFlags.Instance; + if (primaryType is null && !isClassIdentifier) { AppendMember("it", -1, autoCompleteList); @@ -128,6 +131,9 @@ private bool TryGetAutoCompleteList(Type itType, out string list, out int lenEnt if (!primaryType.IsEnum) AppendFields(primaryType, bindingFlags, autoCompleteList); + else if (isClassIdentifier) + AppendEnumNames(primaryType, autoCompleteList); + AppendProperties(primaryType, bindingFlags, autoCompleteList); AppendMethods(primaryType, bindingFlags, autoCompleteList); if (appendTypes) @@ -152,6 +158,14 @@ private void AppendFields(Type type, BindingFlags bindingFlags, StringBuilder sb } } + private void AppendEnumNames(Type type, StringBuilder sb) + { + foreach (var name in Enum.GetNames(type).OrderBy(f => f)) + { + AppendMember(name, 0, sb); + } + } + private void AppendProperties(Type type, BindingFlags bindingFlags, StringBuilder sb) { foreach (var property in type.GetProperties(bindingFlags) From cdeea78a22549dae168ddd87d941f6c87015a1dc Mon Sep 17 00:00:00 2001 From: glopesdev Date: Thu, 2 Oct 2025 14:59:39 +0100 Subject: [PATCH 6/9] Trim last dot character if present This avoids having special logic to distinguish between completing an expression after typing a character or pressing the shortcut. --- .../CaretExpressionAnalyzer.cs | 4 ++++ .../ExpressionScriptEditorDialog.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs index 53cf811..2224f2c 100644 --- a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs +++ b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs @@ -20,6 +20,7 @@ internal class CaretExpressionAnalyzer { private static readonly string[] OutKeywords = ["out", "$out"]; private const string DiscardVariable = "_"; + private const char DotCharacter = '.'; internal static readonly Type[] DefaultTypes = new[] { typeof(Object), @@ -52,6 +53,9 @@ internal class CaretExpressionAnalyzer public CaretExpressionAnalyzer(ParsingConfig config, string text, int position) { + if (!string.IsNullOrEmpty(text) && position > 0 && text[position - 1] == DotCharacter) + position = position - 1; + _parsingConfig = config; _text = position > 0 ? text.Substring(0, position) : string.Empty; _textParser = new TextParser(_parsingConfig, _text); diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs index b20c74b..6c914bd 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs @@ -112,7 +112,7 @@ private bool TryGetAutoCompleteList(Type itType, out string list, out int lenEnt var wordStartPos = scintilla.WordStartPosition(currentPos, true); scintilla.CurrentPosition = wordStartPos; lenEntered = currentPos - wordStartPos; - var analyzer = new CaretExpressionAnalyzer(config, scintilla.Text, wordStartPos - 1); + var analyzer = new CaretExpressionAnalyzer(config, scintilla.Text, wordStartPos); var primaryType = analyzer.ParseExpressionType(itType, out bool isClassIdentifier); autoCompleteList.Clear(); From ac2634e09f2811a3ed83a255aa862711a96c6f7e Mon Sep 17 00:00:00 2001 From: glopesdev Date: Fri, 3 Oct 2025 13:55:09 +0100 Subject: [PATCH 7/9] Allow auto completion from built-in type keywords --- .../CaretExpressionAnalyzer.cs | 62 +++++++++++++------ .../ExpressionScriptEditorDialog.cs | 8 +-- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs index 2224f2c..bd9772a 100644 --- a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs +++ b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs @@ -21,29 +21,50 @@ internal class CaretExpressionAnalyzer private static readonly string[] OutKeywords = ["out", "$out"]; private const string DiscardVariable = "_"; private const char DotCharacter = '.'; - internal static readonly Type[] DefaultTypes = new[] - { - typeof(Object), - typeof(Boolean), - typeof(Char), - typeof(String), - typeof(SByte), - typeof(Byte), - typeof(Int16), - typeof(UInt16), - typeof(Int32), - typeof(UInt32), - typeof(Int64), - typeof(UInt64), - typeof(Single), - typeof(Double), - typeof(Decimal), + internal static readonly Dictionary PredefinedTypeKeywords = new() + { + { "object", typeof(object) }, + { "bool", typeof(bool) }, + { "byte", typeof(byte) }, + { "char", typeof(char) }, + { "decimal", typeof(decimal) }, + { "double", typeof(double) }, + { "float", typeof(float) }, + { "int", typeof(int) }, + { "long", typeof(long) }, + { "sbyte", typeof(sbyte) }, + { "short", typeof(short) }, + { "string", typeof(string) }, + { "uint", typeof(uint) }, + { "ulong", typeof(ulong) }, + { "ushort", typeof(ushort) } + }; + + internal static readonly Type[] PredefinedTypes = new[] + { + typeof(object), + typeof(bool), + typeof(char), + typeof(string), + typeof(sbyte), + typeof(byte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), typeof(Guid), typeof(Math), - typeof(Convert) + typeof(Convert), + typeof(Uri), + typeof(Enum) }; readonly ParsingConfig _parsingConfig; @@ -79,8 +100,9 @@ public Type ParseExpressionType(Type itType, out bool isClassIdentifier) catch (ParseException pex) { isClassIdentifier = true; - return DefaultTypes.Append(itType).FirstOrDefault( - type => primaryText.Equals(type.Name, StringComparison.OrdinalIgnoreCase)) + return PredefinedTypeKeywords.TryGetValue(primaryText, out Type type) + ? type + : PredefinedTypes.Append(itType).FirstOrDefault(type => primaryText.Equals(type.Name)) ?? throw pex; } } diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs index 6c914bd..818b520 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs @@ -34,9 +34,9 @@ public ExpressionScriptEditorDialog() scintilla.Styles[Style.Cpp.Word2].ForeColor = ColorTranslator.FromHtml("#2b91af"); scintilla.Lexer = Lexer.Cpp; - var types = string.Join(" ", CaretExpressionAnalyzer.DefaultTypes.Select(type => type.Name)); - scintilla.SetKeywords(0, "it iif new outerIt as true false null"); - scintilla.SetKeywords(1, string.Join(" ", types, types.ToLowerInvariant())); + var typeKeywords = string.Join(" ", CaretExpressionAnalyzer.PredefinedTypeKeywords.Keys); + scintilla.SetKeywords(0, "it iif new outerIt as true false null " + typeKeywords); + scintilla.SetKeywords(1, string.Join(" ", CaretExpressionAnalyzer.PredefinedTypes.Select(type => type.Name))); scintilla.AutoCSeparator = ';'; scintilla.AutoCTypeSeparator = '?'; @@ -189,7 +189,7 @@ private void AppendMethods(Type type, BindingFlags bindingFlags, StringBuilder s private void AppendTypes(Type itType, StringBuilder sb) { - foreach (var type in CaretExpressionAnalyzer.DefaultTypes.Append(itType) + foreach (var type in CaretExpressionAnalyzer.PredefinedTypes.Append(itType) .OrderBy(t => t.Name)) { AppendMember(type.Name, 3, sb); From d6c6678651968e98675cedf0a03b1f4f69f0944f Mon Sep 17 00:00:00 2001 From: glopesdev Date: Sat, 4 Oct 2025 18:47:39 +0100 Subject: [PATCH 8/9] Avoid rethrowing exception --- .../CaretExpressionAnalyzer.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs index bd9772a..20ae504 100644 --- a/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs +++ b/src/Bonsai.Scripting.Expressions.Design/CaretExpressionAnalyzer.cs @@ -97,13 +97,15 @@ public Type ParseExpressionType(Type itType, out bool isClassIdentifier) ? DynamicExpressionParser.ParseLambda(_parsingConfig, itType, null, primaryText).ReturnType : null; } - catch (ParseException pex) + catch (ParseException) { isClassIdentifier = true; - return PredefinedTypeKeywords.TryGetValue(primaryText, out Type type) - ? type - : PredefinedTypes.Append(itType).FirstOrDefault(type => primaryText.Equals(type.Name)) - ?? throw pex; + if (PredefinedTypeKeywords.TryGetValue(primaryText, out Type keywordType)) + return keywordType; + else if (PredefinedTypes.Append(itType).FirstOrDefault(type => primaryText.Equals(type.Name)) is Type type) + return type; + else + throw; } } From 470aa5a47a5db6236a23bd9030522ee04d6e9456 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Tue, 7 Oct 2025 11:39:12 +0100 Subject: [PATCH 9/9] Add custom type syntax highlight --- .../ExpressionScriptEditor.cs | 4 ++-- .../ExpressionScriptEditorDialog.cs | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditor.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditor.cs index 308e8b1..7d151f3 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditor.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditor.cs @@ -53,9 +53,9 @@ public override object EditValue(ITypeDescriptorContext context, IServiceProvide var editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); if (editorService != null) { - using var editorDialog = new ExpressionScriptEditorDialog(); + var itType = GetDataSource(context, provider)?.ObservableType; + using var editorDialog = new ExpressionScriptEditorDialog(itType); editorDialog.Script = (string)value; - editorDialog.ItType = GetDataSource(context, provider)?.ObservableType; return editorService.ShowDialog(editorDialog) == DialogResult.OK ? editorDialog.Script : value; diff --git a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs index 818b520..e917df3 100644 --- a/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs +++ b/src/Bonsai.Scripting.Expressions.Design/ExpressionScriptEditorDialog.cs @@ -16,8 +16,9 @@ internal partial class ExpressionScriptEditorDialog : Form { readonly StringBuilder autoCompleteList = new(); - public ExpressionScriptEditorDialog() + public ExpressionScriptEditorDialog(Type? itType = null) { + ItType = itType; InitializeComponent(); scintilla.StyleResetDefault(); scintilla.Styles[Style.Default].Font = "Consolas"; @@ -35,8 +36,11 @@ public ExpressionScriptEditorDialog() scintilla.Lexer = Lexer.Cpp; var typeKeywords = string.Join(" ", CaretExpressionAnalyzer.PredefinedTypeKeywords.Keys); + var predefinedTypes = CaretExpressionAnalyzer.PredefinedTypes; scintilla.SetKeywords(0, "it iif new outerIt as true false null " + typeKeywords); - scintilla.SetKeywords(1, string.Join(" ", CaretExpressionAnalyzer.PredefinedTypes.Select(type => type.Name))); + scintilla.SetKeywords(1, string.Join(" ", Enumerable.Concat( + (itType is not null ? predefinedTypes.Append(itType) : predefinedTypes).Select(type => type.Name), + predefinedTypes.Select(type => type.Name.ToLowerInvariant())))); scintilla.AutoCSeparator = ';'; scintilla.AutoCTypeSeparator = '?'; @@ -47,7 +51,7 @@ public ExpressionScriptEditorDialog() scintilla.RegisterRgbaImage(3, Resources.TypeIcon); } - public Type ItType { get; set; } + public Type? ItType { get; } public string Script { get; set; } @@ -190,7 +194,7 @@ private void AppendMethods(Type type, BindingFlags bindingFlags, StringBuilder s private void AppendTypes(Type itType, StringBuilder sb) { foreach (var type in CaretExpressionAnalyzer.PredefinedTypes.Append(itType) - .OrderBy(t => t.Name)) + .OrderBy(t => t.Name)) { AppendMember(type.Name, 3, sb); }