From 867f88e313987e74b30f3735f098ba53f39f090a Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 10 Oct 2023 17:49:22 -0400 Subject: [PATCH] Add extensible context on XmlCompletionSource The context is created upfront and passed to all GetCompletionsAsync methods. Derived completion sources can subclass the context to attach additional information. Entrypoints now have exception logging. --- Core/Completion/XmlCompletionTriggering.cs | 3 +- .../Completion/XmlCompletionTestSource.cs | 25 +-- Editor.Tests/XmlTestEnvironment.cs | 2 +- Editor/Completion/XmlCompletionSource.cs | 198 +++++++----------- .../Completion/XmlCompletionTriggerContext.cs | 94 +++++++++ 5 files changed, 184 insertions(+), 138 deletions(-) create mode 100644 Editor/Completion/XmlCompletionTriggerContext.cs diff --git a/Core/Completion/XmlCompletionTriggering.cs b/Core/Completion/XmlCompletionTriggering.cs index 4b9836e..b75f1ba 100644 --- a/Core/Completion/XmlCompletionTriggering.cs +++ b/Core/Completion/XmlCompletionTriggering.cs @@ -220,6 +220,7 @@ public enum XmlTriggerReason { Invocation, TypedChar, - Backspace + Backspace, + Unknown } } diff --git a/Editor.Tests/Completion/XmlCompletionTestSource.cs b/Editor.Tests/Completion/XmlCompletionTestSource.cs index f6c2659..b3bc616 100644 --- a/Editor.Tests/Completion/XmlCompletionTestSource.cs +++ b/Editor.Tests/Completion/XmlCompletionTestSource.cs @@ -21,6 +21,7 @@ using MonoDevelop.Xml.Editor.Completion; using MonoDevelop.Xml.Editor.Logging; using MonoDevelop.Xml.Editor.Parsing; +using MonoDevelop.Xml.Parser; namespace MonoDevelop.Xml.Editor.Tests.Completion { @@ -47,35 +48,24 @@ public IAsyncCompletionSource GetOrCreate (ITextView textView) } } - class XmlCompletionTestSource : XmlCompletionSource + class XmlCompletionTestSource : XmlCompletionSource { public XmlCompletionTestSource (ITextView textView, ILogger logger, XmlParserProvider parserProvider) : base (textView, logger, parserProvider) { } - protected override Task?> GetElementCompletionsAsync ( - IAsyncCompletionSession session, - SnapshotPoint triggerLocation, - List nodePath, - bool includeBracket, - CancellationToken token) + protected override Task?> GetElementCompletionsAsync (XmlCompletionTriggerContext context, bool includeBracket, CancellationToken token) { var item = new CompletionItem (includeBracket? " () { item }; - items.AddRange (GetMiscellaneousTags (triggerLocation, nodePath, includeBracket)); + items.AddRange (GetMiscellaneousTags (context.TriggerLocation, context.NodePath, includeBracket)); return Task.FromResult?> (items); } - protected override Task?> GetAttributeCompletionsAsync ( - IAsyncCompletionSession session, - SnapshotPoint triggerLocation, - List nodePath, - IAttributedXObject attributedObject, - Dictionary existingAtts, - CancellationToken token) + protected override Task?> GetAttributeCompletionsAsync (XmlCompletionTriggerContext context, IAttributedXObject attributedObject, Dictionary existingAtts, CancellationToken token) { - if (nodePath.LastOrDefault () is XElement xel && xel.NameEquals ("Hello", true)) { + if (context.NodePath.LastOrDefault () is XElement xel && xel.NameEquals ("Hello", true)) { var item = new CompletionItem ("There", this) .AddKind (XmlCompletionItemKind.Attribute); var items = new List () { item }; @@ -84,5 +74,8 @@ public XmlCompletionTestSource (ITextView textView, ILogger logger, XmlParserPro return Task.FromResult?> (null); } + + protected override XmlCompletionTriggerContext CreateTriggerContext (IAsyncCompletionSession session, CompletionTrigger trigger, XmlSpineParser spineParser, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan) + => new (session, triggerLocation, spineParser, trigger, applicableToSpan); } } diff --git a/Editor.Tests/XmlTestEnvironment.cs b/Editor.Tests/XmlTestEnvironment.cs index 52856ec..5380a61 100644 --- a/Editor.Tests/XmlTestEnvironment.cs +++ b/Editor.Tests/XmlTestEnvironment.cs @@ -110,7 +110,7 @@ protected virtual async Task OnInitialize () protected virtual IEnumerable GetAssembliesToCompose () => new[] { typeof (XmlParser).Assembly.Location, - typeof (XmlCompletionSource).Assembly.Location, + typeof (XmlCompletionSource<>).Assembly.Location, typeof (XmlTestEnvironment).Assembly.Location }; diff --git a/Editor/Completion/XmlCompletionSource.cs b/Editor/Completion/XmlCompletionSource.cs index 8c9c653..d68aba4 100644 --- a/Editor/Completion/XmlCompletionSource.cs +++ b/Editor/Completion/XmlCompletionSource.cs @@ -25,11 +25,12 @@ using MonoDevelop.Xml.Dom; using MonoDevelop.Xml.Editor.Parsing; +using MonoDevelop.Xml.Logging; using MonoDevelop.Xml.Parser; namespace MonoDevelop.Xml.Editor.Completion { - public abstract partial class XmlCompletionSource : IAsyncCompletionSource + public abstract partial class XmlCompletionSource : IAsyncCompletionSource where TCompletionTriggerContext : XmlCompletionTriggerContext { protected XmlParserProvider XmlParserProvider { get; } @@ -42,7 +43,7 @@ protected XmlCompletionSource (ITextView textView, ILogger logger, XmlParserProv XmlParserProvider = parserProvider; TextView = textView; Logger = logger; - InitializeBuiltinItems (); + InitializeBuiltInItems (); } protected XmlBackgroundParser GetParser (ITextBuffer textBuffer) => XmlParserProvider.GetParser (textBuffer); @@ -54,11 +55,23 @@ protected XmlSpineParser GetSpineParser (SnapshotPoint snapshotPoint) return spineParser; } - public async virtual Task GetCompletionContextAsync (IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token) + public Task GetCompletionContextAsync (IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token) + => Logger.InvokeAndLogExceptions (() => GetCompletionContextAsyncInternal (session, trigger, triggerLocation, applicableToSpan, token)); + + async Task GetCompletionContextAsyncInternal (IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token) { - var tasks = GetCompletionTasks (session, trigger, triggerLocation, applicableToSpan, token).ToList (); + var spineParser = GetSpineParser (triggerLocation); + var triggerContext = CreateTriggerContext (session, trigger, spineParser, triggerLocation, applicableToSpan); + + if (!triggerContext.IsSupportedTriggerReason) { + return CompletionContext.Empty; + } - await Task.WhenAll (tasks); + await triggerContext.InitializeNodePath (Logger, token).ConfigureAwait (false); + + var tasks = GetCompletionTasks (triggerContext, token).ToList (); + + await Task.WhenAll (tasks).ConfigureAwait (false); var allItems = ImmutableArray.Empty; foreach (var task in tasks) { @@ -76,89 +89,83 @@ public async virtual Task GetCompletionContextAsync (IAsyncCo return new CompletionContext (allItems, null, InitialSelectionHint.SoftSelection); } - IEnumerable?>> GetCompletionTasks (IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken cancellationToken) - { - yield return GetAdditionalCompletionsAsync (session, trigger, triggerLocation, applicableToSpan, cancellationToken); - - var reason = ConvertReason (trigger.Reason, trigger.Character); - if (reason == null) { - yield break; - } - - var parser = GetSpineParser (triggerLocation); - - // FIXME: cache the value from InitializeCompletion somewhere? - var kind = XmlCompletionTriggering.GetTrigger (parser, reason.Value, trigger.Character); + /// + /// Construct a context that gathers computed information about the current completion trigger point. + /// + protected abstract TCompletionTriggerContext CreateTriggerContext (IAsyncCompletionSession session, CompletionTrigger trigger, XmlSpineParser spineParser, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan); - if (kind == XmlCompletionTrigger.None) { - yield break; - } + IEnumerable?>> GetCompletionTasks (TCompletionTriggerContext triggerContext, CancellationToken cancellationToken) + { + yield return GetAdditionalCompletionsAsync (triggerContext, cancellationToken); - if (!parser.TryGetNodePath (triggerLocation.Snapshot, out List? nodePath, cancellationToken: cancellationToken)) { + if (triggerContext.XmlTriggerKind == XmlCompletionTrigger.None) { yield break; } - session.Properties.AddProperty (typeof (XmlCompletionTrigger), kind); + // this is used by XmlCompletionCommitManager.ShouldCommitCompletion to determine whether XmlCompletionSource participated in the session and how the completion should be committed + triggerContext.Session.Properties[typeof (XmlCompletionTrigger)] = triggerContext.XmlTriggerKind; - switch (kind) { + switch (triggerContext.XmlTriggerKind) { case XmlCompletionTrigger.ElementValue: - yield return GetElementValueCompletionsAsync (session, triggerLocation, nodePath, cancellationToken); + yield return GetElementValueCompletionsAsync (triggerContext, cancellationToken); goto case XmlCompletionTrigger.Tag; 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) { - 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) { + if (triggerContext.SpineParser.Spine.TryFind (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); + triggerContext.SpineParser.Clone ().AdvanceUntilEnded ((XObject)attributedOb, triggerContext.TriggerLocation.Snapshot, 1000); var attributes = attributedOb.Attributes.ToDictionary (StringComparer.OrdinalIgnoreCase); - yield return GetAttributeCompletionsAsync (session, triggerLocation, nodePath, attributedOb, attributes, cancellationToken); + yield return GetAttributeCompletionsAsync (triggerContext, attributedOb, attributes, cancellationToken); break; 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, cancellationToken); + if (triggerContext.SpineParser.Spine.TryPeek (out XAttribute? att) && triggerContext.SpineParser.Spine.TryPeek (1, out IAttributedXObject? attributedObject)) { + yield return GetAttributeValueCompletionsAsync (triggerContext, attributedObject, att, cancellationToken); } break; case XmlCompletionTrigger.Entity: - yield return GetEntityCompletionsAsync (session, triggerLocation, nodePath, cancellationToken); + yield return GetEntityCompletionsAsync (triggerContext, cancellationToken); break; case XmlCompletionTrigger.DocType: case XmlCompletionTrigger.DeclarationOrCDataOrComment: - yield return GetDeclarationCompletionsAsync (session, triggerLocation, nodePath, cancellationToken); + yield return GetDeclarationCompletionsAsync (triggerContext, cancellationToken); break; } } - public virtual Task GetDescriptionAsync (IAsyncCompletionSession session, CompletionItem item, CancellationToken token) => item.GetDocumentationAsync (session, token); + public Task GetDescriptionAsync (IAsyncCompletionSession session, CompletionItem item, CancellationToken token) + => Logger.InvokeAndLogExceptions (() => item.GetDocumentationAsync (session, token)); - public virtual CompletionStartData InitializeCompletion (CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) + public CompletionStartData InitializeCompletion (CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) + => Logger.InvokeAndLogExceptions ( + () => { + var spine = GetSpineParser (triggerLocation); + return InitializeCompletion (trigger, triggerLocation, spine, token); + }); + + /// + /// Determine whether the current location is a completion trigger point, and what its span is. Runs on the UI thread so must be fast. + /// + protected virtual CompletionStartData InitializeCompletion (CompletionTrigger trigger, SnapshotPoint triggerLocation, XmlSpineParser spineParser, CancellationToken token) { - var reason = ConvertReason (trigger.Reason, trigger.Character); - if (reason == null) { + var reason = XmlCompletionTriggerContext.ConvertReason (trigger.Reason, trigger.Character); + if (reason == XmlTriggerReason.Unknown) { return CompletionStartData.DoesNotParticipateInCompletion; } - var spine = GetSpineParser (triggerLocation); - - LogAttemptingCompletion (Logger, spine.CurrentState, spine.CurrentStateLength, trigger.Character, trigger.Reason); + LogAttemptingCompletion (Logger, spineParser.CurrentState, spineParser.CurrentStateLength, trigger.Character, trigger.Reason); - var (kind, spanStart, spanLength) = XmlCompletionTriggering.GetTriggerAndSpan (spine, reason.Value, trigger.Character, new SnapshotTextSource (triggerLocation.Snapshot)); + var (kind, spanStart, spanLength) = XmlCompletionTriggering.GetTriggerAndSpan (spineParser, reason, trigger.Character, new SnapshotTextSource (triggerLocation.Snapshot)); if (kind != XmlCompletionTrigger.None) { return new CompletionStartData (CompletionParticipation.ProvidesItems, new SnapshotSpan (triggerLocation.Snapshot, spanStart, spanLength)); @@ -172,83 +179,34 @@ 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 ( - IAsyncCompletionSession session, - SnapshotPoint triggerLocation, - List nodePath, - bool includeBracket, - CancellationToken token - ) - => Task.FromResult?> (null); - - protected virtual Task?> GetAttributeCompletionsAsync ( - IAsyncCompletionSession session, - SnapshotPoint triggerLocation, - List nodePath, - IAttributedXObject attributedObject, - Dictionary existingAtts, - CancellationToken token - ) - => Task.FromResult?> (null); - - protected virtual Task?> GetAttributeValueCompletionsAsync ( - IAsyncCompletionSession session, - SnapshotPoint triggerLocation, - List nodePath, - IAttributedXObject attributedObject, - XAttribute attribute, - CancellationToken token - ) - => Task.FromResult?> (null); - - protected virtual Task?> GetEntityCompletionsAsync ( - IAsyncCompletionSession session, - SnapshotPoint triggerLocation, - List nodePath, - CancellationToken token - ) - => Task.FromResult?> (null); - - protected virtual Task?> GetDeclarationCompletionsAsync ( - IAsyncCompletionSession session, - SnapshotPoint triggerLocation, - List nodePath, - CancellationToken token - ) - => Task.FromResult?> ( - nodePath.Any (n => n is XElement) + protected virtual Task?> GetElementCompletionsAsync (TCompletionTriggerContext context, bool includeBracket, CancellationToken token) + => TaskCompleted (null); + + protected virtual Task?> GetAttributeCompletionsAsync (TCompletionTriggerContext context, IAttributedXObject attributedObject, Dictionary existingAttributes, CancellationToken token) + => TaskCompleted (null); + + protected virtual Task?> GetAttributeValueCompletionsAsync (TCompletionTriggerContext context, IAttributedXObject attributedObject, XAttribute attribute, CancellationToken token) + => TaskCompleted (null); + + protected virtual Task?> GetEntityCompletionsAsync (TCompletionTriggerContext context, CancellationToken token) + => TaskCompleted (null); + + protected virtual Task?> GetDeclarationCompletionsAsync (TCompletionTriggerContext context, CancellationToken token) + => TaskCompleted ( + context.NodePath.Any (n => n is XElement) ? new [] { cdataItemWithBracket, commentItemWithBracket } : new [] { commentItemWithBracket } ); - protected virtual Task?> GetElementValueCompletionsAsync ( - IAsyncCompletionSession session, - SnapshotPoint triggerLocation, - List nodePath, - CancellationToken token) => Task.FromResult?> (null); + protected virtual Task?> GetElementValueCompletionsAsync (TCompletionTriggerContext context, CancellationToken token) + => TaskCompleted (null); - protected virtual Task?> GetAdditionalCompletionsAsync ( - IAsyncCompletionSession session, - CompletionTrigger trigger, - SnapshotPoint triggerLocation, - SnapshotSpan applicableToSpan, - CancellationToken token) => Task.FromResult?> (null); + /// + /// Get additional completions that are not handled by the XmlCompletionSource. + protected virtual Task?> GetAdditionalCompletionsAsync (TCompletionTriggerContext context, CancellationToken token) + => TaskCompleted (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; - } + static Task?> TaskCompleted (IList? items) => Task.FromResult (items); CompletionItem cdataItem, commentItem, prologItem; CompletionItem cdataItemWithBracket, commentItemWithBracket, prologItemWithBracket; @@ -258,7 +216,7 @@ CancellationToken token nameof (cdataItem), nameof (commentItem), nameof (prologItem), nameof (cdataItemWithBracket), nameof (commentItemWithBracket), nameof (prologItemWithBracket), nameof (entityItems))] - void InitializeBuiltinItems () + void InitializeBuiltInItems () { cdataItem = new CompletionItem ("![CDATA[", this, XmlImages.CData) .AddDocumentation ("XML character data") diff --git a/Editor/Completion/XmlCompletionTriggerContext.cs b/Editor/Completion/XmlCompletionTriggerContext.cs new file mode 100644 index 0000000..8e2fa3d --- /dev/null +++ b/Editor/Completion/XmlCompletionTriggerContext.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +#if NETFRAMEWORK +#nullable disable warnings +#endif + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; + +using MonoDevelop.Xml.Dom; +using MonoDevelop.Xml.Parser; + +namespace MonoDevelop.Xml.Editor.Completion; + +/// +/// Encapsulates the context of completion triggering in so that +/// subclassed completion sources can augment it with additional information. +/// +public class XmlCompletionTriggerContext +{ + public XmlCompletionTriggerContext (IAsyncCompletionSession session, SnapshotPoint triggerLocation, XmlSpineParser spineParser, CompletionTrigger trigger, SnapshotSpan applicableToSpan) + { + Session = session; + TriggerLocation = triggerLocation; + SpineParser = spineParser; + Trigger = trigger; + ApplicableToSpan = applicableToSpan; + + XmlTriggerReason = ConvertReason (trigger.Reason, trigger.Character); + + // FIXME: cache the value from InitializeCompletion somewhere? + XmlTriggerKind = XmlCompletionTriggering.GetTrigger (spineParser, XmlTriggerReason, trigger.Character); + } + + /// + /// Initializes the node path. Only called if is true. + /// + public virtual Task InitializeNodePath (ILogger logger, CancellationToken cancellationToken) + { + SpineParser.TryGetNodePath (TriggerLocation.Snapshot, out List? nodePath, cancellationToken: cancellationToken); + NodePath = nodePath; + + // if we're completing an existing element, remove it from the path + // so we don't get completions for its children instead + if ((XmlTriggerKind == XmlCompletionTrigger.ElementName || XmlTriggerKind == XmlCompletionTrigger.Tag) && nodePath.Count > 0) { + if (nodePath[nodePath.Count - 1] is XElement leaf && leaf.Name.Length == ApplicableToSpan.Length) { + nodePath.RemoveAt (nodePath.Count - 1); + } + } + + return Task.CompletedTask; + } + + /// + /// Whether the is supported by this completion source. + /// + public virtual bool IsSupportedTriggerReason => XmlTriggerKind != XmlCompletionTrigger.None; + + public IAsyncCompletionSession Session { get; } + public SnapshotPoint TriggerLocation { get; } + public XmlSpineParser SpineParser { get; } + public CompletionTrigger Trigger { get; } + public SnapshotSpan ApplicableToSpan { get; } + + internal XmlCompletionTrigger XmlTriggerKind { get; } + internal XmlTriggerReason XmlTriggerReason { get; } + + public List NodePath { get; private set; } + + internal 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 XmlTriggerReason.Unknown; + } +}