diff --git a/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems b/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems index c2a7e4e..5cd8933 100644 --- a/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems +++ b/AvaloniaVS.Shared/AvaloniaVS.Shared.projitems @@ -14,6 +14,7 @@ + @@ -24,6 +25,7 @@ + diff --git a/AvaloniaVS.Shared/EnumerableExtesions.cs b/AvaloniaVS.Shared/EnumerableExtesions.cs new file mode 100644 index 0000000..51d6ddd --- /dev/null +++ b/AvaloniaVS.Shared/EnumerableExtesions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace AvaloniaVS +{ + static class EnumerableExtesions + { + public static TSource FirstOrDefault(this IEnumerable source, Func predicate, TArg arg) + { + if (source is null) + throw new ArgumentNullException(nameof(source)); + if (predicate is null) + throw new ArgumentNullException(nameof(predicate)); + + var enumerator = source.GetEnumerator(); + while (enumerator.MoveNext()) + { + TSource item = enumerator.Current; + if (predicate(item, arg)) + { + return item; + } + } + return default; + } + } +} diff --git a/AvaloniaVS.Shared/IntelliSense/XamlCompletionHandlerProvider.cs b/AvaloniaVS.Shared/IntelliSense/XamlCompletionHandlerProvider.cs index e92782b..0cd8064 100644 --- a/AvaloniaVS.Shared/IntelliSense/XamlCompletionHandlerProvider.cs +++ b/AvaloniaVS.Shared/IntelliSense/XamlCompletionHandlerProvider.cs @@ -6,6 +6,7 @@ using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Utilities; @@ -23,6 +24,7 @@ internal class XamlCompletionHandlerProvider : IVsTextViewCreationListener private readonly IServiceProvider _serviceProvider; private readonly IVsEditorAdaptersFactoryService _adapterService; private readonly ICompletionBroker _completionBroker; + private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry; private readonly CompletionEngineSource _completionEngineSource; [ImportingConstructor] @@ -30,11 +32,13 @@ public XamlCompletionHandlerProvider( [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider, IVsEditorAdaptersFactoryService adapterService, ICompletionBroker completionBroker, + ITextUndoHistoryRegistry textUndoHistoryRegistry, CompletionEngineSource completionEngineSource) { _serviceProvider = serviceProvider; _adapterService = adapterService; _completionBroker = completionBroker; + _textUndoHistoryRegistry = textUndoHistoryRegistry; _completionEngineSource = completionEngineSource; } @@ -52,6 +56,14 @@ public void VsTextViewCreated(IVsTextView textViewAdapter) textView, textViewAdapter, _completionEngineSource.CompletionEngine)); + + textView.Properties.GetOrCreateSingletonProperty( + () => new XamlPasteCommandHandler( + _serviceProvider, + textView, + textViewAdapter, + _textUndoHistoryRegistry, + _completionEngineSource.CompletionEngine)); } } } diff --git a/AvaloniaVS.Shared/IntelliSense/XamlPasteCommandHandler.cs b/AvaloniaVS.Shared/IntelliSense/XamlPasteCommandHandler.cs new file mode 100644 index 0000000..1e85f45 --- /dev/null +++ b/AvaloniaVS.Shared/IntelliSense/XamlPasteCommandHandler.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using Avalonia.Ide.CompletionEngine; +using Avalonia.Ide.CompletionEngine.Parsing; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Text.Operations; +using Microsoft.VisualStudio.TextManager.Interop; +using IServiceProvider = System.IServiceProvider; +using OleConstants = Microsoft.VisualStudio.OLE.Interop.Constants; + + +namespace AvaloniaVS.IntelliSense +{ + /// + /// Handles key presses for the Avalonia XAML intellisense completion. + /// + /// + /// Adds a command handler to text views and listens for keypresses which should cause a + /// completion to be opened or comitted. + /// + /// Yes, this is horrible, but it's apparently the official way to do this. Eurgh. + /// + internal class XamlPasteCommandHandler : IOleCommandTarget + { + private readonly IServiceProvider _serviceProvider; + private readonly IOleCommandTarget _nextCommandHandler; + private readonly IWpfTextView _textView; + private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry; + private readonly CompletionEngine _engine; + private readonly uint _pasteCommandId = (uint)VSConstants.VSStd97CmdID.Paste; + public const string Avalonia_DevTools_Selector = nameof(Avalonia_DevTools_Selector); + + + public XamlPasteCommandHandler( + IServiceProvider serviceProvider, + IWpfTextView textView, + IVsTextView textViewAdapter, + ITextUndoHistoryRegistry textUndoHistoryRegistry, + CompletionEngine completionEngine + ) + { + _serviceProvider = serviceProvider; + _textView = textView; + _textUndoHistoryRegistry = textUndoHistoryRegistry; + _engine = completionEngine; + + // Add ourselves as a command to the text view. + textViewAdapter.AddCommandFilter(this, out _nextCommandHandler); + } + + public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) + { + ThreadHelper.ThrowIfNotOnUIThread(); + if (Clipboard.ContainsData(Avalonia_DevTools_Selector)) + { + if (pguidCmdGroup == VSConstants.GUID_VSStandardCommandSet97) + { + for (int i = 0; i < cCmds; i++) + { + if (prgCmds[i].cmdID == _pasteCommandId) + { + if (_engine.Helper.Metadata is not null) + { + var line = _textView.GetTextViewLineContainingBufferPosition(_textView.Caret.Position.BufferPosition); + var end = Math.Min(line.End, _textView.Caret.Position.BufferPosition); + var parser = XmlParser.Parse(_textView.TextSnapshot.GetText().AsMemory(), 0, end); + var state = parser.State; + if (state == XmlParser.ParserState.AttributeValue || + state == XmlParser.ParserState.AfterAttributeValue) + { + if (parser.AttributeName?.Equals("Selector") == true) + { + prgCmds[i].cmdf = (uint)(OLECMDF.OLECMDF_ENABLED | OLECMDF.OLECMDF_SUPPORTED); + } + } + } + return VSConstants.S_OK; + } + } + } + } + return _nextCommandHandler?.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText) ?? (int)OleConstants.OLECMDERR_E_NOTSUPPORTED; + } + + public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + // If we're in an automation function, move to the next command. + if (!VsShellUtilities.IsInAutomationFunction(_serviceProvider) && HandlePasteCommand(pguidCmdGroup, nCmdID)) + { + return VSConstants.S_OK; + } + var result = _nextCommandHandler?.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut) ?? VSConstants.S_OK; + return result; + } + + private bool HandlePasteCommand(Guid pguidCmdGroup, uint nCmdID) + { + if (pguidCmdGroup == VSConstants.GUID_VSStandardCommandSet97 + && nCmdID == _pasteCommandId + ) + { + if (Clipboard.GetData(Avalonia_DevTools_Selector) is MemoryStream @do) + { + var bytes = @do.ToArray(); + var seletcorText = System.Text.Encoding.Unicode.GetChars(bytes, 0, bytes.Length - 2); + var x = _textView.Selection.SelectedSpans; + var p = _textView.Caret.Position.BufferPosition; + + _ = TranslateSelectorAsync(_textView, _textView.TextSnapshot, p, seletcorText) + .ConfigureAwait(false); + + return true; + } + + } + return false; + } + + private async Task TranslateSelectorAsync(ITextView textView, ITextSnapshot snapshot, int postion, char[] seletcorChars) + { + var selectorsInfo = DevToolsSelectorParser.Parse(seletcorChars); + + if (_engine.Helper.Metadata is { } metadata && selectorsInfo.Count > 0) + { + var aliases = _engine.Helper.Aliases; + var sb = new StringBuilder(); + + Index index = default; + Dictionary aliasesToAdd = new(); + var aliasFounded = false; + foreach (var si in selectorsInfo) + { + if (si.AssemblyName.Start.Value - 1 > index.Value) + { + sb.Append(seletcorChars, index.Value, si.AssemblyName.Start.Value - 1 - index.Value); + } + if (si.AssemblyName.End.Value > index.Value) + { + index = si.AssemblyName.End.Value + 1; + } + sb.Append(seletcorChars[index..si.Namespace.Start]); + var fn = DevToolsSelectorInfo.GetFullName(seletcorChars, si); + if (metadata.InverseNamespace.TryGetValue(fn, out var namespaces) && namespaces.Length > 0) + { + aliasFounded = false; + foreach (var item in namespaces) + { + if (aliases.FirstOrDefault((a, arg) => string.Equals(a.Value, arg), item) is { Key: not null } kv) + { + aliasFounded = true; + if (!string.IsNullOrEmpty(kv.Value)) + { + sb.Append(kv.Key); + sb.Append('|'); + } + break; + } + } + if (aliasFounded == false) + { + var @namespace = string.Concat(seletcorChars[si.Namespace]); + var xmlsns = CompletionEngine.GetXmlnsFromNamespace(seletcorChars[si.Namespace]); + aliasesToAdd.Add(xmlsns, @namespace); + sb.Append(xmlsns); + sb.Append('|'); + } + sb.Append(seletcorChars[si.ElementType]); + index = si.ElementType.End.Value; + } + } + sb.Append(seletcorChars[index..seletcorChars.Length]); + + if (sb.Length > 0) + { + var undoHistory = _textUndoHistoryRegistry.RegisterHistory(textView); + using (var transaction = undoHistory.CreateTransaction("Paste Style Selector")) + { + using (var edit = snapshot.TextBuffer.CreateEdit()) + { + edit.Insert(postion, sb.ToString()); + + if (aliasesToAdd.Count > 0) + { + int i = 0; + for (; edit.Snapshot[i] != '>' && i < edit.Snapshot.Length; i++) + { + + } + if (i > 0 && i < edit.Snapshot.Length) + { + var b = _textView.FormattedLineSource.BaseIndentation; + + var line = edit.Snapshot.GetLineFromPosition(i); + var indentation_Space = CalculateLeftOfFirstChar(line, _textView.FormattedLineSource); + var tabSize = _textView.Options.GetTabSize(); + var indentationStyle = _textView.Options.GetIndentStyle(); + var newline = _textView.Options.GetNewLineCharacter(); + var options = _textView.Options.SupportedOptions.ToArray(); + var convert = _textView.Options.IsConvertTabsToSpacesEnabled(); + + var indentationBuilder = new StringBuilder(indentation_Space); + if (convert) + { + indentationBuilder.Append(' ', indentation_Space); + } + else + { + var ntabs = Math.DivRem(indentation_Space, tabSize, out var remainder); + indentationBuilder.Append('\t', ntabs); + indentationBuilder.Append(' ', remainder); + } + + foreach (var item in aliasesToAdd) + { + + edit.Insert(i, $"{newline}{indentationBuilder}xmlns:{item.Key}=\"using:{item.Value}\""); + } + } + } + edit.Apply(); + } + if (snapshot != textView.TextSnapshot) + transaction.Complete(); + } + + } + } + + await Task.CompletedTask; + } + + private static int CalculateLeftOfFirstChar(ITextSnapshotLine line, IFormattedLineSource fls) + { + var nspace = 0; + var start = line.Start; + while (start.GetChar() is { } ch && char.IsWhiteSpace(ch)) + { + if (ch == ' ') + { + nspace++; + } + else if (ch == '\t') + { + nspace += fls.TabSize; + } + start += 1; + } + return nspace; + } + } +} diff --git a/AvaloniaVS.Shared/TextViewExtensions.cs b/AvaloniaVS.Shared/TextViewExtensions.cs index 3d456e9..8e1f88c 100644 --- a/AvaloniaVS.Shared/TextViewExtensions.cs +++ b/AvaloniaVS.Shared/TextViewExtensions.cs @@ -1,13 +1,13 @@ -using Microsoft.VisualStudio.Text.Editor; +using System.Linq; using Microsoft.VisualStudio.Text; -using System.Linq; +using Microsoft.VisualStudio.Text.Editor; namespace AvaloniaVS; internal static class TextViewExtensions { public static NormalizedSnapshotSpanCollection GetSpanInView(this ITextView textView, SnapshotSpan span) -=> textView.BufferGraph.MapUpToSnapshot(span, SpanTrackingMode.EdgeInclusive, textView.TextSnapshot); + => textView.BufferGraph.MapUpToSnapshot(span, SpanTrackingMode.EdgeInclusive, textView.TextSnapshot); public static void SetSelection( this ITextView textView, VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint) diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Avalonia.Ide.CompletionEngine.csproj b/CompletionEngine/Avalonia.Ide.CompletionEngine/Avalonia.Ide.CompletionEngine.csproj index 51eb259..726e4fd 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/Avalonia.Ide.CompletionEngine.csproj +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Avalonia.Ide.CompletionEngine.csproj @@ -21,7 +21,7 @@ - + diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Compatibility/Range.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/Compatibility/Range.cs new file mode 100644 index 0000000..3036986 --- /dev/null +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Compatibility/Range.cs @@ -0,0 +1,278 @@ +// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Index.cs +// https://github.com/dotnet/runtime/blob/419e949d258ecee4c40a460fb09c66d974229623/src/libraries/System.Private.CoreLib/src/System/Range.cs + +#if NETSTANDARD2_0 + +using System.Runtime.CompilerServices; + +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// + public readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + else + { + return _value; + } + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return "^" + ((uint)Value).ToString(); + + return ((uint)Value).ToString(); + } + } + + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + public readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return Start.GetHashCode() * 31 + End.GetHashCode(); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { + return Start + ".." + End; + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start; + var startIndex = Start; + if (startIndex.IsFromEnd) + start = length - startIndex.Value; + else + start = startIndex.Value; + + int end; + var endIndex = End; + if (endIndex.IsFromEnd) + end = length - endIndex.Value; + else + end = endIndex.Value; + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } + } +} + +namespace System.Runtime.CompilerServices +{ + public static class RuntimeHelpers + { + /// + /// Slices the specified array using the specified range. + /// + public static T[] GetSubArray(T[] array, Range range) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + (int offset, int length) = range.GetOffsetAndLength(array.Length); + + if (default(T) != null || typeof(T[]) == array.GetType()) + { + // We know the type of the array to be exactly T[]. + + if (length == 0) + { + return Array.Empty(); + } + + var dest = new T[length]; + Array.Copy(array, offset, dest, 0, length); + return dest; + } + else + { + // The array is actually a U[] where U:T. + var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); + Array.Copy(array, offset, dest, 0, length); + return dest; + } + } + } +} + +#endif diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs index 66e8a54..faf4583 100644 --- a/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Completion/CompletionEngine.cs @@ -648,7 +648,7 @@ private void ProcessStyleSetter(string setterPropertyName, XmlParser state, List { if (state.FindParentAttributeValue("Selector", 1, maxLevels: 0)?.Trim() is { Length: > 0 } selector) { - if (selector[0]=='^') + if (selector[0] == '^') { selectorTypeName = state.FindParentAttributeValue("TargetType", 2, maxLevels: 0); } @@ -1342,7 +1342,7 @@ string GetFullName(SelectorParser parser) if (string.IsNullOrEmpty(typename)) { typename = GetTypeFromControlTheme(); - } + } var typeFullName = string.IsNullOrEmpty(ns) ? typename : $"{ns}:{typename}"; @@ -1375,4 +1375,40 @@ string GetXmlnsFullName(MetadataType type, char namespaceSeparator = '|') return default; } } + + 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!; + } + + public static readonly IEnumerable Default = new INamespaceTrasformation[] + { + new NamespaceTrasformations.ToLowerTrasformation(), + new NamespaceTrasformations.ReplaceDot('_'), + }; + + public static string GetXmlnsFromNamespace(string @namespace) => + GetXmlnsFromNamespace(@namespace.ToCharArray(), Default); + + public static string GetXmlnsFromNamespace(char[] @namespace) => + GetXmlnsFromNamespace(@namespace, Default); + + public static string GetXmlnsFromNamespace(char[] @namespace, IEnumerable trasformations) + { + IEnumerable source = @namespace; + foreach (var trasformation in trasformations) + { + source = trasformation.Apply(source); + } + return string.Concat(source); + } } diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/INamespaceTrasformation.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/INamespaceTrasformation.cs new file mode 100644 index 0000000..8295bc1 --- /dev/null +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/INamespaceTrasformation.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Avalonia.Ide.CompletionEngine; + +public interface INamespaceTrasformation +{ + public IEnumerable Apply(IEnumerable input); +} diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/NamespaceTrasformations/ReplaceDot.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/NamespaceTrasformations/ReplaceDot.cs new file mode 100644 index 0000000..e448e37 --- /dev/null +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/NamespaceTrasformations/ReplaceDot.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Avalonia.Ide.CompletionEngine.NamespaceTrasformations; + +internal class ReplaceDot : INamespaceTrasformation +{ + private readonly char _sobstituion; + + public ReplaceDot(char sobstituion) + { + _sobstituion = sobstituion; + } + + public IEnumerable Apply(IEnumerable input) + { + foreach (char c in input) + { + if (c == '.') + yield return _sobstituion; + else + { + yield return c; + } + } + } +} diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/NamespaceTrasformations/ToLowerTrasformation.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/NamespaceTrasformations/ToLowerTrasformation.cs new file mode 100644 index 0000000..fde0eb4 --- /dev/null +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/NamespaceTrasformations/ToLowerTrasformation.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Avalonia.Ide.CompletionEngine.NamespaceTrasformations; + +internal class ToLowerTrasformation : INamespaceTrasformation +{ + public IEnumerable Apply(IEnumerable input) + { + foreach (char c in input) + { + yield return char.ToLowerInvariant(c); + } + } +} + diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/DevToolsSelectorInfo.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/DevToolsSelectorInfo.cs new file mode 100644 index 0000000..8dc83ab --- /dev/null +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/DevToolsSelectorInfo.cs @@ -0,0 +1,19 @@ +using System; +using System.Text; + +namespace Avalonia.Ide.CompletionEngine.Parsing; + +public record struct DevToolsSelectorInfo(Range ElementType, Range Namespace, Range AssemblyName = default) +{ + public static string GetFullName(char[] buffer, DevToolsSelectorInfo info) + { + var sb = new StringBuilder(); + if (info.Namespace.Start.Value < info.Namespace.End.Value) + { + sb.Append(buffer[info.Namespace]); + sb.Append('.'); + } + sb.Append(buffer[info.ElementType]); + return sb.ToString(); + } +} diff --git a/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/DevToolsSelectorParser.cs b/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/DevToolsSelectorParser.cs new file mode 100644 index 0000000..0d5dbb6 --- /dev/null +++ b/CompletionEngine/Avalonia.Ide.CompletionEngine/Parsing/DevToolsSelectorParser.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Ide.CompletionEngine.Parsing; + +public class DevToolsSelectorParser +{ + private enum SelectorInfoPart + { + AssemblyName = 0, + Namespace = 1, + Element = 2, + } + + public static IReadOnlyList Parse(string input) + => Parse(input.ToCharArray()); + + public static IReadOnlyList Parse(char[] chars) + { + Range[] parts = new Range[3]; + List selectorsInfo = new(); + var partStartIndex = -1; + var partName = SelectorInfoPart.Namespace; + + for (int i = 0; i < chars.Length; i++) + { + var c = chars[i]; + switch (c) + { + case '{': + partName = SelectorInfoPart.AssemblyName; + partStartIndex = i + 1; + break; + case '}' when partName == SelectorInfoPart.AssemblyName: + parts[(int)SelectorInfoPart.AssemblyName] = new(partStartIndex, i); + partStartIndex = -1; + partName = SelectorInfoPart.Namespace; + break; + case '|' when partName == SelectorInfoPart.Namespace && partStartIndex > -1: + parts[(int)SelectorInfoPart.Namespace] = new(partStartIndex, i); + partName = SelectorInfoPart.Element; + partStartIndex = -1; + break; + case '.' or '#' or ':' or ' ' when partName == SelectorInfoPart.Element: + parts[(int)SelectorInfoPart.Element] = new(partStartIndex, i); + selectorsInfo.Add(new(parts[2], parts[1], parts[0])); + parts[0] = default; + parts[1] = default; + parts[2] = default; + partName = SelectorInfoPart.Namespace; + break; + default: + if (partName is SelectorInfoPart.Namespace or SelectorInfoPart.Element + && partStartIndex == -1 + && !char.IsWhiteSpace(c)) + { + partStartIndex = i; + } + break; + } + } + if (partStartIndex > -1 && partName == SelectorInfoPart.Element) + { + parts[(int)SelectorInfoPart.Element] = new(partStartIndex, chars.Length); + selectorsInfo.Add(new(parts[2], parts[1], parts[0])); + parts[0] = default; + parts[1] = default; + parts[2] = default; + } + return selectorsInfo; + } +}