diff --git a/Core.Tests/Completion/CompletionTriggerTests.cs b/Core.Tests/Completion/CompletionTriggerTests.cs index 3b05da25..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,91 +14,117 @@ 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.ElementWithBracket)] - [TestCase("<", XmlCompletionTrigger.Element)] + // the document text may use the following optional marker chars: + // | - cursor position, if not provided, defaults to end of document + // ^ - span start. if not provided, defaults to cursor position + // $ - span end. if not provided, defaults to cursor position, unless trigger reason is 'insertion', in which case it defaults to cursor position + 1 + [TestCase ("", XmlCompletionTrigger.ElementValue)] + [TestCase("^<", XmlCompletionTrigger.Tag)] [TestCase("", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)] - [TestCase("<", 'a', XmlCompletionTrigger.Element, 1)] - [TestCase(" -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--; + } + } - //length is optional, but if provided it's always last - int expectedLength = args[args.Length - 1] as int? ?? 0; + var triggerPos = caretMarkerIndex > -1 ? caretMarkerIndex : doc.Length; - if (typedChar != '\0') { - doc += typedChar; - } + int expectedSpanStart = spanStartMarkerIndex > -1 ? spanStartMarkerIndex : (triggerReason == XmlTriggerReason.TypedChar)? triggerPos - 1 : triggerPos; + + 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); - Assert.AreEqual (expectedLength, result.length); + if (expectedTrigger != XmlCompletionTrigger.None) { + Assert.AreEqual (expectedSpanStart, result.spanStart); + Assert.AreEqual (expectedSpanLength, result.spanLength); + } } } } diff --git a/Core/Completion/XmlCompletionTriggering.cs b/Core/Completion/XmlCompletionTriggering.cs index 488b3eb9..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,9 +10,19 @@ 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 length) 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; bool isTypedChar = reason == XmlTriggerReason.TypedChar; bool isBackspace = reason == XmlTriggerReason.Backspace; @@ -23,78 +34,83 @@ 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; - 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, XmlReadForward.Entity); + } + + if (isTypedChar && XmlChar.IsFirstNameChar (typedCharacter) && text.Length > 1 && text[text.Length - 2] == '&') { + return (XmlCompletionTrigger.Entity, triggerPosition - 1, 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 +124,80 @@ public static (XmlCompletionTrigger kind, int length) GetTrigger (XmlSpineParser context.CurrentState is XmlTextState || XmlRootState.IsFree (context) )) { - return (XmlCompletionTrigger.ElementWithBracket, 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 + } + + //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[] { + "An XML tag, which may be an element with leading angle bracket, comment etc + Tag, + + ElementName, + ElementValue, + AttributeName, AttributeValue, Entity, DocType, diff --git a/Core/Parser/XmlParserTextSourceExtensions.cs b/Core/Parser/XmlParserTextSourceExtensions.cs index 13a8da5b..04bad584 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,60 @@ 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 currentPosition - nameStartPosition; + } + + public static int GetAttributeValueLengthAtPosition (this ITextSource text, char delimiter, int attributeStartPosition, 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 (; currentPosition < limit; currentPosition++) { + char c = text[currentPosition]; + if (XmlChar.IsInvalid (c) || c == '<') { + return currentPosition - attributeStartPosition; + } + switch (delimiter) { + case '\'': + case '"': + if (c == delimiter) { + return currentPosition - attributeStartPosition; + } + break; + default: + if (XmlChar.IsWhitespace (c)) { + return currentPosition - attributeStartPosition; + } + break; + } } - return new XName (text.GetText (start, end - start)); + + return currentPosition - attributeStartPosition; } /// 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 db82dc0d..a445cb6c 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.ElementWithBracket: + 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) { @@ -91,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; diff --git a/Editor/Completion/XmlCompletionSource.cs b/Editor/Completion/XmlCompletionSource.cs index 3e5372af..ed5f2e04 100644 --- a/Editor/Completion/XmlCompletionSource.cs +++ b/Editor/Completion/XmlCompletionSource.cs @@ -53,82 +53,92 @@ 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); // 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) { - 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); + if (kind == XmlCompletionTrigger.None) { + yield break; + } - 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); + List nodePath = parser.GetNodePath (triggerLocation.Snapshot); + session.Properties.AddProperty (typeof (XmlCompletionTrigger), kind); + + switch (kind) { + case XmlCompletionTrigger.ElementValue: + yield return GetElementValueCompletionsAsync (session, triggerLocation, nodePath, token); + 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); } - 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 ( @@ -150,9 +160,10 @@ 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, 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, triggerLocation.Position - length, length)); + return new CompletionStartData (CompletionParticipation.ProvidesItems, new SnapshotSpan (triggerLocation.Snapshot, spanStart, spanLength)); } //TODO: closing tag completion after typing > @@ -163,17 +174,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 +191,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,41 +201,65 @@ 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; 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) @@ -263,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); + } } /// @@ -289,7 +326,7 @@ protected IEnumerable GetMiscellaneousTags (SnapshotPoint trigge } } - protected IEnumerable GetBuiltInEntityItems () => entityItems; + protected IList GetBuiltInEntityItems () => entityItems; IEnumerable GetClosingTags (List nodePath, bool includeBracket) { @@ -310,7 +347,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); 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 . 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;