diff --git a/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems b/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems index d216565f..e872b56b 100644 --- a/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems +++ b/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems @@ -41,6 +41,7 @@ + AvaloniaDesigner.xaml diff --git a/AvaloniaVS.Shared/IntelliSense/XamlCompletion.cs b/AvaloniaVS.Shared/IntelliSense/XamlCompletion.cs index fccda632..0b589636 100644 --- a/AvaloniaVS.Shared/IntelliSense/XamlCompletion.cs +++ b/AvaloniaVS.Shared/IntelliSense/XamlCompletion.cs @@ -21,7 +21,8 @@ public XamlCompletion(Completion completion) completion.InsertText, completion.Description, GetImage(completion.Kind), - completion.Kind.ToString()) + completion.Kind.ToString(), + suffix: string.IsNullOrWhiteSpace(completion.Suffix) ? string.Empty : $"({completion.Suffix})") { if (completion.RecommendedCursorOffset.HasValue) { @@ -29,6 +30,23 @@ public XamlCompletion(Completion completion) } Kind = completion.Kind; + DeleteTextOffset = completion.DeleteTextOffset; + } + + public int? DeleteTextOffset { get; } + + public override string InsertionText + { + get + { + if (HasFlag(Kind, CompletionKind.Name) && !string.IsNullOrEmpty(Suffix)) + { + return $"{Suffix.Substring(1,Suffix.Length-2)}#{base.InsertionText}"; + } + return base.InsertionText; + } + + set => base.InsertionText = value; } public int CursorOffset { get; } @@ -49,7 +67,6 @@ private static ImageMoniker GetImage(CompletionKind kind) { LoadImages(); } - if (HasFlag(kind, CompletionKind.DataProperty)) { return s_images[(int)CompletionKind.DataProperty]; @@ -62,13 +79,20 @@ private static ImageMoniker GetImage(CompletionKind kind) { return s_images[(int)CompletionKind.Enum]; } - - return s_images[(int)kind]; - - bool HasFlag(CompletionKind test, CompletionKind expected) + else if (HasFlag(kind, CompletionKind.Selector)) + { + return s_images[(int)CompletionKind.Enum]; + } + else if (HasFlag(kind, CompletionKind.Name)) { - return (test & expected) == expected; + return s_images[(int)CompletionKind.Class]; } + return s_images[(int)kind]; + } + + private static bool HasFlag(CompletionKind test, CompletionKind expected) + { + return (test & expected) == expected; } private static void LoadImages() @@ -90,6 +114,7 @@ private static void LoadImages() s_images[(int)CompletionKind.MarkupExtension] = KnownMonikers.Namespace; s_images[(int)CompletionKind.DataProperty] = KnownMonikers.DatabaseProperty; s_images[(int)CompletionKind.TargetTypeClass] = KnownMonikers.ClassPublic; + s_images[(int)CompletionKind.Selector] = KnownMonikers.Namespace; } } } diff --git a/AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs b/AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs index 220a1d81..4f76353d 100644 --- a/AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs +++ b/AvaloniaVS.Shared/IntelliSense/XamlCompletionCommandHandler.cs @@ -81,9 +81,7 @@ public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pv return VSConstants.S_OK; } } - var result = _nextCommandHandler.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); - if (HandleSessionStart(c)) { return VSConstants.S_OK; @@ -107,7 +105,7 @@ private bool HandleSessionStart(char c) { if (_session == null || _session.IsDismissed) { - if (TriggerCompletion() && c != '<' && c != '.' && c != ' ') + if (TriggerCompletion() && c != '<' && c != '.' && c != ' ' && c != '[' && c != '(' && c != '|' && c != '#' && c != '/') { _session?.Filter(); } @@ -127,7 +125,6 @@ private bool HandleSessionStart(char c) return true; } } - return false; } @@ -159,7 +156,7 @@ private bool HandleSessionCompletion(char c) // So we only trigger on ' ' or '\t', and swallow that so it doesn't get // inserted into the text buffer if (_session != null && !_session.IsDismissed) - { + { var text = line.Snapshot.GetText(start, end - start); if (text.Contains("xmlns")) @@ -190,14 +187,28 @@ private bool HandleSessionCompletion(char c) // Also adding '#' for Selectors - if (char.IsWhiteSpace(c) || c == '\'' || c == '"' || c == '=' || c == '>' || c == '.' || c == '#') + if (char.IsWhiteSpace(c) + || c == '\'' || c == '"' || c == '=' || c == '>' || c == '.' + || c == '#' || c == ')' || c == ']') { if (_session != null && !_session.IsDismissed && _session.SelectedCompletionSet.SelectionStatus.IsSelected) { var selected = _session.SelectedCompletionSet.SelectionStatus.Completion as XamlCompletion; + var bufferPos = _textView.Caret.Position.BufferPosition; + _session.Commit(); + + if (selected.DeleteTextOffset is int rof) + { + var newCursorPos = bufferPos.Add(rof); + SnapshotSpan deleteSpan = newCursorPos < bufferPos + ? new(newCursorPos, -rof) + : new(bufferPos, rof); + _textView.TextBuffer.Delete(deleteSpan); + } + if (selected?.CursorOffset > 0) { // Offset the cursor if necessary e.g. to place it within the quotation @@ -215,23 +226,24 @@ private bool HandleSessionCompletion(char c) var state = parser.State; bool skip = c != '>'; - if (state == XmlParser.ParserState.StartElement && + if (state == XmlParser.ParserState.StartElement && (c == '.' || c == ' ')) { // Don't swallow the '.' or ' ' if this is an Xml element, like - // Window.Resources. However do swallow tab + // Window.Resources. However do swallow tab skip = false; } - if (state == XmlParser.ParserState.AttributeValue || + if (state == XmlParser.ParserState.AttributeValue || state == XmlParser.ParserState.AfterAttributeValue) { + var isSelector = parser.AttributeName?.Equals("Selector") == true; if (char.IsWhiteSpace(c)) { // For most xml attributes, swallow the space upon completion // For selector, allow it to go into the buffer // Also if in a markupextention - skip = !(parser.AttributeName?.Equals("Selector") == true); + skip = !(isSelector && c != '\n' && c != '\t'); // If we're in a markup extension, only swallow the space if the // completion isn't on the Markup extension @@ -286,11 +298,14 @@ private bool HandleSessionCompletion(char c) skip = false; } + var lastInsertionChar = (selected.InsertionText?.Length ?? 0) > 0 + ? selected.InsertionText[selected.InsertionText.Length - 1] + : default; + // Cases like {Binding Path= result in {Binding Path== // as the completion includes the '=', if the entered char // is the same as the last char here, swallow the entered char - if (!skip && (selected.InsertionText?.Length > 0 && - selected.InsertionText[selected.InsertionText.Length - 1] == c)) + if (!skip && lastInsertionChar == c) { skip = true; @@ -300,6 +315,12 @@ private bool HandleSessionCompletion(char c) if (c == '=') TriggerCompletion(); } + else if (isSelector && lastInsertionChar is '=' or '.') + { + // Trigger Selector property Value Completation + if (c is not '=' or '.') + TriggerCompletion(); + } } else if (state != XmlParser.ParserState.StartElement) { @@ -319,7 +340,7 @@ private bool HandleSessionCompletion(char c) var parser = XmlParser.Parse(_textView.TextSnapshot.GetText().AsMemory(), 0, end); var state = parser.State; - if (state == XmlParser.ParserState.AttributeValue && + if (state == XmlParser.ParserState.AttributeValue && parser.AttributeName?.Equals("Selector") == true) { // Force new session to start to suggest pseudoclasses @@ -327,6 +348,17 @@ private bool HandleSessionCompletion(char c) return false; } } + else if (c == '(' && _session?.IsDismissed == false) + { + var parser = XmlParser.Parse(_textView.TextSnapshot.GetText().AsMemory(), 0, end); + var state = parser.State; + if ((state == XmlParser.ParserState.AttributeValue || state == XmlParser.ParserState.AfterAttributeValue) + && parser.AttributeName?.Equals("Selector") == true) + { + _session.Dismiss(); + return false; + } + } else if (c == '{' && (_session != null && !_session.IsDismissed)) { var parser = XmlParser.Parse(_textView.TextSnapshot.GetText().AsMemory(), 0, end); diff --git a/AvaloniaVS.Shared/TextViewExtensions.cs b/AvaloniaVS.Shared/TextViewExtensions.cs new file mode 100644 index 00000000..3d456e92 --- /dev/null +++ b/AvaloniaVS.Shared/TextViewExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text; +using System.Linq; + +namespace AvaloniaVS; + +internal static class TextViewExtensions +{ + public static NormalizedSnapshotSpanCollection GetSpanInView(this ITextView textView, SnapshotSpan span) +=> textView.BufferGraph.MapUpToSnapshot(span, SpanTrackingMode.EdgeInclusive, textView.TextSnapshot); + + public static void SetSelection( + this ITextView textView, VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint) + { + var isReversed = activePoint < anchorPoint; + var start = isReversed ? activePoint : anchorPoint; + var end = isReversed ? anchorPoint : activePoint; + SetSelection(textView, new SnapshotSpan(start.Position, end.Position), isReversed); + } + + public static void SetSelection( + this ITextView textView, SnapshotSpan span, bool isReversed = false) + { + var spanInView = textView.GetSpanInView(span).Single(); + textView.Selection.Select(spanInView, isReversed); + textView.Caret.MoveTo(isReversed ? spanInView.Start : spanInView.End); + } +} diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine.DnlibMetadataProvider/Wrappers.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine.DnlibMetadataProvider/Wrappers.cs index c2babf19..77068c99 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine.DnlibMetadataProvider/Wrappers.cs +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine.DnlibMetadataProvider/Wrappers.cs @@ -36,6 +36,9 @@ public IEnumerable InternalsVisibleTo public Stream GetManifestResourceStream(string name) => _asm.ManifestModule.Resources.FindEmbeddedResource(name).CreateReader().AsStream(); + public string PublicKey + => _asm.PublicKey.ToString(); + public override string ToString() => Name; } @@ -81,6 +84,7 @@ private TypeWrapper(TypeDef type) public bool IsGeneric => _type.HasGenericParameters; public bool IsAbstract => _type.IsAbstract && !_type.IsSealed; public bool IsInternal => _type.IsNotPublic && !_type.IsNestedPrivate; + public IEnumerable EnumValues { get @@ -119,6 +123,22 @@ public IEnumerable Pseudoclasses public override string ToString() => Name; public IEnumerable NestedTypes => _type.HasNestedTypes ? _type.NestedTypes.Select(t => new TypeWrapper(t)) : Array.Empty(); + + public IEnumerable<(ITypeInformation Type, string Name)> TemplateParts + { + get + { + var attributes = _type.CustomAttributes + .Where(a => a.TypeFullName.EndsWith("TemplatePartAttribute", StringComparison.OrdinalIgnoreCase) + && a.HasConstructorArguments); + foreach (var attr in attributes) + { + var name = attr.ConstructorArguments[0].Value.ToString()!; + ITypeInformation type = TypeWrapper.FromDef(((ClassSig)attr.ConstructorArguments[1].Value).TypeDef)!; + yield return (type, name); + } + } + } } internal class CustomAttributeWrapper : ICustomAttributeInformation @@ -150,11 +170,12 @@ public ConstructorArgumentWrapper(CAArgument ca) internal class PropertyWrapper : IPropertyInformation { private readonly PropertyDef _prop; - private readonly Func _isVisbleTo; + private readonly Func _isVisbleTo; public PropertyWrapper(PropertyDef prop) { Name = prop.Name; + var setMethod = prop.SetMethod; var getMethod = prop.GetMethod; @@ -169,7 +190,7 @@ public PropertyWrapper(PropertyDef prop) { type = getMethod.ReturnType; } - else if(setMethod is not null) + else if (setMethod is not null) { type = setMethod.Parameters[setMethod.IsStatic ? 0 : 1].Type; } @@ -180,7 +201,7 @@ public PropertyWrapper(PropertyDef prop) TypeFullName = type.FullName; QualifiedTypeFullName = type.AssemblyQualifiedName; - + _prop = prop; if (HasPublicGetter || HasPublicSetter) { @@ -188,36 +209,52 @@ public PropertyWrapper(PropertyDef prop) } else { - _isVisbleTo = static (property, targetAssemblyName) => + _isVisbleTo = static (property, targetAssembly) => { if (property.DeclaringType.DefinitionAssembly is AssemblyDef assembly) { - if (string.Equals(targetAssemblyName, assembly.GetFullNameWithPublicKeyToken(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(targetAssembly.AssemblyName, assembly.GetFullNameWithPublicKeyToken(), StringComparison.OrdinalIgnoreCase)) { return true; } - var nameParts = targetAssemblyName.Split(','); var enumerator = assembly.GetVisibleTo()?.GetEnumerator(); + var targetPublicKey = targetAssembly.PublicKey; + var targetName = targetAssembly.Name; while (enumerator?.MoveNext() == true) { - if (string.Equals(targetAssemblyName, enumerator.Current, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else + var current = enumerator.Current; + if (current.StartsWith(targetName, StringComparison.OrdinalIgnoreCase)) { - var attParts = enumerator.Current.Split(','); - var min = Math.Min(attParts.Length, nameParts.Length); - var i = 0; - for (; i < min && attParts[i] == nameParts[i]; i++) + if (!string.IsNullOrEmpty(targetPublicKey)) { - - } - if (i == min) - { - return true; + var startIndex = current.IndexOf("PublicKey", StringComparison.OrdinalIgnoreCase); + if (startIndex > -1) + { + startIndex += 9; + if (startIndex > current.Length) + { + return false; + } + while (startIndex < current.Length && current[startIndex] is ' ' or '=') + { + startIndex++; + } + + if (targetPublicKey.Length != current.Length - startIndex) + { + return false; + } + for (int i = startIndex; i < current.Length; i++) + { + if (current[i] != targetPublicKey[i - startIndex]) + { + return false; + } + } + } } + return true; } } } @@ -235,8 +272,8 @@ public PropertyWrapper(PropertyDef prop) public string QualifiedTypeFullName { get; } public string Name { get; } - public bool IsVisbleTo(string assemblyName) => - _isVisbleTo(_prop, assemblyName); + public bool IsVisbleTo(IAssemblyInformation assembly) => + _isVisbleTo(_prop, assembly); public override string ToString() => Name; } diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/IAssemblyInformation.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/IAssemblyInformation.cs index 4aa1c3a7..c355d661 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/IAssemblyInformation.cs +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/IAssemblyInformation.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +#nullable enable namespace Avalonia.Ide.CompletionEngine.AssemblyMetadata; @@ -12,6 +13,7 @@ public interface IAssemblyInformation Stream GetManifestResourceStream(string name); IEnumerable InternalsVisibleTo { get; } string AssemblyName { get; } + string PublicKey { get; } } public interface ICustomAttributeInformation @@ -38,6 +40,7 @@ public interface ITypeInformation IEnumerable Events { get; } IEnumerable Fields { get; } IEnumerable Pseudoclasses { get; } + IEnumerable<(ITypeInformation Type,string Name)> TemplateParts { get; } bool IsEnum { get; } bool IsStatic { get; } @@ -86,7 +89,7 @@ public interface IPropertyInformation string TypeFullName { get; } string QualifiedTypeFullName { get; } string Name { get; } - bool IsVisbleTo(string assemblyName); + bool IsVisbleTo(IAssemblyInformation assembly); } public interface IEventInformation diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/Metadata.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/Metadata.cs index 55e89247..027f2229 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/Metadata.cs +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/Metadata.cs @@ -6,9 +6,17 @@ namespace Avalonia.Ide.CompletionEngine; public class Metadata { - public Dictionary> Namespaces { get; } = new Dictionary>(); + readonly Dictionary> _namespaces = new(); + readonly Dictionary _inverseNamespace = new(); - public void AddType(string ns, MetadataType type) => Namespaces.GetOrCreate(ns)[type.Name] = type; + public IReadOnlyDictionary> Namespaces => _namespaces; + public IReadOnlyDictionary InverseNamespace => _inverseNamespace; + + public void AddType(string ns, MetadataType type) + { + _namespaces.GetOrCreate(ns)[type.Name] = type; + _inverseNamespace[type.FullName] = ns; + } } [DebuggerDisplay("{Name}")] @@ -20,6 +28,9 @@ public record MetadataType(string Name) public bool HasHintValues { get; set; } public string[]? HintValues { get; set; } + public string[] PseudoClasses { get; set; } = Array.Empty(); + public bool HasPseudoClasses { get; set; } + //assembly, type, property public Func? IsValidForXamlContextFunc { get; set; } //assembly, type, property @@ -37,6 +48,10 @@ public record MetadataType(string Name) public bool IsGeneric { get; set; } public bool IsXamlDirective { get; set; } public string? AssemblyQualifiedName { get; set; } + public bool IsNullable { get; init; } + public MetadataType? UnderlyingType { get; init; } + public IEnumerable<(MetadataType Type,string Name)> TemplateParts { get; internal set; } = + Array.Empty<(MetadataType Type, string Name)>(); } public enum MetadataTypeCtorArgument diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/MetadataConverter.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/MetadataConverter.cs index 318e12e8..ec64b6a8 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/MetadataConverter.cs +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/AssemblyMetadata/MetadataConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Xml.Linq; using Avalonia.Ide.CompletionEngine.AssemblyMetadata; @@ -27,7 +28,12 @@ public static class MetadataConverter "Avalonia.Markup.Xaml.Styling.StyleInclude,", "Avalonia.Markup.Xaml.Styling.StyleIncludeExtension,", }; - + private readonly static Regex extractType = new Regex( + "System.Nullable`1<(?.*)>|System.Nullable`1\\[\\[(?.*)]].*", + RegexOptions.CultureInvariant + | RegexOptions.Compiled + ); internal static bool IsMarkupExtension(ITypeInformation type) { @@ -87,13 +93,14 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider) var resourceUrls = new List(); var avaresValues = new List(); var pseudoclasses = new HashSet(); + var typepseudoclasses = new HashSet(); var ignoredResExt = new[] { ".resources", ".rd.xml", "!AvaloniaResources" }; bool skipRes(string res) => ignoredResExt.Any(r => res.EndsWith(r, StringComparison.OrdinalIgnoreCase)); PreProcessTypes(types, metadata); - + var targetAssembly = provider.Assemblies.First(); foreach (var asm in provider.Assemblies) { var aliases = new Dictionary(); @@ -105,23 +112,53 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider) if (asm.AssemblyName == provider.TargetAssemblyName || asm.InternalsVisibleTo.Any(att => { - var attParts = att.Split(','); - var nameParts = provider.TargetAssemblyName.Split(','); - var min = Math.Min(attParts.Length, nameParts.Length); - var i = 0; - for (; i < min && string.Equals(attParts[i], nameParts[i], StringComparison.OrdinalIgnoreCase); i++) + var endNameIndex = att.IndexOf(','); + var assemblyName = att; + var targetPublicKey = targetAssembly.PublicKey; + if (endNameIndex > 0) { - + assemblyName = att.Substring(0, endNameIndex); + } + if (assemblyName == targetAssembly.Name) + { + if (endNameIndex == -1) + { + return true; + } + var publicKeyIndex = att.IndexOf("PublicKey", endNameIndex, StringComparison.OrdinalIgnoreCase); + if (publicKeyIndex > 0) + { + publicKeyIndex += 9; + if (publicKeyIndex > att.Length) + { + return false; + } + while (publicKeyIndex < att.Length && att[publicKeyIndex] is ' ' or '=') + { + publicKeyIndex++; + } + if (targetPublicKey.Length == att.Length - publicKeyIndex) + { + for (int i = publicKeyIndex; i < att.Length; i++) + { + if (att[i] != targetPublicKey[i - publicKeyIndex]) + { + return false; + } + } + return true; + } + } } - return i == min; + return false; })) { typeFilter = type => type.Name != "" && !type.IsInterface && !type.IsAbstract; } - var asmTypes = asm.Types.ToArray(); + var asmTypes = asm.Types.Where(typeFilter).ToArray(); - foreach (var type in asmTypes.Where(typeFilter)) + foreach (var type in asmTypes) { var mt = types[type.AssemblyQualifiedName] = ConvertTypeInfomation(type); typeDefs[mt] = type; @@ -146,7 +183,8 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider) resourceUrls.AddRange(asm.ManifestResourceNames.Where(r => !skipRes(r)).Select(r => $"resm:{r}?assembly={asm.Name}")); } - foreach (var type in types.Values) + var at = types.Values.ToArray(); + foreach (var type in at) { typeDefs.TryGetValue(type, out var typeDef); @@ -164,21 +202,29 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider) } int level = 0; + typepseudoclasses.Clear(); + + type.TemplateParts = (typeDef?.TemplateParts ?? + Array.Empty<(ITypeInformation, string)>()) + .Select(item => (Type: ConvertTypeInfomation(item.Type), item.Name)); + while (typeDef != null) { - var typePseudoclasses = typeDef.Pseudoclasses; - - foreach (var pc in typePseudoclasses) + foreach (var pc in typeDef.Pseudoclasses) { + typepseudoclasses.Add(pc); pseudoclasses.Add(pc); } var currentType = types.GetValueOrDefault(typeDef.AssemblyQualifiedName); foreach (var prop in typeDef.Properties) { - if (!prop.IsVisbleTo(provider.TargetAssemblyName)) + if (!prop.IsVisbleTo(targetAssembly)) continue; - var p = new MetadataProperty(prop.Name, types.GetValueOrDefault(prop.TypeFullName, prop.QualifiedTypeFullName), + + var propertyType = GetType(types, prop.TypeFullName, prop.QualifiedTypeFullName); + + var p = new MetadataProperty(prop.Name, propertyType, currentType, false, prop.IsStatic, prop.HasPublicGetter, prop.HasPublicSetter); @@ -187,28 +233,14 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider) foreach (var eventDef in typeDef.Events) { - var e = new MetadataEvent(eventDef.Name, types.GetValueOrDefault(eventDef.TypeFullName, eventDef.QualifiedTypeFullName), + var e = new MetadataEvent(eventDef.Name, GetType(types, eventDef.TypeFullName, eventDef.QualifiedTypeFullName), types.GetValueOrDefault(typeDef.FullName, typeDef.AssemblyQualifiedName), false); type.Events.Add(e); } - //check for attached properties only on top level if (level == 0) { - foreach (var methodDef in typeDef.Methods) - { - if (methodDef.Name.StartsWith("Set", StringComparison.OrdinalIgnoreCase) && methodDef.IsStatic && methodDef.IsPublic - && methodDef.Parameters.Count == 2) - { - var name = methodDef.Name.Substring(3); - type.Properties.Add(new MetadataProperty(name, - types.GetValueOrDefault(methodDef.Parameters[1].TypeFullName, methodDef.Parameters[1].QualifiedTypeFullName), - types.GetValueOrDefault(typeDef.FullName, typeDef.AssemblyQualifiedName), - true, false, true, true)); - } - } - foreach (var fieldDef in typeDef.Fields) { if (fieldDef.IsStatic && fieldDef.IsPublic) @@ -226,6 +258,44 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider) types.GetValueOrDefault(typeDef.FullName, typeDef.AssemblyQualifiedName), true)); } + else if (fieldDef.Name.EndsWith("Property", StringComparison.OrdinalIgnoreCase) + && fieldDef.ReturnTypeFullName.StartsWith("Avalonia.AttachedProperty`1") + ) + { + var name = fieldDef.Name.Substring(0, fieldDef.Name.Length - "Property".Length); + + IMethodInformation? setMethod = null; + IMethodInformation? getMethod = null; + + foreach (var methodDef in typeDef.Methods) + { + if (methodDef.Name.StartsWith("Set", StringComparison.OrdinalIgnoreCase) && methodDef.IsStatic && methodDef.IsPublic + && methodDef.Parameters.Count == 2) + { + setMethod = methodDef; + } + if (methodDef.IsStatic + && methodDef.Name.StartsWith("Get", StringComparison.OrdinalIgnoreCase) + && methodDef.IsPublic + && methodDef.Parameters.Count == 1 + && !string.IsNullOrEmpty(methodDef.ReturnTypeFullName) + ) + { + getMethod = methodDef; + } + } + + if (getMethod is not null) + { + type.Properties.Add(new MetadataProperty(name, + Type: types.GetValueOrDefault(getMethod.ReturnTypeFullName, getMethod.QualifiedReturnTypeFullName), + DeclaringType: types.GetValueOrDefault(typeDef.FullName, typeDef.AssemblyQualifiedName), + IsAttached: true, + IsStatic: false, + HasGetter: true, + HasSetter: setMethod is not null)); + } + } else if (type.IsStatic) { type.Properties.Add(new MetadataProperty(fieldDef.Name, null, type, false, true, true, false)); @@ -247,6 +317,11 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider) type.HasAttachedEvents = type.Events.Any(e => e.IsAttached); type.HasStaticGetProperties = type.Properties.Any(p => p.IsStatic && p.HasGetter); type.HasSetProperties = type.Properties.Any(p => !p.IsStatic && p.HasSetter); + if (typepseudoclasses.Count > 0) + { + type.HasPseudoClasses = true; + type.PseudoClasses = typepseudoclasses.ToArray(); + } if (ctors?.Any() == true) { @@ -273,6 +348,35 @@ public static Metadata ConvertMetadata(IMetadataReaderSession provider) PostProcessTypes(types, metadata, resourceUrls, avaresValues, pseudoclasses); + MetadataType? GetType(Dictionary types, params string[] keys) + { + MetadataType? type = default; + foreach (var key in keys) + { + if (types.TryGetValue(key, out type)) + { + break; + } + else if (key.StartsWith("System.Nullable`1", StringComparison.OrdinalIgnoreCase)) + { + var typeName = extractType.Match(key); + if (typeName.Success && types.TryGetValue(typeName.Groups[1].Value, out type)) + { + type = new MetadataType(key) + { + AssemblyQualifiedName = type.AssemblyQualifiedName, + FullName = $"System.Nullable`1<{type.FullName}>", + IsNullable = true, + UnderlyingType = type + }; + types.Add(key, type); + break; + } + } + } + return type; + } + return metadata; } @@ -436,6 +540,12 @@ private static void PreProcessTypes(Dictionary types, Meta HasHintValues = true, HintValues = new[] { "True", "False" } }), + new MetadataType("System.Nullable`1") + { + HasHintValues = true, + IsNullable = true, + UnderlyingType = boolType, + }, new MetadataType(typeof(System.Uri).FullName!), (typeType = new MetadataType(typeof(System.Type).FullName!)), new MetadataType("Avalonia.Media.IBrush"), @@ -699,6 +809,7 @@ IEnumerable filterLocalRes(MetadataType type, string? currentAssemblyNam brushType.HintValues = brushes.Properties.Where(p => p.IsStatic && p.HasGetter).Select(p => p.Name).ToArray(); } + //TODO: Remove if (avaloniaBaseType.TryGetValue("Avalonia.Styling.Selector", out MetadataType? styleSelector)) { styleSelector.HasHintValues = true; diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Avalonia.Ide.CompletionEngine.csproj b/CompletionEngine/Avalonia.Ide.CompletionEngine/Avalonia.Ide.CompletionEngine.csproj index 77bea80f..51eb2590 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/Avalonia.Ide.CompletionEngine.csproj +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Avalonia.Ide.CompletionEngine.csproj @@ -19,4 +19,9 @@ + + + + + diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/Completion.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/Completion.cs index a7f4adb0..c1c26151 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/Completion.cs +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/Completion.cs @@ -31,14 +31,31 @@ public enum CompletionKind /// /// xmlns list in visual studio (uses enum icon instead of namespace icon) /// - VS_XMLNS = 0x800 + VS_XMLNS = 0x800, + + Selector = 0x1000, + Name = 0x2000, } -public record Completion(string DisplayText, string InsertText, string Description, CompletionKind Kind, int? RecommendedCursorOffset = null) +public record Completion(string DisplayText, + string InsertText, + string Description, + CompletionKind Kind, + int? RecommendedCursorOffset = null, + string? Suffix = null, + int? DeleteTextOffset = null + ) { public override string ToString() => DisplayText; - public Completion(string insertText, CompletionKind kind) : this(insertText, insertText, insertText, kind) + public Completion(string insertText, CompletionKind kind, string? suffix = default) : + this(insertText, insertText, insertText, kind, Suffix: suffix) + { + + } + + public Completion(string displayText, string insertText, CompletionKind kind, string? suffix = default) : + this(displayText, insertText, displayText, kind) { } diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs index 85f96d70..f2e7a10d 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -17,6 +18,9 @@ private class MetadataHelper private Dictionary? _types; private string? _currentAssemblyName; + private static Regex? _findElementByNameRegex; + internal static Regex FindElementByNameRegex => _findElementByNameRegex ??= + new($"\\s(?:(x\\:)?Name)=\"(?[\\w\\:\\s\\|\\.]+)\"", RegexOptions.Compiled); public void SetMetadata(Metadata metadata, string xml, string? currentAssemblyName = null) { @@ -49,7 +53,6 @@ public void SetMetadata(Metadata metadata, string xml, string? currentAssemblyNa var types = new Dictionary(); foreach (var alias in Aliases.Concat(new[] { new KeyValuePair("", "") })) { - var aliasValue = alias.Value ?? ""; if (!string.IsNullOrEmpty(_currentAssemblyName) && aliasValue.StartsWith("clr-namespace:") && !aliasValue.Contains(";assembly=")) @@ -119,15 +122,24 @@ public IEnumerable FilterPropertyNames(string typeName, string? propName return MetadataHelper.FilterPropertyNames(t, propName, attached, hasSetter, staticGetter); } - public static IEnumerable FilterPropertyNames(MetadataType? t, string? propName, + public static IEnumerable FilterPropertyNames(MetadataType? t, + string? propName, bool? attached, bool hasSetter, bool staticGetter = false) { + return FilterProperty(t, propName, attached, hasSetter, staticGetter).Select(p => p.Name); + } + public static IEnumerable FilterProperty(MetadataType? t, string? propName, + bool? attached, + bool hasSetter, + bool staticGetter = false + ) + { propName ??= ""; if (t == null) - return Array.Empty(); + return Array.Empty(); var e = t.Properties.Where(p => p.Name.StartsWith(propName, StringComparison.OrdinalIgnoreCase) && (hasSetter ? p.HasSetter : p.HasGetter)); @@ -138,9 +150,10 @@ public static IEnumerable FilterPropertyNames(MetadataType? t, string? p else e = e.Where(p => !p.IsStatic); - return e.Select(p => p.Name); + return e; } + public IEnumerable FilterEventNames(string typeName, string? propName, bool attached) { @@ -257,7 +270,7 @@ private static Dictionary GetNamespaceAliases(string xml) return new Completion(kvp.Key, CompletionKind.Class); })); - } + } } else if (state.State == XmlParser.ParserState.InsideElement || state.State == XmlParser.ParserState.StartAttribute) @@ -297,7 +310,7 @@ private static Dictionary GetNamespaceAliases(string xml) // this up to be dealt with in the future if (state.TagName.Equals("On")) { - completions.Add(new Completion("Options", "Options=\"\"", "Options", + completions.Add(new Completion("Options", "Options=\"\"", "Options", CompletionKind.Property, 9 /*recommendedCursorOffset*/)); } @@ -345,25 +358,34 @@ private static Dictionary GetNamespaceAliases(string xml) if (prop?.Type?.HasHintValues == true && state.CurrentValueStart.HasValue) { var search = textToCursor.Substring(state.CurrentValueStart.Value); + var hintCompletions = true; if (prop.Type.IsCompositeValue) { - var last = search.Split(' ', ',').Last(); - curStart = curStart + search.Length - last?.Length ?? 0; - search = last; - // Special case for pseudoclasses within the current edit - if (state.AttributeName!.Equals("Selector") && search!.Contains(':')) + if (state.AttributeName!.Equals("Selector")) + { + hintCompletions = false; + if (ProcesssSelector(search.AsSpan(), state, completions, currentAssemblyName, fullText) is int delta) + { + curStart = curStart + delta; + } + } + else { - search = ":"; + var last = search.Split(' ', ',').Last(); + search = last; + curStart = curStart + search.Length - last?.Length ?? 0; } } - - completions.AddRange(GetHintCompletions(prop.Type, search, currentAssemblyName)); + if (hintCompletions) + { + completions.AddRange(GetHintCompletions(prop.Type, search, currentAssemblyName)); + } } else if (prop?.Type?.Name == typeof(Type).FullName) { var cKind = CompletionKind.Class; - if (state?.AttributeName?.Equals("TargetType") == true || + if (state?.AttributeName?.Equals("TargetType") == true || state?.AttributeName?.Equals("Selector") == true) { cKind |= CompletionKind.TargetTypeClass; @@ -641,9 +663,8 @@ IEnumerable forProperties(string? filterType, string? filter, Func[\\w\\:\\s\\|\\.]+)\""); - - if (nameMatch.Count > 0) + var nameMatch = MetadataHelper.FindElementByNameRegex.Matches(fullText); + if (nameMatch is { Count: > 0 }) { var result = new List(); foreach (Match m in nameMatch) @@ -791,7 +812,7 @@ private int BuildCompletionsForMarkupExtension(MetadataProperty? property, List< if (prop?.Type?.HasHintValues == true) { completions.AddRange(GetHintCompletions(prop.Type, null, currentAssemblyName)); - } + } } return forcedStart ?? ext.CurrentValueStart; @@ -885,9 +906,351 @@ public static bool ShouldTriggerCompletionListOn(char typedChar) return char.IsLetterOrDigit(typedChar) || typedChar == '/' || typedChar == '<' || typedChar == ' ' || typedChar == '.' || typedChar == ':' || typedChar == '$' || typedChar == '#' || typedChar == '-' || typedChar == '^' || typedChar == '{' - || typedChar == '='; + || typedChar == '=' || typedChar == '[' || typedChar == '|' || typedChar == '('; } public static CompletionKind GetCompletionKindForHintValues(MetadataType type) => type.IsEnum ? CompletionKind.Enum : CompletionKind.StaticProperty; + + + public int? ProcesssSelector(ReadOnlySpan text, XmlParser state, List completions, string? currentAssemblyName, string? fullText) + { + int? parsered = default; + var parser = SelectorParser.Parse(text); + var previusStatment = parser.PreviousStatement; + switch (parser.Statement) + { + case SelectorStatement.Colon: + case SelectorStatement.FunctionArgs: + { + var fn = parser.FunctionName; + var tn = parser.TypeName; + var isEmptyTn = string.IsNullOrEmpty(tn); + if (previusStatment <= SelectorStatement.Middle && isEmptyTn) + { + completions.Add(new Completion(":is()", ":is(", CompletionKind.Selector | CompletionKind.Enum)); + } + else if (string.IsNullOrEmpty(fn)) + { + completions.Add(new Completion(":not()", ":not(", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(":nth-child()", ":nth-child(", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(":nth-last-child()", ":nth-last-child(", CompletionKind.Selector | CompletionKind.Enum)); + } + if (isEmptyTn) + { + var pseudoClasses = _helper.FilterTypes(default) + .Select(kvp => kvp.Value) + .Where(m => m.HasPseudoClasses) + .SelectMany(m => m.PseudoClasses) + .Distinct(StringComparer.OrdinalIgnoreCase); + completions.AddRange(pseudoClasses.Select(v => new Completion(v, CompletionKind.Selector | CompletionKind.Enum))); + } + else + { + var typeFullName = GetFullName(parser); + if (_helper.LookupType(typeFullName) is MetadataType { HasPseudoClasses: true } type) + { + completions.AddRange(type.PseudoClasses.Select(v => new Completion(v, CompletionKind.Selector | CompletionKind.Enum))); + } + } + if (fn == "is") + { + var types = _helper.FilterTypes(default) + .Where(t => t.Value.IsAvaloniaObjectType) + .Select(t => t.Value); + if (types?.Any() == true) + { + parsered = text.Length - (parser.LastParsedPosition + 1); + completions.AddRange(types.Select(v => + { + var name = GetXmlnsFullName(v); + return new Completion(name, name + ".", CompletionKind.Class | CompletionKind.TargetTypeClass); + })); + } + } + if (completions.Count > 0) + { + parsered = parser.LastParsedPosition ?? 0; + } + } + break; + case SelectorStatement.Name: + { + if (parser.IsTemplate) + { + var ton = parser.TemplateOwner; + if (!string.IsNullOrEmpty(ton)) + { + //If it hat TemplateOwner + if (_helper.FilterTypes(ton) + .Where(kvp => kvp.Value.TemplateParts.Any()) + .Select(kvp => kvp.Value) + .FirstOrDefault() is MetadataType ownerType) + { + var parts = ownerType.TemplateParts; + var fullName = GetFullName(parser); + var partType = string.IsNullOrEmpty(fullName) + ? default(MetadataType?) + : _helper.FilterTypes(fullName) + .Select(kvp => kvp.Value) + .FirstOrDefault(); + if (partType is not null) + { + parts = parts + .Where(p => p.Type.AssemblyQualifiedName == partType.AssemblyQualifiedName); + } + if (parts.Any()) + { + parsered = parser.LastParsedPosition ?? 0; + var x = (parser.LastParsedPosition ?? 0) - parser.LastSegmentStartPosition - 1; + if (string.IsNullOrEmpty(fullName) == false) + { + x += fullName.Length + 1; + } + completions.AddRange(parts!.Select(p => new Completion(p.Name, CompletionKind.Name | CompletionKind.Class, p.Type?.Name) + { + RecommendedCursorOffset = (p.Name.Length + (p.Type?.Name.Length > 0 ? p.Type.Name.Length + 3 : 0)), + DeleteTextOffset = -x, + })); + } + } + } + } + else if (fullText is not null) + { + var nameMatch = MetadataHelper + .FindElementByNameRegex + .Matches(fullText); + if (nameMatch is { Count: > 0 }) + { + var filterName = nameMatch.OfType(); + var elementName = parser.ElementName; + if (!string.IsNullOrEmpty(elementName)) + { + filterName = filterName + .Where(m => m.Groups["AttribValue"].Value.StartsWith(elementName, StringComparison.OrdinalIgnoreCase)); + } + foreach (Match m in filterName) + { + if (m.Success) + { + parsered = (parser.LastParsedPosition ?? 0); + var name = m.Groups["AttribValue"].Value; + completions.Add(new Completion(name, CompletionKind.Name | CompletionKind.Class)); + } + } + } + + } + } + break; + case SelectorStatement.CanHaveType: + case SelectorStatement.TypeName: + { + var tn = parser.TypeName; + if (GetFullName(parser) is string typeFullName) + { + var len = typeFullName.Length; + if (len > 0) + { + if (typeFullName[len - 1] == ':') + { + var ns = typeFullName.Substring(0, len - 1); + + if (_helper.Aliases?.TryGetValue(ns!, out var ans) == true + && _helper.Metadata?.Namespaces.TryGetValue(ans, out var types) == true) + { + IEnumerable ft = types.Values; + ft = ft + .Where(t => t.IsGeneric == false) + .Where(t => t.IsMarkupExtension == false) + .Where(t => t.IsAvaloniaObjectType || t.HasAttachedProperties); + completions.AddRange(ft.Select(v => new Completion(v.Name, $"{ns}|{v.Name}", CompletionKind.Class | CompletionKind.TargetTypeClass))); + parsered = (parser.LastParsedPosition ?? 0) - (tn?.Length ?? 0); + } + } + else if (_helper.FilterTypes(typeFullName).Select(kvp => kvp.Value) is { } types) + { + types = types + .Where(t => t.IsGeneric == false) + .Where(t => t.IsMarkupExtension == false) + .Where(t => t.IsAvaloniaObjectType || t.HasAttachedProperties); + completions.AddRange(types.Select(v => + { + var name = GetXmlnsFullName(v); + return new Completion(name, CompletionKind.Class | CompletionKind.TargetTypeClass); + })); + parsered = (parser.LastParsedPosition ?? 0) - (tn?.Length ?? 0); + } + } + } + } + break; + case SelectorStatement.Property: + { + var typeFullName = GetFullName(parser); + if (_helper.LookupType(typeFullName) is MetadataType type) + { + var propertyName = parser.PropertyName; + var selectorElementProperties = MetadataHelper.FilterProperty(type, + propName: propertyName, + attached: default, + hasSetter: false + ); + if (selectorElementProperties?.Any() == true) + { + parsered = (parser.LastParsedPosition ?? 0) - (propertyName?.Length ?? 0); + completions.AddRange(selectorElementProperties.Select(v => new Completion(v.Name, v.Name + "=", v.IsAttached ? CompletionKind.AttachedProperty : CompletionKind.Property))); + } + } + } + break; + case SelectorStatement.AttachedProperty: + { + var typeFullName = GetFullName(parser); + if (_helper.LookupType(typeFullName) is { HasAttachedProperties: true } type) + { + var propertyName = parser.PropertyName; + var selectorElementProperties = MetadataHelper.FilterProperty(type, + propName: propertyName, + attached: true, + hasSetter: false + ); + if (selectorElementProperties?.Any() == true) + { + var lenPropertyName = propertyName?.Length ?? 0; + var lenType = lenPropertyName == 0 || typeFullName is null + ? 0 + : typeFullName.Length + 1; + parsered = (parser.LastParsedPosition ?? 0) - lenType - lenType + 1; + completions.AddRange(selectorElementProperties.Select(v => new Completion(v.Name, v.Name + ")", v.IsAttached ? CompletionKind.AttachedProperty : CompletionKind.Property))); + } + } + else + { + var types = _helper.FilterTypes(default) + .Where(t => t.Value.HasAttachedProperties) + .Select(t => t.Value); + if (types?.Any() == true) + { + parsered = (parser.LastParsedPosition ?? 0) + 1; + completions.AddRange(types.Select(v => + { + var name = GetXmlnsFullName(v); + return new Completion(name, name + ".", CompletionKind.Class); + })); + } + } + } + break; + case SelectorStatement.Template: + { + completions.Add(new("/template/", "/template/", CompletionKind.Selector | CompletionKind.Enum)); + parsered = parser.LastParsedPosition; + } + break; + case SelectorStatement.Traversal: + case SelectorStatement.Start: + { + if (!parser.IsError) + { + parsered = (parser.LastParsedPosition ?? 0); + // TODO: Crowling Selector operator from Attribute of the Selector + completions.Add(new Completion("^", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(":", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(">", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(".", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion("#", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(":is()", ":is(", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(":not()", ":not(", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(":nth-child()", ":nth-child(", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion(":nth-last-child()", ":nth-last-child(", CompletionKind.Selector | CompletionKind.Enum)); + completions.Add(new Completion("/template/", "/template/", CompletionKind.Selector | CompletionKind.Enum)); + var types = _helper.FilterTypes(default) + .Where(t => t.Value.IsAvaloniaObjectType || t.Value.HasAttachedProperties) + .Select(t => new Completion(t.Value.Name.Replace(":", "|"), CompletionKind.Class | CompletionKind.TargetTypeClass)); + completions.AddRange(types); + } + } + break; + case SelectorStatement.Value: + { + var typeFullName = GetFullName(parser); + if (_helper.LookupType(typeFullName) is MetadataType type) + { + var propertyName = parser.PropertyName; + var prop = MetadataHelper.FilterProperty(type, + propName: propertyName, + attached: default, + hasSetter: false + ).FirstOrDefault(); + var propType = prop?.Type; + if (propType?.IsNullable == true) + { + propType = propType.UnderlyingType; + } + if (propType is { HasHintValues: true } pt) + { + var kind = pt.IsEnum + ? CompletionKind.Enum + : CompletionKind.StaticProperty; + IEnumerable values = pt.HintValues!; + var value = parser.Value; + if (!string.IsNullOrEmpty(value)) + { + values = values + .Where(v => v.StartsWith(value, StringComparison.OrdinalIgnoreCase)); + } + completions.AddRange(values.Select(v => new Completion(v, kind))); + parsered = parser.LastParsedPosition - (parser.Value?.Length ?? 0); + } + } + } + break; + case SelectorStatement.Function: + case SelectorStatement.Class: + case SelectorStatement.Middle: + case SelectorStatement.End: + default: + break; + } + return parsered; + + string GetFullName(SelectorParser parser) + { + var ns = parser.Namespace; + var typename = parser.TypeName + ?? GetTypeFromControlTheme(); + var typeFullName = string.IsNullOrEmpty(ns) + ? typename + : $"{ns}:{typename}"; + return typeFullName ?? string.Empty; + } + + string GetXmlnsFullName(MetadataType type, char namespaceSeparator = '|') + { + if (_helper.Metadata?.InverseNamespace.TryGetValue(type.FullName, out var ns) == true + && !string.IsNullOrEmpty(ns)) + { + var alias = _helper.Aliases?.FirstOrDefault(a => Equals(a.Value, ns)); + if (alias is not null && !string.IsNullOrEmpty(alias.Value.Key)) + { + return $"{alias.Value.Key}{namespaceSeparator}{type.Name}"; + } + } + return type.Name!; + } + + string? GetTypeFromControlTheme() + { + if (state.GetParentTagName(1)?.Equals("ControlTheme") == true) + { + if (state.FindParentAttributeValue("TargetType", 1, maxLevels: 0) is string implicitSelectorTypeName) + { + return implicitSelectorTypeName; + } + } + return default; + } + } } diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/SelectorParser.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/SelectorParser.cs new file mode 100644 index 00000000..5356e6ef --- /dev/null +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/SelectorParser.cs @@ -0,0 +1,804 @@ +using System; +using System.Globalization; + +namespace Avalonia.Ide.CompletionEngine; + +internal enum SelectorStatement +{ + Start, + Middle, + Colon, + Class, + Name, + CanHaveType, + Traversal, + TypeName, + Property, + AttachedProperty, + Template, + Value, + Function, + FunctionArgs, + End, +} + +internal ref struct SelectorParser +{ + private ref struct ParserContext + { + private ReadOnlySpan _data; + private ReadOnlySpan _original; + public ParserContext(ReadOnlySpan data) : + this() + { + _data = data; + _original = data; + } + private SelectorStatement statement = SelectorStatement.Start; + public int NamespaceStart = -1; + public int NamespaceEnd = -1; + public int TypeNameStart = -1; + public int TypeNameEnd = -1; + public int ClassNameStart = -1; + public int ClassNameEnd = -1; + public int PropertyNameStart = -1; + public int PropertyNameEnd = -1; + public int ValueStart = -1; + public int ValueEnd = -1; + public int NameStart = -1; + public int NameEnd = -1; + public int FunctionNameStart = -1; + public int FunctionNameEnd = -1; + public int Position { get; private set; } + public bool IsError = false; + public char Peek => _data[0]; + public bool End => + _data.IsEmpty; + public int? LastParsedPosition = default; + public bool IsTemplate; + public int TemplateOwnerStart = -1; + public int TemplateOwnerEnd = -1; + public int NamespaceTemplateOwnerStart = -1; + public int NamespaceTemplateOwnerEnd = -1; + public int LastSegmentStartPosition; + + public SelectorStatement PreviousStatement { get; private set; } + + public SelectorStatement Statement + { + get => statement; + set + { + if (statement != value) + { + if (value is SelectorStatement.Start or SelectorStatement.Middle) + { + LastSegmentStartPosition = Position; + } + if (value is SelectorStatement.Start) + { + (NamespaceStart, NamespaceEnd, TypeNameStart, TypeNameEnd, ClassNameStart, ClassNameEnd, PropertyNameStart, PropertyNameEnd, NameStart, NameEnd, ValueStart, ValueEnd, FunctionNameStart, FunctionNameEnd) = + (-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1); + } + PreviousStatement = statement; + } + statement = value; + } + } + + public char Take() + { + Position++; + var take = _data[0]; + _data = _data.Slice(1); + return take; + } + + public void SkipWhitespace() + { + var trimmed = _data.TrimStart(); + Position += _data.Length - trimmed.Length; + _data = trimmed; + } + + public bool TakeIf(char c) + { + if (!End && Peek == c) + { + Take(); + return true; + } + else + { + return false; + } + } + + public bool TakeIf(Func condition) + { + if (condition(Peek)) + { + Take(); + return true; + } + return false; + } + + public ReadOnlySpan TakeUntil(char c) + { + int len; + for (len = 0; len < _data.Length && _data[len] != c; len++) + { + } + var span = _data.Slice(0, len); + _data = _data.Slice(len); + Position += len; + return span; + } + + public ReadOnlySpan TakeWhile(Func condition) + { + int len; + for (len = 0; len < _data.Length && condition(_data[len]); len++) + { + } + var span = _data.Slice(0, len); + _data = _data.Slice(len); + Position += len; + return span; + } + + public ReadOnlySpan TryPeek(int count) + { + if (_data.Length < count) + return ReadOnlySpan.Empty; + return _data.Slice(0, count); + } + + public ReadOnlySpan PeekWhitespace() + { + var trimmed = _data.TrimStart(); + return _data.Slice(0, _data.Length - trimmed.Length); + } + + public void Skip(int count) + { + if (_data.Length < count) + throw new IndexOutOfRangeException(); + _data = _data.Slice(count); + } + + public ReadOnlySpan ParseStyleClass() + { + if (!End && IsValidIdentifierStart(Peek)) + { + return TakeWhile(c => IsValidIdentifierChar(c)); + } + return ReadOnlySpan.Empty; + } + + public ReadOnlySpan ParseIdentifier() + { + if (!End && IsValidIdentifierStart(Peek)) + { + return TakeWhile(c => IsValidIdentifierChar(c)); + } + return ReadOnlySpan.Empty; + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c) || c == '-') + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format || + cat == UnicodeCategory.DecimalDigitNumber; + } + } + + public ReadOnlySpan GetRange(int from, int to) + { + if (from < 0 || from > _original.Length) + { + return ReadOnlySpan.Empty; + } + if (to < 0 || to > _original.Length) + { + return _original.Slice(from); + } + else + { + return _original.Slice(from, to - from); + } + } + + } + + ParserContext _context; + + private SelectorParser(ReadOnlySpan data) + { + _context = new ParserContext(data); + } + + public string? Namespace => + _context.GetRange(_context.NamespaceStart, _context.NamespaceEnd).ToString(); + + public string? TypeName => + _context.GetRange(_context.TypeNameStart, _context.TypeNameEnd).ToString(); + + public string? Class => + _context.GetRange(_context.ClassNameStart, _context.ClassNameEnd).ToString(); + + public string? PropertyName => + _context.GetRange(_context.PropertyNameStart, _context.PropertyNameEnd).ToString(); + + public string? Value => + _context.GetRange(_context.ValueStart, _context.ValueEnd).ToString(); + + public string? ElementName => + _context.GetRange(_context.NameStart, _context.NameEnd).ToString(); + + public string? FunctionName => + _context.GetRange(_context.FunctionNameStart, _context.FunctionNameEnd).ToString(); + + public SelectorStatement Statement => + _context.Statement; + + public bool IsError => + _context.IsError; + + public int? LastParsedPosition => + _context.LastParsedPosition; + + public SelectorStatement PreviousStatement => + _context.PreviousStatement; + + public bool IsTemplate => + _context.IsTemplate; + + public int LastSegmentStartPosition => + _context.LastSegmentStartPosition; + + public string? TemplateOwner + { + get + { + var sb = new System.Text.StringBuilder(); + if (_context.NamespaceTemplateOwnerEnd > -1) + { +#if NET5_0_OR_GREATER + sb.Append(_context.GetRange(_context.NamespaceTemplateOwnerStart, _context.NamespaceTemplateOwnerEnd)); +#else + sb.Append(_context.GetRange(_context.NamespaceTemplateOwnerStart, _context.NamespaceTemplateOwnerEnd).ToArray()); +#endif + sb.Append(':'); + } +#if NET5_0_OR_GREATER + sb.Append(_context.GetRange(_context.TemplateOwnerStart, _context.TemplateOwnerEnd)); +#else + sb.Append(_context.GetRange(_context.TemplateOwnerStart, _context.TemplateOwnerEnd).ToArray()); +#endif + return sb.ToString(); + } + } + + public static SelectorParser Parse(ReadOnlySpan data) + { + var selector = new SelectorParser(data); + selector.Parse(); + return selector; + } + + + private void Parse() + { + Parse(ref _context); + } + + private static void Parse(ref ParserContext context, char? end = default) + { + while (!context.End && !context.IsError && context.Statement != SelectorStatement.End) + { + switch (context.Statement) + { + case SelectorStatement.Start: + ParseStart(ref context); + break; + case SelectorStatement.Middle: + ParseMiddle(ref context, end); + break; + case SelectorStatement.Colon: + ParseColon(ref context); + break; + case SelectorStatement.Class: + ParseClass(ref context); + break; + case SelectorStatement.Name: + ParseName(ref context); + break; + case SelectorStatement.CanHaveType: + ParseCanHaveType(ref context); + break; + case SelectorStatement.Traversal: + ParseTraversal(ref context); + break; + case SelectorStatement.TypeName: + ParseTypeName(ref context); + break; + case SelectorStatement.Property: + ParseProperty(ref context); + break; + case SelectorStatement.AttachedProperty: + ParseAttachedProperty(ref context); + break; + case SelectorStatement.Template: + ParseTemplate(ref context); + break; + case SelectorStatement.FunctionArgs: + ParseFunctionArgs(ref context); + break; + case SelectorStatement.End: + break; + default: + break; + } + } + } + + private static void ParseFunctionArgs(ref ParserContext context) + { + context.Statement = SelectorStatement.Middle; + } + + private static void ParseStart(ref ParserContext context) + { + context.SkipWhitespace(); + if (context.End) + { + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.End; + } + + if (context.TakeIf(':')) + { + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.Colon; + } + else if (context.TakeIf('.')) + { + + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.Class; + } + else if (context.TakeIf('#')) + { + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.Name; + } + else if (context.TakeIf('^')) + { + + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.CanHaveType; + } + else if (!context.End) + { + context.Statement = SelectorStatement.Middle; + } + } + + private static void ParseMiddle(ref ParserContext context, char? end) + { + if (context.TakeIf(':')) + { + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.Colon; + } + else if (context.TakeIf('.')) + { + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.Class; + } + else if (context.TakeIf(char.IsWhiteSpace) || context.Peek == '>') + { + + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.Traversal; + } + else if (context.TakeIf('/')) + { + + context.Statement = SelectorStatement.Template; + } + else if (context.TakeIf('#')) + { + + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.Name; + } + else if (context.TakeIf(',')) + { + + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.Start; + } + else if (context.TakeIf('^')) + { + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.CanHaveType; + } + else if (end.HasValue && !context.End && context.Peek == end.Value) + { + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.End; + } + else + { + context.LastParsedPosition = context.Position; + context.Statement = SelectorStatement.TypeName; + } + } + + private static void ParseColon(ref ParserContext r) + { + var start = r.Position; + var identifier = r.ParseStyleClass(); + + if (identifier.IsEmpty) + { + r.IsError = true; + return; + } + + const string IsKeyword = "is"; + const string NotKeyword = "not"; + const string NthChildKeyword = "nth-child"; + const string NthLastChildKeyword = "nth-last-child"; + + if (identifier.SequenceEqual(IsKeyword.AsSpan())) + { + r.FunctionNameStart = start; + r.Statement = SelectorStatement.Function; + r.LastParsedPosition = r.Position; + if (r.TakeIf('(')) + { + r.Statement = SelectorStatement.FunctionArgs; + r.FunctionNameEnd = r.Position - 1; + if (r.End) + { + return; + } + r.Statement = SelectorStatement.TypeName; + ParseType(ref r); + if (!Expect(ref r, ')')) + { + return; + } + r.Statement = SelectorStatement.Middle; + } + } + else if (identifier.SequenceEqual(NotKeyword.AsSpan())) + { + r.FunctionNameStart = start; + r.Statement = SelectorStatement.Function; + r.LastParsedPosition = r.Position; + if (r.TakeIf('(')) + { + r.FunctionNameEnd = r.Position - 1; + r.Statement = SelectorStatement.FunctionArgs; + Parse(ref r, ')'); + if (r.IsError) + { + return; + } + r.Statement = SelectorStatement.FunctionArgs; + Expect(ref r, ')'); + if (r.IsError) + { + return; + } + r.Statement = SelectorStatement.Middle; + } + } + else if (identifier.SequenceEqual(NthChildKeyword.AsSpan())) + { + r.FunctionNameStart = start; + r.Statement = SelectorStatement.Function; + r.LastParsedPosition = r.Position; + if (r.TakeIf('(')) + { + r.FunctionNameEnd = r.Position - 1; + r.Statement = SelectorStatement.FunctionArgs; + r.TakeUntil(')'); + Expect(ref r, ')'); + if (r.IsError) + { + return; + } + r.Statement = SelectorStatement.Middle; + r.LastParsedPosition = r.Position; + return; + } + + } + else if (identifier.SequenceEqual(NthLastChildKeyword.AsSpan())) + { + r.FunctionNameStart = start; + r.Statement = SelectorStatement.Function; + r.LastParsedPosition = r.Position; + if (r.TakeIf('(')) + { + r.FunctionNameEnd = r.Position - 1; + r.Statement = SelectorStatement.FunctionArgs; + r.TakeUntil(')'); + Expect(ref r, ')'); + if (r.IsError) + { + return; + } + r.LastParsedPosition = r.Position; + r.Statement = SelectorStatement.Middle; + } + } + else + { + r.ClassNameStart = start; + r.ClassNameEnd = r.Position; + r.LastParsedPosition = r.Position; + r.Statement = SelectorStatement.CanHaveType; + } + } + + private static void ParseClass(ref ParserContext r) + { + r.ClassNameStart = r.Position; + var @class = r.ParseStyleClass(); + if (@class.IsEmpty) + { + r.IsError = true; + return; + } + r.ClassNameEnd = r.Position; + r.LastParsedPosition = r.Position; + r.Statement = SelectorStatement.CanHaveType; + } + + private static void ParseName(ref ParserContext r) + { + r.NameStart = r.Position; + var name = r.ParseIdentifier(); + if (name.IsEmpty) + { + r.IsError = true; + return; + } + r.NameEnd = r.Position; + if (!r.End) + r.Statement = SelectorStatement.CanHaveType; + } + + private static void ParseCanHaveType(ref ParserContext r) + { + if (r.TakeIf('[')) + { + r.LastParsedPosition = r.Position; + r.Statement = SelectorStatement.Property; + } + else + { + r.Statement = SelectorStatement.Middle; + } + } + + private static void ParseTraversal(ref ParserContext r) + { + r.SkipWhitespace(); + if (r.TakeIf('>')) + { + r.SkipWhitespace(); + r.Statement = SelectorStatement.Middle; + } + else if (r.TakeIf('/')) + { + r.LastParsedPosition = r.Position; + r.Statement = SelectorStatement.Template; + } + else if (!r.End) + { + r.Statement = SelectorStatement.Middle; + } + else + { + r.LastParsedPosition = r.Position; + r.Statement = SelectorStatement.End; + } + } + + private static void ParseTypeName(ref ParserContext r) + { + ParseType(ref r); + if (r.IsError) + { + return; + } + r.LastParsedPosition = r.Position; + r.Statement = SelectorStatement.CanHaveType; + } + + private static void ParseProperty(ref ParserContext r) + { + r.LastParsedPosition = r.Position; + r.PropertyNameStart = r.Position; + var property = r.ParseIdentifier(); + + if (r.End) + { + r.IsError = true; + return; + } + + if (r.TakeIf('(')) + { + r.Statement = SelectorStatement.AttachedProperty; + return; + } + else if (!r.TakeIf('=')) + { + r.IsError = true; + } + r.PropertyNameEnd = r.Position - 1; + r.LastParsedPosition = r.Position; + r.Statement = SelectorStatement.Value; + r.ValueStart = r.Position; + _ = r.TakeUntil(']'); + if (!Expect(ref r, ']')) + { + return; + } + r.ValueEnd = r.Position; + r.Statement = SelectorStatement.Property; + r.LastParsedPosition = r.Position; + if (!r.End) + { + r.Statement = SelectorStatement.Middle; + } + } + + private static void ParseAttachedProperty(ref ParserContext r) + { + r.LastParsedPosition = r.Position; + ParseType(ref r); + if (r.IsError) + { + return; + } + r.LastParsedPosition = r.Position; + if (r.End || !r.TakeIf('.')) + { + r.IsError = true; + return; + } + r.PropertyNameStart = r.Position; + if (r.End) + { + r.IsError = true; + return; + } + var property = r.ParseIdentifier(); + if (r.End || property.IsEmpty) + { + r.IsError = true; + return; + } + r.PropertyNameEnd = r.Position; + + if (!r.TakeIf(')')) + { + r.IsError = true; + return; + } + r.SkipWhitespace(); + r.LastParsedPosition = r.Position; + + if (r.End || !r.TakeIf('=')) + { + r.IsError = true; + return; + } + r.ValueStart = r.Position; + r.Statement = SelectorStatement.Value; + _ = r.TakeUntil(']'); + r.ValueEnd = r.Position; + if (Expect(ref r, ']')) + { + r.IsError = true; + return; + } + r.LastParsedPosition = r.Position; + if (!r.End) + { + r.Statement = SelectorStatement.Middle; + } + } + + private static void ParseTemplate(ref ParserContext r) + { + var template = r.ParseIdentifier(); + const string TemplateKeyword = "template"; + if (!template.SequenceEqual(TemplateKeyword.AsSpan())) + { + r.LastParsedPosition = r.Position; + r.IsError = true; + return; + } + else if (!r.TakeIf('/')) + { + r.LastParsedPosition = r.Position; + r.IsError = true; + return; + } + r.LastParsedPosition = r.Position; + r.IsTemplate = true; + (r.TemplateOwnerStart, r.TemplateOwnerEnd, r.NamespaceTemplateOwnerStart, r.NamespaceTemplateOwnerEnd) = + (r.TypeNameStart, r.TypeNameEnd, r.NamespaceStart, r.NamespaceEnd); + r.Statement = SelectorStatement.Start; + } + + private static void ParseType(ref ParserContext r) + { + r.LastParsedPosition = r.Position; + ReadOnlySpan ns = default; + var startPosition = r.Position; + var namespaceOrTypeName = r.ParseIdentifier(); + + if (namespaceOrTypeName.IsEmpty) + { + r.IsError = true; + return; + } + + if (!r.End && r.TakeIf('|')) + { + ns = namespaceOrTypeName; + r.NamespaceStart = startPosition; + r.NamespaceEnd = r.Position - 1; + if (r.End) + { + r.IsError = true; + return; + } + r.TypeNameStart = r.Position; + _ = r.ParseIdentifier(); + r.TypeNameEnd = r.Position; + } + else + { + r.TypeNameStart = startPosition; + r.TypeNameEnd = r.Position; + } + r.LastParsedPosition = r.Position; + } + + private static bool Expect(ref ParserContext r, char c) + { + if (r.End || !r.TakeIf(c)) + { + r.IsError = true; + return false; + } + return true; + } +} diff --git a/Directory.Build.props b/Directory.Build.props index 7e89c189..697905a6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,6 +5,10 @@ 11.0 + + 0024000004800000940000000602000000240000525341310004000001000100b111cf707bc11956645caffc0d3749fde7a81ee7bdca74d0a3f3f7f599aab8d6c256db70757a96a4589c353e81c8d521d7b72e3fbc59f90715f362057c4d06cd35a5c3956b4d38f12251cbf1d53db778c94dd6fb652a3fb27a03256a9df604bf9bb4c435e5163b605e00f18200433e354459c8a812fa1dee5b6b90efa3100db2 + + all diff --git a/tests/CompletionEngineTests/AdvancedTests.cs b/tests/CompletionEngineTests/AdvancedTests.cs index bba11f28..a0ec45c1 100644 --- a/tests/CompletionEngineTests/AdvancedTests.cs +++ b/tests/CompletionEngineTests/AdvancedTests.cs @@ -1,5 +1,7 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Xunit; @@ -60,25 +62,25 @@ public void Extension_With_CtorArgument_Enum_Should_Be_Completed() { AssertSingleCompletion(" v.InsertText == ":pointerover"); Assert.Contains(compl, v => v.InsertText == ":disabled"); @@ -183,7 +185,7 @@ public void Style_Attached_Property_Name_Should_Be_Completed() { var xaml = "