diff --git a/Core.Tests/MonoDevelop.Xml.Core.Tests.csproj b/Core.Tests/MonoDevelop.Xml.Core.Tests.csproj index 2a0eb1fc..703bf724 100644 --- a/Core.Tests/MonoDevelop.Xml.Core.Tests.csproj +++ b/Core.Tests/MonoDevelop.Xml.Core.Tests.csproj @@ -1,9 +1,7 @@ - net8.0 - - net48;net8.0 + net48;net8.0 true MonoDevelop.Xml.Tests @@ -13,6 +11,12 @@ annotations + + + False + False + + @@ -26,11 +30,12 @@ + + - diff --git a/Core.Tests/Parser/ParsingTests.cs b/Core.Tests/Parser/ParsingTests.cs index f3098f8c..615d415c 100644 --- a/Core.Tests/Parser/ParsingTests.cs +++ b/Core.Tests/Parser/ParsingTests.cs @@ -515,6 +515,13 @@ public void SpineParserRecoveryXhtmlStrictSchema () using var sr = new StreamReader (ResourceManager.GetXhtmlStrictSchema ()); var docTxt = sr.ReadToEnd (); + SpineParserRecovery (docTxt, 1127391); + } + + [TestCase("foo\\", 25)] + [TestCase("", 130)] + public void SpineParserRecovery (string docTxt, int expectedDelta) + { var rootState = CreateRootState (); var treeParser = new XmlTreeParser (rootState); foreach (char c in docTxt) { @@ -530,7 +537,7 @@ public void SpineParserRecoveryXhtmlStrictSchema () char c = docTxt[i]; spineParser.Push (c); - var recoveredParser = XmlSpineParser.FromDocumentPosition (rootState, doc, i).AssertNotNull (); + var recoveredParser = XmlSpineParser.FromDocumentPosition (rootState, doc, spineParser.Position).AssertNotNull (); var delta = i - recoveredParser.Position; totalNotRecovered += delta; @@ -542,12 +549,14 @@ public void SpineParserRecoveryXhtmlStrictSchema () AssertEqual (spineParser.GetContext (), recoveredParser.GetContext ()); } + /* int total = docTxt.Length * docTxt.Length / 2; float recoveryRate = 1f - totalNotRecovered / (float)total; TestContext.WriteLine ($"Recovered {(recoveryRate * 100f):F2}%"); + */ // check it never regresses - Assert.LessOrEqual (totalNotRecovered, 1118088); + Assert.LessOrEqual (totalNotRecovered, expectedDelta); } [TestCase ("\r\n\r\n", "\r\n")] @@ -574,7 +583,7 @@ public void SpineParserRecoverFromError (string docTxt, string recoverFromDoc) char c = docTxt[i]; spineParser.Push (c); - var recoveredParser = XmlSpineParser.FromDocumentPosition (rootState, doc, Math.Min (i, maxCompat)).AssertNotNull (); + var recoveredParser = XmlSpineParser.FromDocumentPosition (rootState, doc, Math.Min (i, maxCompat)); var end = Math.Min (i + 1, docTxt.Length); for (int j = recoveredParser.Position; j < end; j++) { diff --git a/Core.Tests/Parser/TestXmlParser.cs b/Core.Tests/Parser/TestXmlParser.cs index d3e4ba9b..635b35a1 100644 --- a/Core.Tests/Parser/TestXmlParser.cs +++ b/Core.Tests/Parser/TestXmlParser.cs @@ -216,7 +216,7 @@ public static void AssertDiagnosticCount (this IReadOnlyList? dia var sb = new System.Text.StringBuilder (); sb.AppendLine ($"Expected {count} diagnostics, got {actualCount}:"); foreach (var err in filter is null? diagnostics : diagnostics.Where (filter)) { - sb.AppendLine ($"{err.Descriptor.Severity}@{err.Span}: {err.GetFormattedMessage ()}"); + sb.AppendLine ($"{err.Descriptor.Severity}@{err.Span}: {err.GetFormattedMessageWithTitle ()}"); } Assert.AreEqual (count, actualCount, sb.ToString ()); } diff --git a/Core.Tests/Utils/TextWithMarkers.cs b/Core.Tests/Utils/TextWithMarkers.cs index b0237b25..d9140fcf 100644 --- a/Core.Tests/Utils/TextWithMarkers.cs +++ b/Core.Tests/Utils/TextWithMarkers.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; @@ -12,12 +13,18 @@ namespace MonoDevelop.Xml.Tests.Utils; +public record struct TextMarkerPosition(int Offset, int Line, int Column) +{ + public static implicit operator int (TextMarkerPosition p) => p.Offset; + public static implicit operator TextSpan (TextMarkerPosition p) => new (p.Offset, 0); +} + /// /// Represents text with marked spans and/or positions /// public class TextWithMarkers { - TextWithMarkers (string text, char[] markerChars, List[] markedPositionsById) + TextWithMarkers (string text, char[] markerChars, List[] markedPositionsById) { Text = text; this.markerChars = markerChars; @@ -25,7 +32,7 @@ public class TextWithMarkers } readonly char[] markerChars; - readonly List[] markedPositionsById; + readonly List[] markedPositionsById; /// /// The text with the marker characters removed @@ -53,7 +60,13 @@ int GetMarkerId (char? markerChar) /// Gets all the marked positions for the specified /// /// Which marker character to use. May be null if only one marker character was specified when creating the - public IList GetMarkedPositions (char? markerChar = null) => markedPositionsById[GetMarkerId (markerChar)]; + public IReadOnlyList GetMarkedLineColPositions (char? markerChar = null) => markedPositionsById[GetMarkerId (markerChar)]; + + /// + /// Gets all the marked positions for the specified + /// + /// Which marker character to use. May be null if only one marker character was specified when creating the + public IReadOnlyList GetMarkedPositions (char? markerChar = null) => new PositionList(markedPositionsById[GetMarkerId (markerChar)]); /// /// Gets the single position marked with the specified @@ -72,11 +85,28 @@ public int GetMarkedPosition (char? markerChar = null) return position; } + /// + /// Gets the single position marked with the specified + /// + /// Which marker character to use. May be null if only one marker character was specified when creating the + /// The position + /// Did not find exactly one marker for + /// The was not specified when creating the + /// The was null but multiple markers were specified when creating the + + public TextMarkerPosition GetMarkedLineColPosition (char? markerChar = null) + { + if (!TryGetMarkedLineColPosition (out TextMarkerPosition position, markerChar)) { + ThrowExactMismatchException (markerChar, 1, "position"); + } + return position; + } + /// /// Tries to get the single position marked with the specified . /// /// Which marker character to use. May be null if only one marker character was specified when creating the - /// The position, if this method returned true, otherwise default + /// The position, if this method returned true, otherwise default /// Whether a single position was found for . More than one marker will cause an exception to be thrown /// The presence of more than one marker will cause an exception to be thrown /// More than one marker was found for @@ -84,6 +114,27 @@ public int GetMarkedPosition (char? markerChar = null) /// The was null but multiple markers were specified when creating the public bool TryGetMarkedPosition (out int position, char? markerChar = null) + { + if (TryGetMarkedLineColPosition(out var p, markerChar)) { + position = p; + return true; + } + position = default; + return false; + } + + /// + /// Tries to get the single position marked with the specified . + /// + /// Which marker character to use. May be null if only one marker character was specified when creating the + /// The position, if this method returned true, otherwise default + /// Whether a single position was found for . More than one marker will cause an exception to be thrown + /// The presence of more than one marker will cause an exception to be thrown + /// More than one marker was found for + /// The was not specified when creating the + /// The was null but multiple markers were specified when creating the + + public bool TryGetMarkedLineColPosition (out TextMarkerPosition position, char? markerChar = null) { var id = GetMarkerId (markerChar); var positions = markedPositionsById[id]; @@ -119,35 +170,73 @@ public TextSpan GetMarkedSpan (char? markerChar = null, bool allowZeroWidthSingl } /// - /// Tries to get the span marked with the specified . + /// Gets the single span marked with the specified . /// /// Which marker character to use. May be null if only one marker character was specified when creating the + /// Whether to allow use of a single marker character when the span is zero-width + /// The representing the start and end of the marked span + /// Did not find exactly one span (two markers) for + /// The was not specified when creating the + /// The was null but multiple markers were specified when creating the + public (TextMarkerPosition start, TextMarkerPosition end) GetMarkedLineColSpan (char? markerChar = null, bool allowZeroWidthSingleMarker = false) + { + if (!TryGetMarkedLineColSpan (out var start, out var end, markerChar, allowZeroWidthSingleMarker)) { + ThrowExactMismatchException (markerChar, 2, "span"); + } + return (start, end); + } + + /// + /// Tries to get the span marked with the specified . + /// /// The representing the marked span, if this method returned true, otherwise default + /// Which marker character to use. May be null if only one marker character was specified when creating the /// Whether to allow use of a single marker character when the span is zero-width /// Whether a single span was found for /// Did not find exactly zero or one spans (zero or two markers) for /// The was not specified when creating the /// The was null but multiple markers were specified when creating the public bool TryGetMarkedSpan (out TextSpan span, char? markerChar = null, bool allowZeroWidthSingleMarker = false) + { + if (TryGetMarkedLineColSpan(out var start, out var end, markerChar, allowZeroWidthSingleMarker)) { + span = TextSpan.FromBounds (start, end); + return true; + } + + span = default; + return false; + } + + /// + /// Tries to get the span marked with the specified . + /// + /// The representing the start of the marked span, if this method returned true, otherwise default + /// The representing the end of the marked span, if this method returned true, otherwise default + /// Which marker character to use. May be null if only one marker character was specified when creating the + /// Whether to allow use of a single marker character when the span is zero-width + /// Whether a single span was found for + /// Did not find exactly zero or one spans (zero or two markers) for + /// The was not specified when creating the + /// The was null but multiple markers were specified when creating the + public bool TryGetMarkedLineColSpan (out TextMarkerPosition start, out TextMarkerPosition end, char? markerChar = null, bool allowZeroWidthSingleMarker = false) { var id = GetMarkerId (markerChar); var positions = markedPositionsById[id]; if (allowZeroWidthSingleMarker && positions.Count == 1) { - span = new TextSpan (positions[0], 0); + start = end = positions[0]; return true; } if (positions.Count == 2) { - int start = positions[0]; - int end = positions[1]; - span = TextSpan.FromBounds (start, end); + start = positions[0]; + end = positions[1]; return true; } else if (positions.Count > 2) { - ThrowZeroOrNMismatchException(markerChar, 2, "span"); + ThrowZeroOrNMismatchException (markerChar, 2, "span"); } - span = default; + start = end = default; return false; } @@ -177,6 +266,25 @@ public TextSpan GetMarkedSpan (char spanStartMarker, char spanEndMarker) return span; } + /// + /// Gets the single span marked with the specified . + /// + /// The marker character that indicates the start of the span + /// The marker character that indicates the end of the span + /// The representing the marked span + /// Did not find exactly one span + /// The number of characters did not match the number of characters + /// The span end marker was found before the start marker + /// The or was not specified when creating the + /// Cannot use same character as both start and end markers with this overload + public (TextMarkerPosition start, TextMarkerPosition end) GetMarkedLineColSpan (char spanStartMarker, char spanEndMarker) + { + if (!TryGetMarkedLineColSpan (out var start, out var end, spanStartMarker, spanEndMarker)) { + ThrowExactSpanMismatchException (spanStartMarker, spanEndMarker, 1); + } + return (start, end); + } + /// /// Tries to get the span marked with the specified and /// @@ -189,14 +297,36 @@ public TextSpan GetMarkedSpan (char spanStartMarker, char spanEndMarker) /// The or was not specified when creating the /// Cannot use same character as both start and end markers with this overload public bool TryGetMarkedSpan (out TextSpan span, char spanStartMarker, char spanEndMarker) + { + if (TryGetMarkedLineColSpan (out var start, out var end, spanStartMarker, spanEndMarker)) { + span = TextSpan.FromBounds (start, end); + return true; + } + + span = default; + return false; + } + + /// + /// Tries to get the span marked with the specified and + /// + /// The marker character that indicates the start of the span + /// The marker character that indicates the end of the span + /// Whether a single span was found for the and + /// Multiple spans were found, must be zero or one + /// The number of characters did not match the number of characters + /// A span end marker was found before the corresponding start marker + /// The or was not specified when creating the + /// Cannot use same character as both start and end markers with this overload + public bool TryGetMarkedLineColSpan (out TextMarkerPosition start, out TextMarkerPosition end, char spanStartMarker, char spanEndMarker) { CheckStartEndMarkersDifferent (spanStartMarker, spanEndMarker); - var startPositions = GetMarkedPositions (spanStartMarker); - var endPositions = GetMarkedPositions (spanEndMarker); + var startPositions = GetMarkedLineColPositions (spanStartMarker); + var endPositions = GetMarkedLineColPositions (spanEndMarker); if (startPositions.Count == 0 && endPositions.Count == 0) { - span = default; + start = end = default; return false; } @@ -206,12 +336,11 @@ public bool TryGetMarkedSpan (out TextSpan span, char spanStartMarker, char span ThrowZeroOrOneSpanMismatchException (spanStartMarker, spanEndMarker, startPositions); } - int start = startPositions[0]; - int end = endPositions[0]; + start = startPositions[0]; + end = endPositions[0]; if (end < start) { ThrowEndBeforeStartMismatchException (spanStartMarker, start, spanEndMarker, end); } - span = TextSpan.FromBounds (start, end); return true; } @@ -254,8 +383,8 @@ public TextSpan[] GetMarkedSpans (char? markerChar = null) /// The or was not specified when creating the public TextSpan[] GetMarkedSpans (char spanStartMarker, char spanEndMarker) { - var startPositions = GetMarkedPositions (spanStartMarker); - var endPositions = GetMarkedPositions (spanEndMarker); + var startPositions = GetMarkedLineColPositions (spanStartMarker); + var endPositions = GetMarkedLineColPositions (spanEndMarker); if (startPositions.Count != endPositions.Count) { ThrowNonEqualMismatchException (spanStartMarker, startPositions, spanEndMarker, endPositions); @@ -308,18 +437,29 @@ public static TextWithMarkers Parse (string textWithMarkers, params char[] marke } } - var markerIndices = Array.ConvertAll (markerChars, c => new List ()); + var markerIndices = Array.ConvertAll (markerChars, c => new List ()); var sb = new StringBuilder (textWithMarkers.Length); + int line = 0, col = 0; + for (int i = 0; i < textWithMarkers.Length; i++) { var c = textWithMarkers[i]; + int markerId = Array.IndexOf (markerChars, c); if (markerId > -1) { - markerIndices[markerId].Add (sb.Length); + markerIndices[markerId].Add (new (sb.Length, line, col)); + continue; + } + + if (c == '\n') { + line++; + col = 0; } else { - sb.Append (c); + col++; } + + sb.Append (c); } return new (sb.ToString (), markerChars, markerIndices); @@ -339,17 +479,28 @@ public static TextWithMarkers Parse (string textWithMarkers, char markerChar) throw new ArgumentNullException (nameof (textWithMarkers)); } - var markerIndices = new List (); + var markerIndices = new List (); var sb = new StringBuilder (textWithMarkers.Length); + int line = 0, col = 0; + for (int i = 0; i < textWithMarkers.Length; i++) { var c = textWithMarkers[i]; + if (c == markerChar) { - markerIndices.Add (sb.Length); + markerIndices.Add (new (sb.Length, line, col)); + continue; + } + + if (c == '\n') { + line++; + col = 0; } else { - sb.Append (c); + col++; } + + sb.Append (c); } return new (sb.ToString (), [markerChar], [markerIndices]); @@ -370,6 +521,21 @@ public static (string text, int position) ExtractSinglePosition (string textWith return (parsed.Text, caret); } + /// + /// Extract a single marked position and marker-free text from a string with a single marker character + /// + /// The text with a marker character + /// The marker character + /// A tuple with the marker-free text and the marked position + /// Did not find exactly one marker for + /// The was null + public static (string text, TextMarkerPosition position) ExtractSingleLineColPosition (string textWithMarker, char markerChar = '|') + { + var parsed = Parse (textWithMarker, markerChar); + var caret = parsed.GetMarkedLineColPosition (markerChar); + return (parsed.Text, caret); + } + /// /// Extract a single marked span and marker-free text from a string with exactly two marker characters /// @@ -386,6 +552,22 @@ public static (string text, TextSpan span) ExtractSingleSpan (string textWithMar return (parsed.Text, span); } + /// + /// Extract a single marked span and marker-free text from a string with exactly two marker characters + /// + /// The text with marker characters + /// The marker character + /// Whether to allow use of a single marker character when the span is zero-width + /// A tuple with the marker-free text and the marked span + /// Did not find exactly one span (two markers) for + /// The was null + public static (string text, TextMarkerPosition start, TextMarkerPosition end) ExtractSingleLineColSpan (string textWithMarkers, char markerChar = '|', bool allowZeroWidthSingleMarker = false) + { + var parsed = Parse (textWithMarkers, markerChar); + var span = parsed.GetMarkedLineColSpan (markerChar, allowZeroWidthSingleMarker); + return (parsed.Text, span.start, span.end); + } + /// /// Extract a single marked span and marker-free text from a string with exactly two marker characters /// @@ -430,9 +612,9 @@ void ThrowZeroOrNMismatchException (char? markerChar, int expected, string kind) } [DoesNotReturn] - void ThrowZeroOrOneSpanMismatchException (char startMarker, char endMarker, IList startPositions) + void ThrowZeroOrOneSpanMismatchException (char startMarker, char endMarker, IReadOnlyList startPositions) { - throw new TextWithMarkersMismatchException ($"Expected zerone or one '{startMarker}' start marker and '{endMarker}' end marker characters for span, found {startPositions.Count}"); + throw new TextWithMarkersMismatchException ($"Expected zero or one '{startMarker}' start marker and '{endMarker}' end marker characters for span, found {startPositions.Count}"); } [DoesNotReturn] @@ -450,10 +632,26 @@ void ThrowEndBeforeStartMismatchException (char spanStartMarker, int startMarker } [DoesNotReturn] - void ThrowNonEqualMismatchException (char spanStartMarker, IList startPositions, char spanEndMarker, IList endPositions) + void ThrowNonEqualMismatchException (char spanStartMarker, IReadOnlyList startPositions, char spanEndMarker, IReadOnlyList endPositions) { throw new TextWithMarkersMismatchException ($"Expected number of '{spanStartMarker}' span start markers to equal number of '{spanEndMarker}' span end markers, found {startPositions.Count} != {endPositions.Count}"); } + + struct PositionList (IReadOnlyList inner) : IReadOnlyList + { + public int this[int index] => inner[index].Offset; + + public int Count => inner.Count; + + public IEnumerator GetEnumerator () + { + foreach(var item in inner) { + yield return item.Offset; + } + } + + IEnumerator IEnumerable.GetEnumerator () => GetEnumerator (); + } } class TextWithMarkersMismatchException : Exception diff --git a/Core/Analysis/XmlDiagnostic.cs b/Core/Analysis/XmlDiagnostic.cs index c18c3bb5..661e4523 100644 --- a/Core/Analysis/XmlDiagnostic.cs +++ b/Core/Analysis/XmlDiagnostic.cs @@ -27,6 +27,6 @@ public XmlDiagnostic (XmlDiagnosticDescriptor descriptor, TextSpan span, params { } - public string GetFormattedMessage () => Descriptor.GetFormattedMessage (messageArgs); + public string GetFormattedMessageWithTitle () => Descriptor.GetFormattedMessageWithTitle (messageArgs); } } \ No newline at end of file diff --git a/Core/Analysis/XmlDiagnosticDescriptor.cs b/Core/Analysis/XmlDiagnosticDescriptor.cs index adb219d1..9b694b3f 100644 --- a/Core/Analysis/XmlDiagnosticDescriptor.cs +++ b/Core/Analysis/XmlDiagnosticDescriptor.cs @@ -13,35 +13,34 @@ public class XmlDiagnosticDescriptor public string Title { get; } [StringSyntax (StringSyntaxAttribute.CompositeFormat)] - public string? Message { get; } + public string? MessageFormat { get; } public XmlDiagnosticSeverity Severity { get; } - public XmlDiagnosticDescriptor (string id, string title, [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string? message, XmlDiagnosticSeverity severity) + public XmlDiagnosticDescriptor (string id, string title, [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string? messageFormat, XmlDiagnosticSeverity severity) { Title = title ?? throw new ArgumentNullException (nameof (title)); Id = id ?? throw new ArgumentNullException (nameof (id)); - Message = message; + MessageFormat = messageFormat; Severity = severity; } public XmlDiagnosticDescriptor (string id, string title, XmlDiagnosticSeverity severity) : this (id, title, null, severity) { } - string? combinedMsg; - - internal string GetFormattedMessage (object[]? args) + internal string GetFormattedMessageWithTitle (object[]? messageArgs) { try { - combinedMsg ??= (combinedMsg = Title + Environment.NewLine + Message); - if (args != null && args.Length > 0) { - return string.Format (combinedMsg, args); - } + string? message = messageArgs?.Length > 0 && MessageFormat is string format + ? string.Format (MessageFormat, messageArgs) + : MessageFormat; + return string.IsNullOrEmpty (message) + ? Title + : Title + Environment.NewLine + message; } catch (FormatException ex) { // this is likely to be called from somewhere other than where the diagnostic was constructed // so ensure the error has enough info to track it down throw new FormatException ($"Error formatting message for diagnostic {Id}", ex); } - return combinedMsg; } } } \ No newline at end of file diff --git a/Core/Completion/XmlCompletionTriggering.cs b/Core/Completion/XmlCompletionTriggering.cs index b75f1ba3..1ee3c73c 100644 --- a/Core/Completion/XmlCompletionTriggering.cs +++ b/Core/Completion/XmlCompletionTriggering.cs @@ -10,7 +10,7 @@ namespace MonoDevelop.Xml.Editor.Completion { - class XmlCompletionTriggering + public class XmlCompletionTriggering { public static XmlCompletionTrigger GetTrigger (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter) => GetTriggerAndIncompleteSpan (parser, reason, typedCharacter).kind; @@ -200,7 +200,7 @@ static bool TryGetReadForwardLength (ITextSource textSource, XmlSpineParser spin }; } - enum XmlCompletionTrigger + public enum XmlCompletionTrigger { None, diff --git a/Core/IsExternalInit.cs b/Core/IsExternalInit.cs new file mode 100644 index 00000000..23c4077e --- /dev/null +++ b/Core/IsExternalInit.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +// Declare this to get init properties. See https://github.com/dotnet/roslyn/issues/45510#issuecomment-694977239 +internal static class IsExternalInit { } diff --git a/Core/Options/IOptionsReader.cs b/Core/Options/IOptionsReader.cs new file mode 100644 index 00000000..b59083fb --- /dev/null +++ b/Core/Options/IOptionsReader.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MonoDevelop.Xml.Options; + +public interface IOptionsReader +{ + bool TryGetOption (Option option, out T? value); +} diff --git a/Core/Options/OptionReaderExtensions.cs b/Core/Options/OptionReaderExtensions.cs new file mode 100644 index 00000000..1ff808f2 --- /dev/null +++ b/Core/Options/OptionReaderExtensions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MonoDevelop.Xml.Options; + +public static class OptionReaderExtensions +{ + public static T GetOption(this IOptionsReader options, Option option) + { + if (options.TryGetOption (option, out T? value)) { + return value!; + } + return option.DefaultValue; + } +} \ No newline at end of file diff --git a/Core/Options/Option`1.cs b/Core/Options/Option`1.cs new file mode 100644 index 00000000..09179676 --- /dev/null +++ b/Core/Options/Option`1.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace MonoDevelop.Xml.Options; + +/// +/// Defines an option that may affect formatter, editor, analyzer or code fix behavior. +/// Some of these are read from .editorconfig, and others may be mapped to equivalent settings +/// of the host IDE. +/// +public class Option +{ + public Option(string name, T defaultValue, bool isEditorConfigOption) + { + Name = name; + DefaultValue = defaultValue; + IsEditorConfigOption = isEditorConfigOption; + } + + public Option(string name, T value, EditorConfigSerializer? serializer = null) : this(name, value, true) + { + Serializer = serializer; + } + + /// + /// A unique name for the option. If this is an editorconfig option, this will be used as the name + /// in .editorconfig. + /// + public string Name { get; } + + /// + /// The value to use for this option when no setting is found in EditorConfig or + /// in the host. + /// + public T DefaultValue { get; } + + /// + /// Whether this option will be read from .editorconfig. + /// + public bool IsEditorConfigOption { get; } + + /// + /// Optionally override the EditorConfig serialization behavior + /// + public EditorConfigSerializer? Serializer { get; } +} + +public record EditorConfigSerializer (Func Deserialize, Func Serialize); diff --git a/Core/Options/TextFormattingOptionValues.cs b/Core/Options/TextFormattingOptionValues.cs new file mode 100644 index 00000000..71413dbe --- /dev/null +++ b/Core/Options/TextFormattingOptionValues.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace MonoDevelop.Xml.Options; + +// based on https://github.com/dotnet/roslyn/blob/df4ae6b81013ac45367372176b9c3135a35a7e3c/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Formatting/LineFormattingOptions.cs +/// +/// Captures common text formatting options values from an +/// so that they may be accessed more efficiently. +/// +public sealed record class TextFormattingOptionValues () +{ + public static readonly TextFormattingOptionValues Default = new (); + + public bool ConvertTabsToSpaces { get; init; } = false; + public int TabSize { get; init; } = 4; + public int IndentSize { get; init; } = 4; + public string NewLine { get; init; } = Environment.NewLine; + public bool TrimTrailingWhitespace { get; init; } = false; + + public TextFormattingOptionValues (IOptionsReader options) + : this () + { + ConvertTabsToSpaces = options.GetOption (TextFormattingOptions.ConvertTabsToSpaces); + TabSize = options.GetOption (TextFormattingOptions.TabSize); + IndentSize = options.GetOption (TextFormattingOptions.IndentSize); + NewLine = options.GetOption (TextFormattingOptions.NewLine); + } +} \ No newline at end of file diff --git a/Core/Options/TextFormattingOptions.cs b/Core/Options/TextFormattingOptions.cs new file mode 100644 index 00000000..e6718bc4 --- /dev/null +++ b/Core/Options/TextFormattingOptions.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace MonoDevelop.Xml.Options; + +// based on https://github.com/dotnet/roslyn/blob/199c241cef61d94e25fcfd0f6bcaa91faa35d515/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Formatting/FormattingOptions2.cs#L23 +/// +/// Options that control text formatting. Accessing these multiple times may be done more efficiently using . +/// +public class TextFormattingOptions +{ + public static readonly Option ConvertTabsToSpaces = new ( + "indent_style", + TextFormattingOptionValues.Default.ConvertTabsToSpaces, + new EditorConfigSerializer (str => str != "tab", value => value ? "space" : "tab") + ); + + public static readonly Option TabSize = new ("tab_size", TextFormattingOptionValues.Default.TabSize, true); + + public static readonly Option IndentSize = new ("indent_size", TextFormattingOptionValues.Default.IndentSize, true); + + public static readonly Option NewLine = new ( + "end_of_line", + TextFormattingOptionValues.Default.NewLine, + new EditorConfigSerializer ( + str => str switch { + "lf" => "\n", + "cr" => "\r", + "crlf" => "\r\n", + _ => Environment.NewLine + }, + value => value switch { + "\n" => "lf", + "\r" => "cr", + "\r\n" => "crlf", + _ => "unset" + })); + + + public static readonly Option InsertFinalNewline = new ("insert_final_newline", true, true); + + public static readonly Option TrimTrailingWhitespace = new ("trim_trailing_whitespace", TextFormattingOptionValues.Default.TrimTrailingWhitespace, true); + + public static readonly Option MaxLineLength = new ("max_line_length", null, new EditorConfigSerializer ( + str => str != "off" && int.TryParse (str, out var val) && val > 0 ? val : null, + val => val.HasValue && val.Value > 0 ? val.Value.ToString () : "off" + )); +} diff --git a/Core/Options/XmlFormattingOptions.cs b/Core/Options/XmlFormattingOptions.cs new file mode 100644 index 00000000..2c6df962 --- /dev/null +++ b/Core/Options/XmlFormattingOptions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MonoDevelop.Xml.Options; + +/// +/// Options that control XML formatting +/// +public static class XmlFormattingOptions +{ + public static readonly Option OmitXmlDeclaration = new ("xml_omit_declaration", false, true); + public static readonly Option IndentContent = new ("xml_indent_content", true, true); + + public static readonly Option AttributesOnNewLine = new ("xml_attributes_on_new_line", false, true); + public static readonly Option MaxAttributesPerLine = new ("xml_max_attributes_per_line", 10, true); + + public static readonly Option AlignAttributes = new ("xml_align_attributes", false, true); + public static readonly Option AlignAttributeValues = new ("xml_align_attribute_values", false, true); + public static readonly Option WrapAttributes = new ("xml_wrap_attributes", false, true); + public static readonly Option SpacesBeforeAssignment = new ("xml_spaces_before_assignment", 0, true); + public static readonly Option SpacesAfterAssignment = new ("xml_spaces_after_assignment", 0, true); + + public static readonly Option QuoteChar = new ("xml_quote_style", '"', new EditorConfigSerializer ( + str => str == "single" ? '\'' : '"', + val => val == '\'' ? "single" : "double" + )); + + public static readonly Option EmptyLinesBeforeStart = new ("xml_empty_lines_before_start", 0, true); + public static readonly Option EmptyLinesAfterStart = new ("xml_empty_lines_after_start", 0, true); + public static readonly Option EmptyLinesBeforeEnd = new ("xml_empty_lines_before_end", 0, true); + public static readonly Option EmptyLinesAfterEnd = new ("xml_empty_lines_after_end", 0, true); +} \ No newline at end of file diff --git a/Core/Parser/XmlParserContext.cs b/Core/Parser/XmlParserContext.cs index 499aff49..5328fce5 100644 --- a/Core/Parser/XmlParserContext.cs +++ b/Core/Parser/XmlParserContext.cs @@ -149,7 +149,7 @@ public override string ToString () builder.AppendLine ("Errors="); foreach (XmlDiagnostic err in Diagnostics) { builder.Append (' ', 4); - builder.AppendLine ($"[{err.Descriptor.Severity}@{err.Span}: {err.GetFormattedMessage ()}"); + builder.AppendLine ($"[{err.Descriptor.Severity}@{err.Span}: {err.GetFormattedMessageWithTitle ()}"); } } diff --git a/Core/Parser/XmlParserTextSourceExtensions.cs b/Core/Parser/XmlParserTextSourceExtensions.cs index 76a450db..3f55cfbf 100644 --- a/Core/Parser/XmlParserTextSourceExtensions.cs +++ b/Core/Parser/XmlParserTextSourceExtensions.cs @@ -225,22 +225,23 @@ public static bool AdvanceParserUntilConditionOrEol (this XmlSpineParser parser, /// /// A spine parser. Its state will not be modified. /// The text snapshot corresponding to the parser. - public static bool TryGetNodePath (this XmlSpineParser parser, ITextSource text, [NotNullWhen (true)] out List? nodePath, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default) + /// The node path. If the method returns false, the maximum readahead was reached, and the deepest node will have an incomplete name. + /// The maximum number of characters to read ahead when completing the name of the deepest node. + /// True if the name of the deepest node could be completed + public static bool TryGetNodePath (this XmlSpineParser parser, ITextSource text, out List nodePath, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default) { - var path = parser.GetPath (); + nodePath = parser.GetPath (); //complete last node's name without altering the parser state - int lastIdx = path.Count - 1; - if (parser.CurrentState is XmlNameState && path[lastIdx] is INamedXObject) { + int lastIdx = nodePath.Count - 1; + if (parser.CurrentState is XmlNameState && nodePath[lastIdx] is INamedXObject) { if (!TryGetCompleteName (parser, text, out XName completeName, maximumReadahead, cancellationToken)) { - nodePath = null; return false; } - var obj = path[lastIdx] = path[lastIdx].ShallowCopy (); + var obj = nodePath[lastIdx] = nodePath[lastIdx].ShallowCopy (); ((INamedXObject)obj).Name = completeName; } - nodePath = path; return true; } diff --git a/Core/Parser/XmlProcessingInstructionState.cs b/Core/Parser/XmlProcessingInstructionState.cs index d4ebbe08..546ba304 100644 --- a/Core/Parser/XmlProcessingInstructionState.cs +++ b/Core/Parser/XmlProcessingInstructionState.cs @@ -82,6 +82,11 @@ public class XmlProcessingInstructionState : XmlParserState parents.Push (new XProcessingInstruction (pi.Span.Start)); } + // still in RootState + if (length == 1) { + return null; + } + return new ( currentState: this, position: position, diff --git a/Core/Parser/XmlSpineParser.cs b/Core/Parser/XmlSpineParser.cs index 5db89c09..854a286c 100644 --- a/Core/Parser/XmlSpineParser.cs +++ b/Core/Parser/XmlSpineParser.cs @@ -58,10 +58,15 @@ public XmlSpineParser (XmlParserContext context, XmlRootState rootState) : base /// the parser is not guaranteed but will not exceed . /// /// - public static XmlSpineParser? FromDocumentPosition (XmlRootState stateMachine, XDocument xdocument, int maximumPosition) - => xdocument.FindAtOrBeforeOffset (maximumPosition) is XObject obj - && stateMachine.TryRecreateState (ref obj, maximumPosition) is XmlParserContext ctx - ? new XmlSpineParser (ctx, stateMachine) - : null; + public static XmlSpineParser FromDocumentPosition (XmlRootState stateMachine, XDocument xdocument, int maximumPosition) + { + // Recovery must be based on the node before the target position. + // The state for the node at the position won't be entered until the character at (i.e. after) the position is processed. + var node = maximumPosition == 0? xdocument : xdocument.FindAtOrBeforeOffset (maximumPosition - 1); + if (node is XObject obj && stateMachine.TryRecreateState (ref obj, maximumPosition) is XmlParserContext ctx) { + return new XmlSpineParser (ctx, stateMachine); + } + return new XmlSpineParser (stateMachine); ; + } } } diff --git a/Core/Parser/XmlTagState.cs b/Core/Parser/XmlTagState.cs index d9c98e3a..4fd8331b 100644 --- a/Core/Parser/XmlTagState.cs +++ b/Core/Parser/XmlTagState.cs @@ -226,7 +226,7 @@ public XmlTagState (XmlAttributeState attributeState, XmlNameState nameState) newEl.Attributes.AddAttribute ((XAttribute)att.ShallowCopy ()); continue; } - if (att.Span.End > position) { + if (att.Span.End >= position) { foundPosition = Math.Min (position, att.Span.Start); break; } diff --git a/Directory.Packages.props b/Directory.Packages.props index 38d760d8..a09825f5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,32 +1,32 @@ + + + + + + + + + + + - - + + + - - - - - - - - - - - - diff --git a/Editor.Tests/Commands/AutoClosingTests.cs b/Editor.Tests/Commands/AutoClosingTests.cs index 22149bc9..4864f94b 100644 --- a/Editor.Tests/Commands/AutoClosingTests.cs +++ b/Editor.Tests/Commands/AutoClosingTests.cs @@ -29,7 +29,7 @@ public Task TestComment (string sourceText, string expectedText, string typeChar return this.TestCommands ( sourceText, expectedText, - (s) => s.Type (typeChars), + EditorAction.Type (typeChars), caretMarkerChar: '|', initialize: (ITextView tv) => { tv.Options.SetOptionValue (XmlOptions.AutoInsertClosingTag, true); diff --git a/Editor.Tests/Completion/CommitTests.cs b/Editor.Tests/Completion/CommitTests.cs index 69768020..7684c5a8 100644 --- a/Editor.Tests/Completion/CommitTests.cs +++ b/Editor.Tests/Completion/CommitTests.cs @@ -1,12 +1,9 @@ // 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.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Editor.Commanding; using MonoDevelop.Xml.Editor.Tests.Extensions; @@ -28,10 +25,7 @@ public Task SingleClosingTag () $ ", - (s) => { - s.Type (" $ ", - (s) => { - s.Type (" $ ", - (s) => { - s.InvokeCompletion (); - s.Type (" $ ", - (s) => { - s.InvokeCompletion (); - s.Type (" this.TestCommands ( @"$", @"$", - (s) => { - s.InvokeCompletion (); - s.Type (" $", - (s) => { - s.InvokeCompletion (); - s.Type (" $ ", - (s) => { - s.Type (" $ ", - (s) => { - s.Type (" { Action a = (s) => s.Type (t); return a; }); - return this.TestCommands (before, after, actions, initialize: (ITextView tv) => { - tv.Options.SetOptionValue ("BraceCompletion/Enabled", true); - return Task.CompletedTask; - }); + return this.TestCommands ( + before, + after, + EditorAction.Type(typeChars), + initialize: (ITextView tv) => { + tv.Options.SetOptionValue ("BraceCompletion/Enabled", true); + return Task.CompletedTask; + } + ); } [Test] [TestCase ("", "$")] - [TestCase ("", "$")] + [TestCase (" TestTypeCommands ("$", after, typeChars); [Test] [TestCase (" T\n", " TestTypeCommands (" new TypeCharCommandArgs (v, b, c)); - break; - } - } - } - - public static void Enter (this IEditorCommandHandlerService commandService) - => commandService.CheckAndExecute ((v, b) => new ReturnKeyCommandArgs (v, b)); - - public static void InvokeCompletion (this IEditorCommandHandlerService commandService) - => commandService.CheckAndExecute ((v, b) => new InvokeCompletionListCommandArgs (v, b)); + /// + /// Enables logging of additional trace information to debug nondeterministic test failures + /// + public static bool EnableDebugTrace { get; set; } public static void CheckAndExecute ( this IEditorCommandHandlerService commandService, @@ -46,14 +31,67 @@ public static void CheckAndExecute ( throw new InvalidOperationException ($"No handler available for `{typeof (T)}`"); } - //ensure the computation is completed before we continue typing + // There is a race here where a completion session may have triggered on the UI thread + // but the task to compute the completion items is still running. This can cause the + // completion to be dismissed before the items are computed. + // + // We mitigate this by checking if a session is open and attempting to wait for it. if (textView != null) { if (textView.Properties.TryGetProperty (typeof (IAsyncCompletionSession), out IAsyncCompletionSession session)) { + if (EnableDebugTrace) { + LogTrace ("Session open"); + RegisterTraceHandlers (session); + } + + // The first time we see the session, wait for a short time to allow it to initialize, + // otherwise completion will dismiss via TryDismissSafelyAsync if the snapshot is updated + // before the session is initialized. + // + // This wait is not necessary on my local machine, but it mitigates nondeterministic + // failures on GitHub Actions CI. + // + // Note that polling IAsyncCompletionSessionOperations.IsStarted does not help. + if (IsGitHubActions && !session.Properties.TryGetProperty (HasWaitedForCompletionToInitializeKey, out bool hasWaited)) { + session.Properties.AddProperty (HasWaitedForCompletionToInitializeKey, true); + Thread.Sleep (500); + } + + // Block until the computation is updated before we run more actions. This makes the + // test reliable on my local machine. session.GetComputedItems (CancellationToken.None); } + } else{ + LogTrace ("Session not open"); } } + const string TraceID = "CommandServiceExtensions.Trace"; + + static readonly object HasWaitedForCompletionToInitializeKey = new(); + + static readonly bool IsGitHubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") != null; + + static void LogTrace(string message) => Console.WriteLine ($"{TraceID}: {message}"); + + static void RegisterTraceHandlers (IAsyncCompletionSession session) + { + if (session.Properties.TryGetProperty (TraceID, out bool hasHandler)) { + return; + } + + session.Properties.AddProperty (TraceID, true); + session.Dismissed += (s, e) => { + LogTrace ($"Session dismissed:\n{Environment.StackTrace}"); + LogTrace (Environment.StackTrace); + }; + session.ItemCommitted += (s, e) => { + LogTrace ($"Session committed '{e.Item.InsertText}':\n{Environment.StackTrace}"); + }; + session.ItemsUpdated += (s, e) => { + LogTrace ($"Session updated"); + }; + } + static Action Noop { get; } = new Action (() => { }); static Func Unspecified { get; } = () => CommandState.Unspecified; } diff --git a/Editor.Tests/Extensions/EditorAction.cs b/Editor.Tests/Extensions/EditorAction.cs new file mode 100644 index 00000000..81939d35 --- /dev/null +++ b/Editor.Tests/Extensions/EditorAction.cs @@ -0,0 +1,52 @@ +// 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.Collections.Generic; + +using Microsoft.VisualStudio.Text.Editor.Commanding; +using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; + +namespace MonoDevelop.Xml.Editor.Tests.Extensions +{ + public static class EditorAction + { + public static IEnumerable> Type (string text) + { + foreach (var c in text) { + switch (c) { + case '\n': + + yield return Enter; + break; + default: + if (EnableDebugTrace) { + LogTrace ($"Typing '{c}'"); + } + yield return (commandService) => commandService.CheckAndExecute ((v, b) => new TypeCharCommandArgs (v, b, c)); + break; + } + } + } + + public static void Enter (IEditorCommandHandlerService commandService) + { + if (EnableDebugTrace) { + LogTrace ("Invoking return key"); + } + commandService.CheckAndExecute ((v, b) => new ReturnKeyCommandArgs (v, b)); + } + + public static void InvokeCompletion (IEditorCommandHandlerService commandService) + { + if (EnableDebugTrace) { + LogTrace ("Invoking completion"); + } + commandService.CheckAndExecute ((v, b) => new InvokeCompletionListCommandArgs (v, b)); + } + + const string TraceID = "EditorAction.Trace"; + static bool EnableDebugTrace => CommandServiceExtensions.EnableDebugTrace; + static void LogTrace (string message) => Console.WriteLine ($"{TraceID}: {message}"); + } +} diff --git a/Editor.Tests/Extensions/EditorCommandExtensions.cs b/Editor.Tests/Extensions/EditorCommandExtensions.cs index d0376a25..2dd0cd02 100644 --- a/Editor.Tests/Extensions/EditorCommandExtensions.cs +++ b/Editor.Tests/Extensions/EditorCommandExtensions.cs @@ -77,6 +77,10 @@ public static async Task TestCommands ( foreach (var c in commands) { c (commandService); + + // yield to let things catch up + // and so we don't block the UI thread between the commands + await Task.Delay (20); } Assert.AreEqual (afterDocumentText, textView.TextBuffer.CurrentSnapshot.GetText ()); diff --git a/Editor.Tests/MonoDevelop.Xml.Editor.Tests.csproj b/Editor.Tests/MonoDevelop.Xml.Editor.Tests.csproj index 90b742ef..c54c286f 100644 --- a/Editor.Tests/MonoDevelop.Xml.Editor.Tests.csproj +++ b/Editor.Tests/MonoDevelop.Xml.Editor.Tests.csproj @@ -5,6 +5,16 @@ true + + + False + False + + + + + + @@ -22,11 +32,11 @@ - + + - diff --git a/Editor.Tests/XmlTestEnvironment.cs b/Editor.Tests/XmlTestEnvironment.cs index 5380a61e..b2e028d6 100644 --- a/Editor.Tests/XmlTestEnvironment.cs +++ b/Editor.Tests/XmlTestEnvironment.cs @@ -114,7 +114,18 @@ protected virtual IEnumerable GetAssembliesToCompose () => new[] { typeof (XmlTestEnvironment).Assembly.Location }; - protected virtual bool ShouldIgnoreCompositionError (string error) => false; + // ignore errors we expect to happen in the test composition + protected virtual bool ShouldIgnoreCompositionError (string error) + => error.IndexOf ("Contract name: Microsoft.VisualStudio.Text.SpellChecker.ISpellCheckService", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.VisualStudio.Shell.ServiceBroker.SVsFullAccessServiceBroker", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.VisualStudio.Text.Editor.IObscuringTipManager", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.VisualStudio.Text.Editor.IAudioProvider", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.VisualStudio.Audio.IAudioPlayer", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.VisualStudio.Text.Editor.ErrorList.ITaskList", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.VisualStudio.Text.Editor.ErrorList.IErrorList", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.ServiceHub.Framework.ServiceMoniker", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.VisualStudio.Text.Structure.StructureContextFactory", StringComparison.Ordinal) > -1 + || error.IndexOf ("Contract name: Microsoft.VisualStudio.Text.BrokeredServices.Implementation.Diagnostics.DiagnosticReporter", StringComparison.Ordinal) > -1; protected virtual void HandleError (object? source, Exception ex) { diff --git a/Editor/Completion/XmlCompletionSource.cs b/Editor/Completion/XmlCompletionSource.cs index ecd47dde..39659dd3 100644 --- a/Editor/Completion/XmlCompletionSource.cs +++ b/Editor/Completion/XmlCompletionSource.cs @@ -32,6 +32,15 @@ namespace MonoDevelop.Xml.Editor.Completion { public abstract partial class XmlCompletionSource : IAsyncCompletionSource where TCompletionTriggerContext : XmlCompletionTriggerContext { + /// + /// Enables logging of additional trace information to debug nondeterministic test failures + /// + public static bool EnableDebugTrace { get; set; } + + const string TraceID = "XmlCompletionSource.Trace"; + + protected static void LogTrace(string message) => Console.WriteLine ($"{TraceID}: {message}"); + protected XmlParserProvider XmlParserProvider { get; } protected ITextView TextView { get; } @@ -60,10 +69,21 @@ public Task GetCompletionContextAsync (IAsyncCompletionSessio async Task GetCompletionContextAsyncInternal (IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token) { + if (EnableDebugTrace) { + LogTrace ($"GetCompletionContextAsyncInternal entered'"); + } + var spineParser = GetSpineParser (triggerLocation); var triggerContext = CreateTriggerContext (session, trigger, spineParser, triggerLocation, applicableToSpan); + if (EnableDebugTrace) { + LogTrace ($"GetCompletionContextAsyncInternal got trigger {triggerContext.XmlTriggerKind}"); + } + if (!triggerContext.IsSupportedTriggerReason) { + if (EnableDebugTrace) { + LogTrace ($"GetCompletionContextAsyncInternal exited with unsupported trigger reason"); + } return CompletionContext.Empty; } @@ -71,21 +91,35 @@ async Task GetCompletionContextAsyncInternal (IAsyncCompletio var tasks = GetCompletionTasks (triggerContext, token).ToList (); + if (EnableDebugTrace) { + LogTrace ($"GetCompletionContextAsyncInternal got {tasks.Count} completion tasks"); + } + await Task.WhenAll (tasks).ConfigureAwait (false); 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) { + if (EnableDebugTrace) { + LogTrace ($"GetCompletionContextAsyncInternal: task returned {taskItems.Count} items"); + } allItems = allItems.AddRange (taskItems); } #pragma warning restore VSTHRD103 // Call async methods when in an async method } if (allItems.IsEmpty) { + if (EnableDebugTrace) { + LogTrace ($"GetCompletionContextAsyncInternal Exited: no items"); + } return CompletionContext.Empty; } + if (EnableDebugTrace) { + LogTrace ($"GetCompletionContextAsyncInternal Exited with {allItems.Length} items"); + } + return new CompletionContext (allItems, null, InitialSelectionHint.SoftSelection); } diff --git a/Editor/MonoDevelop.Xml.Editor.csproj b/Editor/MonoDevelop.Xml.Editor.csproj index a4f2707e..0999a2df 100644 --- a/Editor/MonoDevelop.Xml.Editor.csproj +++ b/Editor/MonoDevelop.Xml.Editor.csproj @@ -1,7 +1,7 @@ - net48 + net48 diff --git a/Editor/Options/XmlEditorOptions.cs b/Editor/Options/XmlEditorOptions.cs new file mode 100644 index 00000000..b603694a --- /dev/null +++ b/Editor/Options/XmlEditorOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MonoDevelop.Xml.Options; + +/// +/// Options that control the behavior of the XML editor +/// +public static class XmlEditorOptions +{ + public static Option AutoInsertClosingTag = new ("xml_auto_insert_closing_tag", true, false); + public static Option AutoInsertAttributeValue = new ("xml_auto_insert_attribute_value", true, false); +} diff --git a/Editor/Parsing/BackgroundParser.ParseOperation.cs b/Editor/Parsing/BackgroundParser.ParseOperation.cs index 0b05a8de..b8b1790a 100644 --- a/Editor/Parsing/BackgroundParser.ParseOperation.cs +++ b/Editor/Parsing/BackgroundParser.ParseOperation.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#nullable enable + using System.Threading; using System.Threading.Tasks; @@ -10,7 +12,7 @@ public abstract partial class BackgroundProcessor { class Operation { - CancellationTokenSource tokenSource; + CancellationTokenSource? tokenSource; // if this ever reaches zero, the task gets cancelled int ownerCount; @@ -34,7 +36,7 @@ public Operation (BackgroundProcessor processor, Task o public TInput Input { get; } #pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - public TOutput Output => Task.IsCompleted ? Task.Result : default; + public TOutput? Output => Task.IsCompleted ? Task.Result : default; #pragma warning restore VSTHRD002 public void Cancel () diff --git a/Editor/Parsing/BackgroundParser.cs b/Editor/Parsing/BackgroundParser.cs index eb36d91d..ba6436ba 100644 --- a/Editor/Parsing/BackgroundParser.cs +++ b/Editor/Parsing/BackgroundParser.cs @@ -43,7 +43,7 @@ Operation CreateOperation (TInput input) Operation op = (Operation)state!; try { // op.Output accesses the task.Result, throwing any exceptions - op.Processor.OnOperationCompleted (op.Input, op.Output); + op.Processor.OnOperationCompleted (op.Input, op.Output!); // output only returns null if task is not completed op.Processor.lastSuccessfulOperation = op; } catch (Exception eventException) { op.Processor.OnUnhandledParseError (eventException); @@ -65,7 +65,7 @@ protected virtual void OnOperationCompleted (TInput input, TOutput output) { } - void LastDitchLog (Exception ex) + static void LastDitchLog (Exception ex) { if (System.Diagnostics.Debugger.IsAttached) { System.Diagnostics.Debugger.Break (); @@ -80,7 +80,7 @@ void LastDitchLog (Exception ex) /// protected virtual void OnUnhandledParseError (Exception ex) { - LastDitchLog (ex); + BackgroundProcessor.LastDitchLog (ex); } Operation? currentOperation; @@ -117,6 +117,7 @@ public Task GetOrProcessAsync (TInput input, CancellationToken token) protected virtual void Dispose (bool disposing) { + currentOperation?.Cancel(); } ~BackgroundProcessor () => Dispose (false); diff --git a/Editor/Tagging/XmlSyntaxValidationTagger.cs b/Editor/Tagging/XmlSyntaxValidationTagger.cs index 049fe657..d7cd0375 100644 --- a/Editor/Tagging/XmlSyntaxValidationTagger.cs +++ b/Editor/Tagging/XmlSyntaxValidationTagger.cs @@ -79,7 +79,7 @@ IEnumerable> GetTagsInternal (NormalizedSnapshotSpanCollecti if (diagSpan.IntersectsWith (taggingSpan)) { var errorType = GetErrorTypeName (diag.Descriptor.Severity); - yield return new TagSpan (diagSpan, new ErrorTag (errorType, diag.GetFormattedMessage ())); + yield return new TagSpan (diagSpan, new ErrorTag (errorType, diag.GetFormattedMessageWithTitle ())); } } }