From 0bb2ce282aaf63648e202ee3400852610ff4775e Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 4 Jul 2023 02:32:43 -0400 Subject: [PATCH 1/6] Alter how XmlCompletionSource gathers items The list can now contain items of multiple kinds, such as element values and child elements values. When there are multiple kinds, the tasks will run in parallel. There is a virtual method that allows subclasses to add additional items to the list, and this will also run in parallel with the other tasks. --- .../Completion/CompletionTriggerTests.cs | 2 +- Core/Completion/XmlCompletionTriggering.cs | 6 +- .../Completion/XmlCompletionCommitManager.cs | 2 +- Editor/Completion/XmlCompletionSource.cs | 180 +++++++++++------- 4 files changed, 111 insertions(+), 79 deletions(-) diff --git a/Core.Tests/Completion/CompletionTriggerTests.cs b/Core.Tests/Completion/CompletionTriggerTests.cs index 3b05da25..886043be 100644 --- a/Core.Tests/Completion/CompletionTriggerTests.cs +++ b/Core.Tests/Completion/CompletionTriggerTests.cs @@ -20,7 +20,7 @@ public class CompletionTriggerTests // trigger reason defaults to insertion if typedChar is non-null, else invocation // length defaults to 0 // if typedChar is provided, it's added to the document text - [TestCase ("", XmlCompletionTrigger.ElementWithBracket)] + [TestCase ("", XmlCompletionTrigger.ElementValue)] [TestCase("<", XmlCompletionTrigger.Element)] [TestCase("", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)] [TestCase("<", 'a', XmlCompletionTrigger.Element, 1)] diff --git a/Core/Completion/XmlCompletionTriggering.cs b/Core/Completion/XmlCompletionTriggering.cs index 488b3eb9..6556227b 100644 --- a/Core/Completion/XmlCompletionTriggering.cs +++ b/Core/Completion/XmlCompletionTriggering.cs @@ -23,7 +23,7 @@ public static (XmlCompletionTrigger kind, int length) GetTrigger (XmlSpineParser // explicit invocation in element name if (isExplicit && context.CurrentState is XmlNameState && context.CurrentState.Parent is XmlTagState) { - int length = context.CurrentStateLength; + int length = context.CurrentStateLength + 1; // include the < character return (XmlCompletionTrigger.Element, length); } @@ -108,7 +108,7 @@ public static (XmlCompletionTrigger kind, int length) GetTrigger (XmlSpineParser context.CurrentState is XmlTextState || XmlRootState.IsFree (context) )) { - return (XmlCompletionTrigger.ElementWithBracket, 0); + return (XmlCompletionTrigger.ElementValue, 0); } return (XmlCompletionTrigger.None, 0); @@ -119,7 +119,7 @@ enum XmlCompletionTrigger { None, Element, - ElementWithBracket, + ElementValue, Attribute, AttributeValue, Entity, diff --git a/Editor/Completion/XmlCompletionCommitManager.cs b/Editor/Completion/XmlCompletionCommitManager.cs index db82dc0d..001bf7d3 100644 --- a/Editor/Completion/XmlCompletionCommitManager.cs +++ b/Editor/Completion/XmlCompletionCommitManager.cs @@ -54,7 +54,7 @@ public bool ShouldCommitCompletion (IAsyncCompletionSession session, SnapshotPoi switch (kind) { case XmlCompletionTrigger.Element: - case XmlCompletionTrigger.ElementWithBracket: + case XmlCompletionTrigger.ElementValue: // allow using / as a commit char for elements as self-closing elements, but special case disallowing it // in the cases where that could conflict with typing the / at the start of a closing tag if (typedChar == '/') { diff --git a/Editor/Completion/XmlCompletionSource.cs b/Editor/Completion/XmlCompletionSource.cs index 3e5372af..f4c82195 100644 --- a/Editor/Completion/XmlCompletionSource.cs +++ b/Editor/Completion/XmlCompletionSource.cs @@ -53,16 +53,35 @@ protected XmlSpineParser GetSpineParser (SnapshotPoint snapshotPoint) return spineParser; } - public async virtual Task GetCompletionContextAsync ( - IAsyncCompletionSession session, - CompletionTrigger trigger, - SnapshotPoint triggerLocation, - SnapshotSpan applicableToSpan, - CancellationToken token) + public async virtual Task GetCompletionContextAsync (IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token) { + var tasks = GetCompletionTasks (session, trigger, triggerLocation, applicableToSpan, token).ToList (); + + await Task.WhenAll (tasks); + + var allItems = ImmutableArray.Empty; + foreach (var task in tasks) { +#pragma warning disable VSTHRD103 // Call async methods when in an async method + if (task.Result is IList taskItems && taskItems.Count > 0) { + allItems = allItems.AddRange (taskItems); + } +#pragma warning restore VSTHRD103 // Call async methods when in an async method + } + + if (allItems.IsEmpty) { + return CompletionContext.Empty; + } + + return new CompletionContext (allItems, null, InitialSelectionHint.SoftSelection); + } + + IEnumerable?>> GetCompletionTasks (IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token) + { + yield return GetAdditionalCompletionsAsync (session, trigger, triggerLocation, applicableToSpan, token); + var reason = ConvertReason (trigger.Reason, trigger.Character); if (reason == null) { - return CompletionContext.Empty; + yield break; } var parser = GetSpineParser (triggerLocation); @@ -70,65 +89,55 @@ public async virtual Task GetCompletionContextAsync ( // FIXME: cache the value from InitializeCompletion somewhere? var (kind, _) = XmlCompletionTriggering.GetTrigger (parser, reason.Value, trigger.Character); - if (kind != XmlCompletionTrigger.None) { - List nodePath = parser.GetNodePath (triggerLocation.Snapshot); - - session.Properties.AddProperty (typeof (XmlCompletionTrigger), kind); - - switch (kind) { - case XmlCompletionTrigger.Element: - case XmlCompletionTrigger.ElementWithBracket: - // if we're completing an existing element, remove it from the path - // so we don't get completions for its children instead - if (nodePath.Count > 0) { - if (nodePath[nodePath.Count-1] is XElement leaf && leaf.Name.Length == applicableToSpan.Length) { - nodePath.RemoveAt (nodePath.Count - 1); - } - } - //TODO: if it's on the first or second line and there's no DTD declaration, add the DTDs, or at least (maxDepth: 1) is not IAttributedXObject attributedOb) { - throw new InvalidOperationException ("Did not find IAttributedXObject in stack for XmlCompletionTrigger.Attribute"); - } - parser.Clone ().AdvanceUntilEnded ((XObject)attributedOb, triggerLocation.Snapshot, 1000); - var attributes = attributedOb.Attributes.ToDictionary (StringComparer.OrdinalIgnoreCase); - return await GetAttributeCompletionsAsync (session, triggerLocation, nodePath, attributedOb, attributes, token); + List nodePath = parser.GetNodePath (triggerLocation.Snapshot); + session.Properties.AddProperty (typeof (XmlCompletionTrigger), kind); - case XmlCompletionTrigger.AttributeValue: - if (parser.Spine.TryPeek (out XAttribute? att) && parser.Spine.TryPeek (1, out IAttributedXObject? attributedObject)) { - return await GetAttributeValueCompletionsAsync (session, triggerLocation, nodePath, attributedObject, att, token); + switch (kind) { + case XmlCompletionTrigger.ElementValue: + yield return GetElementValueCompletionsAsync (session, triggerLocation, nodePath, token); + goto case XmlCompletionTrigger.Element; + + case XmlCompletionTrigger.Element: + // if we're completing an existing element, remove it from the path + // so we don't get completions for its children instead + if (nodePath.Count > 0) { + if (nodePath[nodePath.Count - 1] is XElement leaf && leaf.Name.Length == applicableToSpan.Length) { + nodePath.RemoveAt (nodePath.Count - 1); } - break; + } + //TODO: if it's on the first or second line and there's no DTD declaration, add the DTDs, or at least (maxDepth: 1) is not IAttributedXObject attributedOb) { + throw new InvalidOperationException ("Did not find IAttributedXObject in stack for XmlCompletionTrigger.Attribute"); + } + parser.Clone ().AdvanceUntilEnded ((XObject)attributedOb, triggerLocation.Snapshot, 1000); + var attributes = attributedOb.Attributes.ToDictionary (StringComparer.OrdinalIgnoreCase); + yield return GetAttributeCompletionsAsync (session, triggerLocation, nodePath, attributedOb, attributes, token); + break; - case XmlCompletionTrigger.DocType: - case XmlCompletionTrigger.DeclarationOrCDataOrComment: - return await GetDeclarationCompletionsAsync (session, triggerLocation, nodePath, token); + case XmlCompletionTrigger.AttributeValue: + if (parser.Spine.TryPeek (out XAttribute? att) && parser.Spine.TryPeek (1, out IAttributedXObject? attributedObject)) { + yield return GetAttributeValueCompletionsAsync (session, triggerLocation, nodePath, attributedObject, att, token); } - } + break; - return CompletionContext.Empty; - } + case XmlCompletionTrigger.Entity: + yield return GetEntityCompletionsAsync (session, triggerLocation, nodePath, token); + break; - static XmlTriggerReason? ConvertReason (CompletionTriggerReason reason, char typedChar) - { - switch (reason) { - case CompletionTriggerReason.Insertion: - if (typedChar != '\0') - return XmlTriggerReason.TypedChar; + case XmlCompletionTrigger.DocType: + case XmlCompletionTrigger.DeclarationOrCDataOrComment: + yield return GetDeclarationCompletionsAsync (session, triggerLocation, nodePath, token); break; - case CompletionTriggerReason.Backspace: - return XmlTriggerReason.Backspace; - case CompletionTriggerReason.Invoke: - case CompletionTriggerReason.InvokeAndCommitIfUnique: - return XmlTriggerReason.Invocation; } - return null; } public virtual Task GetDescriptionAsync ( @@ -163,17 +172,16 @@ public virtual CompletionStartData InitializeCompletion (CompletionTrigger trigg [LoggerMessage (EventId = 2, Level = LogLevel.Trace, Message = "Attempting completion for state '{state}'x{currentSpineLength}, character='{triggerChar}', trigger='{triggerReason}'")] static partial void LogAttemptingCompletion (ILogger logger, XmlParserState state, int currentSpineLength, char triggerChar, CompletionTriggerReason triggerReason); - - protected virtual Task GetElementCompletionsAsync ( + protected virtual Task?> GetElementCompletionsAsync ( IAsyncCompletionSession session, SnapshotPoint triggerLocation, List nodePath, bool includeBracket, CancellationToken token ) - => Task.FromResult (CompletionContext.Empty); + => Task.FromResult?> (null); - protected virtual Task GetAttributeCompletionsAsync ( + protected virtual Task?> GetAttributeCompletionsAsync ( IAsyncCompletionSession session, SnapshotPoint triggerLocation, List nodePath, @@ -181,9 +189,9 @@ protected virtual Task GetAttributeCompletionsAsync ( Dictionary existingAtts, CancellationToken token ) - => Task.FromResult (CompletionContext.Empty); + => Task.FromResult?> (null); - protected virtual Task GetAttributeValueCompletionsAsync ( + protected virtual Task?> GetAttributeValueCompletionsAsync ( IAsyncCompletionSession session, SnapshotPoint triggerLocation, List nodePath, @@ -191,32 +199,56 @@ protected virtual Task GetAttributeValueCompletionsAsync ( XAttribute attribute, CancellationToken token ) - => Task.FromResult (CompletionContext.Empty); + => Task.FromResult?> (null); - protected virtual Task GetEntityCompletionsAsync ( + protected virtual Task?> GetEntityCompletionsAsync ( IAsyncCompletionSession session, SnapshotPoint triggerLocation, List nodePath, CancellationToken token ) - => Task.FromResult (CreateCompletionContext (GetBuiltInEntityItems ())); + => Task.FromResult?> (null); - protected virtual Task GetDeclarationCompletionsAsync ( + protected virtual Task?> GetDeclarationCompletionsAsync ( IAsyncCompletionSession session, SnapshotPoint triggerLocation, List nodePath, CancellationToken token ) - => Task.FromResult ( - CreateCompletionContext ( - nodePath.Any (n => n is XElement) - ? new [] { cdataItemWithBracket, commentItemWithBracket } - : new [] { commentItemWithBracket } - ) + => Task.FromResult?> ( + nodePath.Any (n => n is XElement) + ? new [] { cdataItemWithBracket, commentItemWithBracket } + : new [] { commentItemWithBracket } ); - static CompletionContext CreateCompletionContext (IEnumerable items) - => new (ImmutableArray.Empty.AddRange (items), null, InitialSelectionHint.SoftSelection); + protected virtual Task?> GetElementValueCompletionsAsync ( + IAsyncCompletionSession session, + SnapshotPoint triggerLocation, + List nodePath, + CancellationToken token) => Task.FromResult?> (null); + + protected virtual Task?> GetAdditionalCompletionsAsync ( + IAsyncCompletionSession session, + CompletionTrigger trigger, + SnapshotPoint triggerLocation, + SnapshotSpan applicableToSpan, + CancellationToken token) => Task.FromResult?> (null); + + static XmlTriggerReason? ConvertReason (CompletionTriggerReason reason, char typedChar) + { + switch (reason) { + case CompletionTriggerReason.Insertion: + if (typedChar != '\0') + return XmlTriggerReason.TypedChar; + break; + case CompletionTriggerReason.Backspace: + return XmlTriggerReason.Backspace; + case CompletionTriggerReason.Invoke: + case CompletionTriggerReason.InvokeAndCommitIfUnique: + return XmlTriggerReason.Invocation; + } + return null; + } CompletionItem cdataItem, commentItem, prologItem; CompletionItem cdataItemWithBracket, commentItemWithBracket, prologItemWithBracket; From 77e572b035403430246a8b2d36118de368f3917b Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 4 Jul 2023 02:33:37 -0400 Subject: [PATCH 2/6] Fix property key for cached per-view/buffer loggers --- Editor/Logging/EditorLoggerFactoryExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Editor/Logging/EditorLoggerFactoryExtensions.cs b/Editor/Logging/EditorLoggerFactoryExtensions.cs index 9cb81451..cb23adb3 100644 --- a/Editor/Logging/EditorLoggerFactoryExtensions.cs +++ b/Editor/Logging/EditorLoggerFactoryExtensions.cs @@ -14,13 +14,13 @@ public static class EditorLoggerFactoryExtensions /// Get a logger for the if it exists, reusing the existing logger if possible, and creating one if necessary. /// public static ILogger GetLogger (this IEditorLoggerFactory factory, ITextBuffer buffer) - => buffer.Properties.GetOrCreateSingletonProperty (typeof (T), () => factory.CreateLogger (buffer)); + => buffer.Properties.GetOrCreateSingletonProperty (typeof (ILogger), () => factory.CreateLogger (buffer)); /// /// Get a logger for the , reusing the existing logger if possible, and creating one if necessary. /// public static ILogger GetLogger (this IEditorLoggerFactory factory, ITextView textView) - => textView.Properties.GetOrCreateSingletonProperty (typeof (T), () => factory.CreateLogger (textView)); + => textView.Properties.GetOrCreateSingletonProperty (typeof (ILogger), () => factory.CreateLogger (textView)); /// /// Create a logger for the . From 1b65d132387240e3dd0339133c55b96b70580f92 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 4 Jul 2023 02:33:56 -0400 Subject: [PATCH 3/6] Force closing tags to sort last --- Editor/Completion/XmlCompletionSource.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Editor/Completion/XmlCompletionSource.cs b/Editor/Completion/XmlCompletionSource.cs index f4c82195..a2da2060 100644 --- a/Editor/Completion/XmlCompletionSource.cs +++ b/Editor/Completion/XmlCompletionSource.cs @@ -342,7 +342,12 @@ IEnumerable GetClosingTags (List nodePath, bool include continue; } - var item = new CompletionItem (prefix + name, this, XmlImages.ClosingTag) + string insertText = prefix + name; + + // force these to sort last, they're not very interesting values to browse as these tags are most likely already closed + string sortText = "ZZZZZZ" + insertText; + + var item = new CompletionItem (insertText, this, XmlImages.ClosingTag, ImmutableArray.Empty, "", insertText, sortText, insertText, ImmutableArray.Empty) .AddClosingElementDocumentation (el, dedup.Count > 1) .AddKind (dedup.Count == 1? XmlCompletionItemKind.ClosingTag : XmlCompletionItemKind.MultipleClosingTags); item.Properties.AddProperty (typeof (List), nodePath); From 8f04ce9ad0d562a1b2914755e09c9938641148b1 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 5 Jul 2023 03:17:35 -0400 Subject: [PATCH 4/6] Tweak triggering and add trigger span readforward --- .../Completion/CompletionTriggerTests.cs | 93 ++++++++++--------- Core/Completion/XmlCompletionTriggering.cs | 65 ++++++++----- Core/Parser/XmlParserTextSourceExtensions.cs | 38 ++++---- Editor.Tests/Completion/CompletionTests.cs | 4 +- .../Completion/XmlCompletionTestSource.cs | 17 ++-- .../Completion/XmlCompletionCommitManager.cs | 11 ++- Editor/Completion/XmlCompletionSource.cs | 25 +++-- Editor/XmlParserSnapshotExtensions.cs | 3 +- 8 files changed, 147 insertions(+), 109 deletions(-) diff --git a/Core.Tests/Completion/CompletionTriggerTests.cs b/Core.Tests/Completion/CompletionTriggerTests.cs index 886043be..9680abe7 100644 --- a/Core.Tests/Completion/CompletionTriggerTests.cs +++ b/Core.Tests/Completion/CompletionTriggerTests.cs @@ -20,74 +20,76 @@ public class CompletionTriggerTests // trigger reason defaults to insertion if typedChar is non-null, else invocation // length defaults to 0 // if typedChar is provided, it's added to the document text - [TestCase ("", XmlCompletionTrigger.ElementValue)] - [TestCase("<", XmlCompletionTrigger.Element)] - [TestCase("", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)] - [TestCase("<", 'a', XmlCompletionTrigger.Element, 1)] - [TestCase(" (out T result, T defaultVal) where T: notnull + { + if (argIdx < args.Length && args[argIdx] is T r) { + argIdx++; + result = r; + return true; + } + result = defaultVal; + return false; + } - XmlTriggerReason reason; - char typedChar; + //first arg is the doc + string doc = (string)args[argIdx++]; //next arg can be typed char or a trigger reason - //this would make a nice switch expression w/c#8 - if (args[1] is XmlTriggerReason r) { - reason = r; - typedChar = '\0'; - } else if (args[1] is char c) { + XmlTriggerReason reason; + if (TryGetArg (out char typedChar, defaultVal: '\0')) { reason = XmlTriggerReason.TypedChar; - typedChar = c; - } else { - reason = XmlTriggerReason.Invocation; - typedChar = '\0'; + } else{ + TryGetArg (out reason, defaultVal: XmlTriggerReason.Invocation); } - //expected trigger will be last unless length is provided, then it's penultimate - var expectedTrigger = args[args.Length - 1] is XmlCompletionTrigger - ? args[args.Length - 1] - : args[args.Length - 2]; + int triggerPos = doc.Length; - //length is optional, but if provided it's always last - int expectedLength = args[args.Length - 1] as int? ?? 0; + XmlCompletionTrigger expectedTrigger = (XmlCompletionTrigger) args[argIdx++]; + TryGetArg (out int expectedSpanStart, defaultVal: triggerPos); + TryGetArg (out XmlReadForward expectedReadForward, defaultVal: XmlReadForward.None); if (typedChar != '\0') { doc += typedChar; @@ -98,7 +100,10 @@ public void TriggerTests (object[] args) var result = XmlCompletionTriggering.GetTrigger (spine, reason, typedChar); Assert.AreEqual (expectedTrigger, result.kind); - Assert.AreEqual (expectedLength, result.length); + if (expectedTrigger != XmlCompletionTrigger.None) { + Assert.AreEqual (expectedSpanStart, result.spanStart); + Assert.AreEqual (expectedReadForward, result.spanReadForward); + } } } } diff --git a/Core/Completion/XmlCompletionTriggering.cs b/Core/Completion/XmlCompletionTriggering.cs index 6556227b..dd24cc51 100644 --- a/Core/Completion/XmlCompletionTriggering.cs +++ b/Core/Completion/XmlCompletionTriggering.cs @@ -10,8 +10,9 @@ namespace MonoDevelop.Xml.Editor.Completion class XmlCompletionTriggering { //FIXME: the length should do a readahead to capture the whole token - public static (XmlCompletionTrigger kind, int length) GetTrigger (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter) + public static (XmlCompletionTrigger kind, int spanStart, XmlReadForward spanReadForward) GetTrigger (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter) { + int triggerPosition = parser.Position; bool isExplicit = reason == XmlTriggerReason.Invocation; bool isTypedChar = reason == XmlTriggerReason.TypedChar; bool isBackspace = reason == XmlTriggerReason.Backspace; @@ -23,78 +24,79 @@ public static (XmlCompletionTrigger kind, int length) GetTrigger (XmlSpineParser // explicit invocation in element name if (isExplicit && context.CurrentState is XmlNameState && context.CurrentState.Parent is XmlTagState) { - int length = context.CurrentStateLength + 1; // include the < character - return (XmlCompletionTrigger.Element, length); + int start = triggerPosition - context.CurrentStateLength; + return (XmlCompletionTrigger.ElementName, start, XmlReadForward.XmlName); } //auto trigger after < in free space if ((isTypedChar || isBackspace) && XmlRootState.MaybeTag (context)) { - return (XmlCompletionTrigger.Element, 0); + return (XmlCompletionTrigger.Tag, triggerPosition - 1, XmlReadForward.None); } - //auto trigger after typing first char after < or fist char of attribute + //auto trigger after typing first char after < or first char of attribute if (isTypedChar && context.CurrentStateLength == 1 && context.CurrentState is XmlNameState && XmlChar.IsFirstNameChar (typedCharacter)) { if (context.CurrentState.Parent is XmlTagState) { - return (XmlCompletionTrigger.Element, 1); + return (XmlCompletionTrigger.ElementName, triggerPosition - 1, XmlReadForward.None); } if (context.CurrentState.Parent is XmlAttributeState) { - return (XmlCompletionTrigger.Attribute, 1); + return (XmlCompletionTrigger.AttributeName, triggerPosition - 1, XmlReadForward.None); } + return (XmlCompletionTrigger.None, 0, XmlReadForward.None); } // trigger on explicit invocation after < if (isExplicit && XmlRootState.MaybeTag (context)) { - return (XmlCompletionTrigger.Element, 0); + return (XmlCompletionTrigger.Tag, triggerPosition - 1, XmlReadForward.TagStart); } //doctype/cdata completion, explicit trigger after 0 && text[text.Length - 1] == '&') { - return (XmlCompletionTrigger.Entity, 0); + return (XmlCompletionTrigger.Entity, triggerPosition - 1, isExplicit ? XmlReadForward.Entity : XmlReadForward.None); } if (isExplicit) { for (int i = 0; i < text.Length; i++) { var c = text[text.Length - i - 1]; if (c == '&') { - return (XmlCompletionTrigger.Entity, i); + return (XmlCompletionTrigger.Entity, triggerPosition - i - 1, XmlReadForward.Entity); } if (!XmlChar.IsNameChar (c)) { break; @@ -108,19 +110,36 @@ public static (XmlCompletionTrigger kind, int length) GetTrigger (XmlSpineParser context.CurrentState is XmlTextState || XmlRootState.IsFree (context) )) { - return (XmlCompletionTrigger.ElementValue, 0); + return (XmlCompletionTrigger.ElementValue, triggerPosition, XmlReadForward.None); } - return (XmlCompletionTrigger.None, 0); + return (XmlCompletionTrigger.None, triggerPosition, XmlReadForward.None); } } + /// + /// Describes how to read forward from the completion span start to get the completion span + /// + enum XmlReadForward + { + None, + XmlName, + TagStart, + DocType, + AttributeValue, + Entity + } + enum XmlCompletionTrigger { None, - Element, + + /// An XML tag, which may be an element with leading angle bracket, comment etc + Tag, + + ElementName, ElementValue, - Attribute, + AttributeName, AttributeValue, Entity, DocType, diff --git a/Core/Parser/XmlParserTextSourceExtensions.cs b/Core/Parser/XmlParserTextSourceExtensions.cs index 13a8da5b..307db7b6 100644 --- a/Core/Parser/XmlParserTextSourceExtensions.cs +++ b/Core/Parser/XmlParserTextSourceExtensions.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using MonoDevelop.Xml.Dom; -using MonoDevelop.Xml.Parser; namespace MonoDevelop.Xml.Parser { @@ -23,29 +22,32 @@ public static XName GetCompleteName (this XmlSpineParser spine, ITextSource text { Debug.Assert (spine.CurrentState is XmlNameState); - int end = spine.Position; - int start = end - spine.CurrentStateLength; - int mid = -1; + int start = spine.Position - spine.CurrentStateLength; + var length = text.GetXNameLengthAtPosition (start, spine.Position, maximumReadahead); - int limit = Math.Min (text.Length, end + maximumReadahead); + string name = text.GetText (start, length); + + int i = name.IndexOf (':'); + if (i < 0) { + return new XName (name); + } else { + return new XName (name.Substring (0, i), name.Substring (i + 1)); + } + } + + public static int GetXNameLengthAtPosition (this ITextSource text, int nameStartPosition, int currentPosition, int maximumReadahead = DEFAULT_READAHEAD_LIMIT) + { + int limit = Math.Min (text.Length, currentPosition + maximumReadahead); //try to find the end of the name, but don't go too far - for (; end < limit; end++) { - char c = text[end]; - - if (c == ':') { - if (mid == -1) - mid = end; - else - break; - } else if (!XmlChar.IsNameChar (c)) + for (; currentPosition < limit; currentPosition++) { + char c = text[currentPosition]; + if (!XmlChar.IsNameChar (c)) { break; + } } - if (mid > 0 && end > mid + 1) { - return new XName (text.GetText (start, mid - start), text.GetText (mid + 1, end - mid - 1)); - } - return new XName (text.GetText (start, end - start)); + return currentPosition - nameStartPosition; } /// diff --git a/Editor.Tests/Completion/CompletionTests.cs b/Editor.Tests/Completion/CompletionTests.cs index 25e94815..bf7bf72d 100644 --- a/Editor.Tests/Completion/CompletionTests.cs +++ b/Editor.Tests/Completion/CompletionTests.cs @@ -15,8 +15,8 @@ public async Task TestElementStartCompletion () { var result = await this.GetCompletionContext ("<$"); result.AssertNonEmpty (); - result.AssertContains ("Hello"); - result.AssertContains ("!--"); + result.AssertContains (" GetElementCompletionsAsync ( + protected override Task?> GetElementCompletionsAsync ( IAsyncCompletionSession session, SnapshotPoint triggerLocation, List nodePath, @@ -62,12 +62,12 @@ protected override Task GetElementCompletionsAsync ( { var item = new CompletionItem (includeBracket? ".Empty; - items = items.Add (item).AddRange (GetMiscellaneousTags (triggerLocation, nodePath, includeBracket)); - return Task.FromResult (new CompletionContext (items)); + var items = new List () { item }; + items.AddRange (GetMiscellaneousTags (triggerLocation, nodePath, includeBracket)); + return Task.FromResult?> (items); } - protected override Task GetAttributeCompletionsAsync ( + protected override Task?> GetAttributeCompletionsAsync ( IAsyncCompletionSession session, SnapshotPoint triggerLocation, List nodePath, @@ -78,12 +78,11 @@ protected override Task GetAttributeCompletionsAsync ( if (nodePath.LastOrDefault () is XElement xel && xel.NameEquals ("Hello", true)) { var item = new CompletionItem ("There", this) .AddKind (XmlCompletionItemKind.Attribute); - var items = ImmutableArray.Empty; - items = items.Add (item); - return Task.FromResult (new CompletionContext (items)); + var items = new List () { item }; + return Task.FromResult?> (items); } - return Task.FromResult (CompletionContext.Empty); + return Task.FromResult?> (null); } } } diff --git a/Editor/Completion/XmlCompletionCommitManager.cs b/Editor/Completion/XmlCompletionCommitManager.cs index 001bf7d3..022f5efd 100644 --- a/Editor/Completion/XmlCompletionCommitManager.cs +++ b/Editor/Completion/XmlCompletionCommitManager.cs @@ -53,19 +53,20 @@ public bool ShouldCommitCompletion (IAsyncCompletionSession session, SnapshotPoi }; switch (kind) { - case XmlCompletionTrigger.Element: + case XmlCompletionTrigger.Tag: + case XmlCompletionTrigger.ElementName: case XmlCompletionTrigger.ElementValue: // allow using / as a commit char for elements as self-closing elements, but special case disallowing it // in the cases where that could conflict with typing the / at the start of a closing tag if (typedChar == '/') { var span = session.ApplicableToSpan.GetSpan (location.Snapshot); - if (span.Length == (kind == XmlCompletionTrigger.Element ? 0 : 1)) { + if (span.Length == (kind == XmlCompletionTrigger.ElementName ? 0 : 1)) { return false; } } return Array.IndexOf (tagCommitChars, typedChar) > -1; - case XmlCompletionTrigger.Attribute: + case XmlCompletionTrigger.AttributeName: return Array.IndexOf (attributeCommitChars, typedChar) > -1; case XmlCompletionTrigger.AttributeValue: @@ -81,9 +82,9 @@ public bool ShouldCommitCompletion (IAsyncCompletionSession session, SnapshotPoi return false; } - static readonly CommitResult CommitSwallowChar = new CommitResult (true, CommitBehavior.SuppressFurtherTypeCharCommandHandlers); + static readonly CommitResult CommitSwallowChar = new (true, CommitBehavior.SuppressFurtherTypeCharCommandHandlers); - static readonly CommitResult CommitCancel = new CommitResult (true, CommitBehavior.CancelCommit); + static readonly CommitResult CommitCancel = new (true, CommitBehavior.CancelCommit); public CommitResult TryCommit (IAsyncCompletionSession session, ITextBuffer buffer, CompletionItem item, char typedChar, CancellationToken token) { diff --git a/Editor/Completion/XmlCompletionSource.cs b/Editor/Completion/XmlCompletionSource.cs index a2da2060..17796daf 100644 --- a/Editor/Completion/XmlCompletionSource.cs +++ b/Editor/Completion/XmlCompletionSource.cs @@ -87,7 +87,7 @@ public async virtual Task GetCompletionContextAsync (IAsyncCo var parser = GetSpineParser (triggerLocation); // FIXME: cache the value from InitializeCompletion somewhere? - var (kind, _) = XmlCompletionTriggering.GetTrigger (parser, reason.Value, trigger.Character); + var (kind, _, _) = XmlCompletionTriggering.GetTrigger (parser, reason.Value, trigger.Character); if (kind == XmlCompletionTrigger.None) { yield break; @@ -99,9 +99,10 @@ public async virtual Task GetCompletionContextAsync (IAsyncCo switch (kind) { case XmlCompletionTrigger.ElementValue: yield return GetElementValueCompletionsAsync (session, triggerLocation, nodePath, token); - goto case XmlCompletionTrigger.Element; + goto case XmlCompletionTrigger.Tag; - case XmlCompletionTrigger.Element: + case XmlCompletionTrigger.Tag: + case XmlCompletionTrigger.ElementName: // if we're completing an existing element, remove it from the path // so we don't get completions for its children instead if (nodePath.Count > 0) { @@ -111,10 +112,10 @@ public async virtual Task GetCompletionContextAsync (IAsyncCo } //TODO: if it's on the first or second line and there's no DTD declaration, add the DTDs, or at least (maxDepth: 1) is not IAttributedXObject attributedOb) { throw new InvalidOperationException ("Did not find IAttributedXObject in stack for XmlCompletionTrigger.Attribute"); } @@ -159,9 +160,19 @@ public virtual CompletionStartData InitializeCompletion (CompletionTrigger trigg LogAttemptingCompletion (Logger, spine.CurrentState, spine.CurrentStateLength, trigger.Character, trigger.Reason); - var (kind, length) = XmlCompletionTriggering.GetTrigger (spine, reason.Value, trigger.Character); + var (kind, spanStart, spanReadForward) = XmlCompletionTriggering.GetTrigger (spine, reason.Value, trigger.Character); + + int spanLength; + if (spanReadForward == XmlReadForward.XmlName) { + spanLength = new SnapshotTextSource (triggerLocation.Snapshot).GetXNameLengthAtPosition (spanStart, triggerLocation.Position); + } else { + spanLength = triggerLocation.Position - spanStart; + } + + //TODO: handle spanReadforward + if (kind != XmlCompletionTrigger.None) { - return new CompletionStartData (CompletionParticipation.ProvidesItems, new SnapshotSpan (triggerLocation.Snapshot, triggerLocation.Position - length, length)); + return new CompletionStartData (CompletionParticipation.ProvidesItems, new SnapshotSpan (triggerLocation.Snapshot, spanStart, spanLength)); } //TODO: closing tag completion after typing > diff --git a/Editor/XmlParserSnapshotExtensions.cs b/Editor/XmlParserSnapshotExtensions.cs index cdfcd091..7e0c274f 100644 --- a/Editor/XmlParserSnapshotExtensions.cs +++ b/Editor/XmlParserSnapshotExtensions.cs @@ -3,8 +3,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; + using Microsoft.VisualStudio.Text; + using MonoDevelop.Xml.Dom; using MonoDevelop.Xml.Parser; From 25d442c7505124ae64f81047cf94aa94d8d1c079 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 5 Jul 2023 03:17:47 -0400 Subject: [PATCH 5/6] Support per-item CommitChars --- Editor/Completion/XmlCompletionCommitManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Editor/Completion/XmlCompletionCommitManager.cs b/Editor/Completion/XmlCompletionCommitManager.cs index 022f5efd..a445cb6c 100644 --- a/Editor/Completion/XmlCompletionCommitManager.cs +++ b/Editor/Completion/XmlCompletionCommitManager.cs @@ -92,6 +92,13 @@ public CommitResult TryCommit (IAsyncCompletionSession session, ITextBuffer buff return CommitResult.Unhandled; } + // per-item CommitCharacters overrides the default commit chars + if (!item.CommitCharacters.IsDefaultOrEmpty) { + if (item.CommitCharacters.Contains (typedChar)) { + return CommitCancel; + } + } + var span = session.ApplicableToSpan.GetSpan (buffer.CurrentSnapshot); bool wasTypedInFull = span.Length == item.InsertText.Length; From d541340764f42c9694d0e222cc9a3992f6bc9a7a Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 6 Jul 2023 19:11:46 -0400 Subject: [PATCH 6/6] Completion span improvements and tests --- .../Completion/CompletionTriggerTests.cs | 151 ++++++++++-------- Core/Completion/XmlCompletionTriggering.cs | 88 ++++++++-- Core/Parser/XmlParserTextSourceExtensions.cs | 28 ++++ Editor/Completion/XmlCompletionSource.cs | 34 ++-- 4 files changed, 201 insertions(+), 100 deletions(-) diff --git a/Core.Tests/Completion/CompletionTriggerTests.cs b/Core.Tests/Completion/CompletionTriggerTests.cs index 9680abe7..999fc237 100644 --- a/Core.Tests/Completion/CompletionTriggerTests.cs +++ b/Core.Tests/Completion/CompletionTriggerTests.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.IO; +using System; + using MonoDevelop.Xml.Editor.Completion; using MonoDevelop.Xml.Parser; -using MonoDevelop.Xml.Tests.Parser; using NUnit.Framework; using NUnit.Framework.Internal; @@ -14,95 +14,116 @@ namespace MonoDevelop.Xml.Tests.Completion public class CompletionTriggerTests { [Test] - // params are: document text, typedChar, trigger reason, trigger result, length + // params are: document text, typedChar, trigger reason, trigger result // typedChar, trigger reason and length can be omitted // typedChar defaults to \0 // trigger reason defaults to insertion if typedChar is non-null, else invocation - // length defaults to 0 - // if typedChar is provided, it's added to the document text - [TestCase ("", XmlCompletionTrigger.ElementValue, 0)] - [TestCase("<", XmlCompletionTrigger.Tag, 0, XmlReadForward.TagStart)] - [TestCase("", XmlTriggerReason.Backspace, XmlCompletionTrigger.None, 0)] - [TestCase("<", 'a', XmlCompletionTrigger.ElementName, 1)] - [TestCase(" (out T result, T defaultVal) where T: notnull - { - if (argIdx < args.Length && args[argIdx] is T r) { - argIdx++; - result = r; - return true; - } - result = defaultVal; - return false; - } //first arg is the doc - string doc = (string)args[argIdx++]; + if (args.Length == 0 || args[argIdx++] is not string doc) { + throw new ArgumentException ("First argument must be an string"); + } + if (argIdx < args.Length && args[argIdx] is XmlTriggerReason triggerReason) { + argIdx++; + } else { + triggerReason = XmlTriggerReason.Invocation; + } + if (argIdx != args.Length - 1 || args[argIdx] is not XmlCompletionTrigger expectedTrigger) { + throw new ArgumentException ("Last argument must be an XmlCompletionTrigger"); + } - //next arg can be typed char or a trigger reason - XmlTriggerReason reason; - if (TryGetArg (out char typedChar, defaultVal: '\0')) { - reason = XmlTriggerReason.TypedChar; - } else{ - TryGetArg (out reason, defaultVal: XmlTriggerReason.Invocation); + var caretMarkerIndex = doc.IndexOf ('|'); + if (caretMarkerIndex > -1) { + doc = doc.Remove (caretMarkerIndex, 1); + } + var spanStartMarkerIndex = doc.IndexOf ('^'); + if (spanStartMarkerIndex > -1) { + doc = doc.Remove (spanStartMarkerIndex, 1); + if (caretMarkerIndex > -1 && caretMarkerIndex >= spanStartMarkerIndex) { + caretMarkerIndex--; + } + } + var spanEndMarkerIndex = doc.IndexOf ('$'); + if (spanEndMarkerIndex > -1) { + doc = doc.Remove (spanEndMarkerIndex, 1); + if (caretMarkerIndex > -1 && caretMarkerIndex >= spanEndMarkerIndex) { + caretMarkerIndex--; + } + if (spanStartMarkerIndex > -1 && spanStartMarkerIndex >= spanEndMarkerIndex) { + spanStartMarkerIndex--; + } } - int triggerPos = doc.Length; + var triggerPos = caretMarkerIndex > -1 ? caretMarkerIndex : doc.Length; - XmlCompletionTrigger expectedTrigger = (XmlCompletionTrigger) args[argIdx++]; - TryGetArg (out int expectedSpanStart, defaultVal: triggerPos); - TryGetArg (out XmlReadForward expectedReadForward, defaultVal: XmlReadForward.None); + int expectedSpanStart = spanStartMarkerIndex > -1 ? spanStartMarkerIndex : (triggerReason == XmlTriggerReason.TypedChar)? triggerPos - 1 : triggerPos; - if (typedChar != '\0') { - doc += typedChar; - } + int expectedSpanEnd = spanEndMarkerIndex > -1 ? spanEndMarkerIndex : triggerPos; + int expectedSpanLength = expectedSpanEnd - expectedSpanStart; + + char typedChar = triggerReason == XmlTriggerReason.TypedChar ? doc[triggerPos - 1] : '\0'; var spine = new XmlSpineParser (new XmlRootState ()); - spine.Parse (doc); + for (int i = spine.Position; i < triggerPos; i++) { + spine.Push (doc [i]); + } + + var result = XmlCompletionTriggering.GetTriggerAndSpan (spine, triggerReason, typedChar, new StringTextSource (doc)); - var result = XmlCompletionTriggering.GetTrigger (spine, reason, typedChar); Assert.AreEqual (expectedTrigger, result.kind); if (expectedTrigger != XmlCompletionTrigger.None) { Assert.AreEqual (expectedSpanStart, result.spanStart); - Assert.AreEqual (expectedReadForward, result.spanReadForward); + Assert.AreEqual (expectedSpanLength, result.spanLength); } } } diff --git a/Core/Completion/XmlCompletionTriggering.cs b/Core/Completion/XmlCompletionTriggering.cs index dd24cc51..775fda2a 100644 --- a/Core/Completion/XmlCompletionTriggering.cs +++ b/Core/Completion/XmlCompletionTriggering.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Diagnostics; using MonoDevelop.Xml.Dom; using MonoDevelop.Xml.Parser; @@ -9,8 +10,17 @@ namespace MonoDevelop.Xml.Editor.Completion { class XmlCompletionTriggering { + public static XmlCompletionTrigger GetTrigger (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter) => GetTriggerAndIncompleteSpan (parser, reason, typedCharacter).kind; + + public static (XmlCompletionTrigger kind, int spanStart, int spanLength) GetTriggerAndSpan (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter, ITextSource spanReadForwardTextSource) + { + var result = GetTriggerAndIncompleteSpan (parser, reason, typedCharacter); + var spanLength = GetReadForwardLength (spanReadForwardTextSource, parser, result.spanStart, result.spanReadForward); + return (result.kind, result.spanStart, spanLength); + } + //FIXME: the length should do a readahead to capture the whole token - public static (XmlCompletionTrigger kind, int spanStart, XmlReadForward spanReadForward) GetTrigger (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter) + static (XmlCompletionTrigger kind, int spanStart, XmlReadForward spanReadForward) GetTriggerAndIncompleteSpan (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter) { int triggerPosition = parser.Position; bool isExplicit = reason == XmlTriggerReason.Invocation; @@ -51,7 +61,7 @@ public static (XmlCompletionTrigger kind, int spanStart, XmlReadForward spanRead //doctype/cdata completion, explicit trigger after 0 && text[text.Length - 1] == '&') { - return (XmlCompletionTrigger.Entity, triggerPosition - 1, isExplicit ? XmlReadForward.Entity : XmlReadForward.None); + return (XmlCompletionTrigger.Entity, triggerPosition - 1, XmlReadForward.Entity); + } + + if (isTypedChar && XmlChar.IsFirstNameChar (typedCharacter) && text.Length > 1 && text[text.Length - 2] == '&') { + return (XmlCompletionTrigger.Entity, triggerPosition - 1, XmlReadForward.None); } if (isExplicit) { @@ -115,19 +129,63 @@ context.CurrentState is XmlTextState return (XmlCompletionTrigger.None, triggerPosition, XmlReadForward.None); } - } - /// - /// Describes how to read forward from the completion span start to get the completion span - /// - enum XmlReadForward - { - None, - XmlName, - TagStart, - DocType, - AttributeValue, - Entity + /// + /// Describes how to read forward from the completion span start to get the completion span + /// + enum XmlReadForward + { + None, + XmlName, + TagStart, + DocType, + AttributeValue, + Entity + } + + //TODO: support the other readforward types + static int GetReadForwardLength (ITextSource textSource, XmlSpineParser spine, int spanStart, XmlReadForward spanReadForward) + { + int triggerPosition = spine.Position; + switch (spanReadForward) { + case XmlReadForward.XmlName: + return textSource.GetXNameLengthAtPosition (spanStart, triggerPosition); + + case XmlReadForward.AttributeValue: + var attributeDelimiter = spine.GetAttributeValueDelimiter () ?? '\0'; + return textSource.GetAttributeValueLengthAtPosition (attributeDelimiter, spanStart, triggerPosition); + + case XmlReadForward.TagStart: + int existingLength = triggerPosition - spanStart; + foreach (string specialTag in specialStartTags) { + if (specialTag.Length >= existingLength) { + int max = Math.Min (spanStart + specialTag.Length, textSource.Length); + for (int i = spanStart; i < max; i++) { + int specialTagIndex = i - spanStart; + if (textSource[i] != specialTag[specialTagIndex]) { + break; + } + if (specialTagIndex + 1 == specialTag.Length) { + return specialTag.Length; + } + } + } + } + return textSource.GetXNameLengthAtPosition (spanStart, triggerPosition); + + case XmlReadForward.DocType: + case XmlReadForward.Entity: + case XmlReadForward.None: + return triggerPosition - spanStart; + default: + throw new ArgumentException ("Unsupported XmlReadForward value", nameof (spanReadForward)); + } + } + + static string[] specialStartTags = new[] { + " /// Advances the parser until the specified object is closed i.e. has a closing tag. /// diff --git a/Editor/Completion/XmlCompletionSource.cs b/Editor/Completion/XmlCompletionSource.cs index 17796daf..ed5f2e04 100644 --- a/Editor/Completion/XmlCompletionSource.cs +++ b/Editor/Completion/XmlCompletionSource.cs @@ -87,7 +87,7 @@ public async virtual Task GetCompletionContextAsync (IAsyncCo var parser = GetSpineParser (triggerLocation); // FIXME: cache the value from InitializeCompletion somewhere? - var (kind, _, _) = XmlCompletionTriggering.GetTrigger (parser, reason.Value, trigger.Character); + var kind = XmlCompletionTriggering.GetTrigger (parser, reason.Value, trigger.Character); if (kind == XmlCompletionTrigger.None) { yield break; @@ -160,16 +160,7 @@ public virtual CompletionStartData InitializeCompletion (CompletionTrigger trigg LogAttemptingCompletion (Logger, spine.CurrentState, spine.CurrentStateLength, trigger.Character, trigger.Reason); - var (kind, spanStart, spanReadForward) = XmlCompletionTriggering.GetTrigger (spine, reason.Value, trigger.Character); - - int spanLength; - if (spanReadForward == XmlReadForward.XmlName) { - spanLength = new SnapshotTextSource (triggerLocation.Snapshot).GetXNameLengthAtPosition (spanStart, triggerLocation.Position); - } else { - spanLength = triggerLocation.Position - spanStart; - } - - //TODO: handle spanReadforward + var (kind, spanStart, spanLength) = XmlCompletionTriggering.GetTriggerAndSpan (spine, reason.Value, trigger.Character, new SnapshotTextSource (triggerLocation.Snapshot)); if (kind != XmlCompletionTrigger.None) { return new CompletionStartData (CompletionParticipation.ProvidesItems, new SnapshotSpan (triggerLocation.Snapshot, spanStart, spanLength)); @@ -265,10 +256,10 @@ CancellationToken token CompletionItem cdataItemWithBracket, commentItemWithBracket, prologItemWithBracket; CompletionItem[] entityItems; - [MemberNotNull( - nameof(cdataItem), nameof (commentItem), nameof (prologItem), - nameof(cdataItemWithBracket), nameof (commentItemWithBracket), nameof (prologItemWithBracket), - nameof(entityItems))] + [MemberNotNull ( + nameof (cdataItem), nameof (commentItem), nameof (prologItem), + nameof (cdataItemWithBracket), nameof (commentItemWithBracket), nameof (prologItemWithBracket), + nameof (entityItems))] void InitializeBuiltinItems () { cdataItem = new CompletionItem ("![CDATA[", this, XmlImages.CData) @@ -306,10 +297,13 @@ void InitializeBuiltinItems () }; //TODO: need to tweak semicolon insertion dor XmlCompletionItemKind.Entity - CompletionItem EntityItem (string name, string character) => - new CompletionItem (name, this, XmlImages.Entity, ImmutableArray.Empty, string.Empty, name, name, character, ImmutableArray.Empty) - .AddEntityDocumentation (character) - .AddKind (XmlCompletionItemKind.Entity); + CompletionItem EntityItem (string name, string character) + { + name = $"&{name};"; + return new CompletionItem (name, this, XmlImages.Entity, ImmutableArray.Empty, string.Empty, name, name, character, ImmutableArray.Empty) + .AddEntityDocumentation (character) + .AddKind (XmlCompletionItemKind.Entity); + } } /// @@ -332,7 +326,7 @@ protected IEnumerable GetMiscellaneousTags (SnapshotPoint trigge } } - protected IEnumerable GetBuiltInEntityItems () => entityItems; + protected IList GetBuiltInEntityItems () => entityItems; IEnumerable GetClosingTags (List nodePath, bool includeBracket) {