From 66e1c97c5d1bc5a83014c1a1514e00b345d54d87 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 1 Jun 2024 01:01:35 -0400 Subject: [PATCH 01/19] Fix some nullability issues --- Editor/Parsing/BackgroundParser.ParseOperation.cs | 6 ++++-- Editor/Parsing/BackgroundParser.cs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Editor/Parsing/BackgroundParser.ParseOperation.cs b/Editor/Parsing/BackgroundParser.ParseOperation.cs index 0b05a8d..b8b1790 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 eb36d91..6c1116a 100644 --- a/Editor/Parsing/BackgroundParser.cs +++ b/Editor/Parsing/BackgroundParser.cs @@ -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; From 1eb83426be9a2d7519a3a7041bf174259d1be536 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Sat, 1 Jun 2024 01:02:51 -0400 Subject: [PATCH 02/19] Clean up diagnostic message formatting --- Core.Tests/Parser/TestXmlParser.cs | 2 +- Core/Analysis/XmlDiagnostic.cs | 2 +- Core/Analysis/XmlDiagnosticDescriptor.cs | 21 ++++++++++----------- Core/Parser/XmlParserContext.cs | 2 +- Editor/Tagging/XmlSyntaxValidationTagger.cs | 2 +- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Core.Tests/Parser/TestXmlParser.cs b/Core.Tests/Parser/TestXmlParser.cs index d3e4ba9..635b35a 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/Analysis/XmlDiagnostic.cs b/Core/Analysis/XmlDiagnostic.cs index c18c3bb..661e452 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 adb219d..9b694b3 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/Parser/XmlParserContext.cs b/Core/Parser/XmlParserContext.cs index 499aff4..5328fce 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/Editor/Tagging/XmlSyntaxValidationTagger.cs b/Editor/Tagging/XmlSyntaxValidationTagger.cs index 049fe65..d7cd037 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 ())); } } } From 38d91b16fb59f0899e2c9df42935cc72f0eb5c2a Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 22 Aug 2024 14:54:13 -0400 Subject: [PATCH 03/19] Improve some doc comments --- Core/Parser/XmlParserTextSourceExtensions.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Core/Parser/XmlParserTextSourceExtensions.cs b/Core/Parser/XmlParserTextSourceExtensions.cs index 76a450d..3f55cfb 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; } From fbfee69e779c2b6113cedaed0d7ff09f0fd8bf22 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 22 Aug 2024 14:54:41 -0400 Subject: [PATCH 04/19] Make XML triggering code public for MSBuildEditor LSP --- Core/Completion/XmlCompletionTriggering.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Completion/XmlCompletionTriggering.cs b/Core/Completion/XmlCompletionTriggering.cs index b75f1ba..1ee3c73 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, From 0f9b899b8b917072d92626d93d01e769b7ce88f2 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 22 Aug 2024 20:52:50 -0400 Subject: [PATCH 05/19] Support line/col in TextWithMarkers --- Core.Tests/Utils/TextWithMarkers.cs | 248 +++++++++++++++++++++++++--- 1 file changed, 222 insertions(+), 26 deletions(-) diff --git a/Core.Tests/Utils/TextWithMarkers.cs b/Core.Tests/Utils/TextWithMarkers.cs index b0237b2..2d4b3cf 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)) { + 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,15 +437,25 @@ 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]; + + if (c == '\n') { + line++; + col = 0; + } else { + col++; + } + int markerId = Array.IndexOf (markerChars, c); if (markerId > -1) { - markerIndices[markerId].Add (sb.Length); + markerIndices[markerId].Add (new (sb.Length, line, col)); } else { sb.Append (c); } @@ -339,14 +478,24 @@ 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 == '\n') { + line++; + col = 0; + } else { + col++; + } + if (c == markerChar) { - markerIndices.Add (sb.Length); + markerIndices.Add (new (sb.Length, line, col)); } else { sb.Append (c); } @@ -370,6 +519,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 +550,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 +610,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 +630,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 From 0f56c9f53e3256a516ae344f19aa8924d6f3ac45 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Fri, 23 Aug 2024 13:35:40 -0400 Subject: [PATCH 06/19] Fix nullability warning in BackgroundProcessor --- Editor/Parsing/BackgroundParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Editor/Parsing/BackgroundParser.cs b/Editor/Parsing/BackgroundParser.cs index 6c1116a..8c6893c 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); From dff45c798e0bbf5a0d143ae30e62478092977537 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 3 Sep 2024 14:09:12 -0400 Subject: [PATCH 07/19] Sort Directory.Packages.props --- Directory.Packages.props | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 38d760d..a09825f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,32 +1,32 @@ + + + + + + + + + + + - - + + + - - - - - - - - - - - - From 9fa78cb94f1e0d409ddcf2e2ab61b6ea87c0e233 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 3 Sep 2024 15:40:52 -0400 Subject: [PATCH 08/19] Remove redundant plural TargetFrameworks --- Editor/MonoDevelop.Xml.Editor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Editor/MonoDevelop.Xml.Editor.csproj b/Editor/MonoDevelop.Xml.Editor.csproj index a4f2707..0999a2d 100644 --- a/Editor/MonoDevelop.Xml.Editor.csproj +++ b/Editor/MonoDevelop.Xml.Editor.csproj @@ -1,7 +1,7 @@ - net48 + net48 From 11ca5cbdc735ae970144a69a3729cc1af63accec Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 4 Sep 2024 03:03:40 -0400 Subject: [PATCH 09/19] Fix TextWithMarkers ignoring custom marker --- Core.Tests/Utils/TextWithMarkers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core.Tests/Utils/TextWithMarkers.cs b/Core.Tests/Utils/TextWithMarkers.cs index 2d4b3cf..c0ea66c 100644 --- a/Core.Tests/Utils/TextWithMarkers.cs +++ b/Core.Tests/Utils/TextWithMarkers.cs @@ -115,7 +115,7 @@ public TextMarkerPosition GetMarkedLineColPosition (char? markerChar = null) public bool TryGetMarkedPosition (out int position, char? markerChar = null) { - if (TryGetMarkedLineColPosition(out var p)) { + if (TryGetMarkedLineColPosition(out var p, markerChar)) { position = p; return true; } From b4137f83df46ea25f6e756b999112d3f808ab0c4 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 4 Sep 2024 03:04:44 -0400 Subject: [PATCH 10/19] Ignore expected errors in editor composition --- Editor.Tests/XmlTestEnvironment.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Editor.Tests/XmlTestEnvironment.cs b/Editor.Tests/XmlTestEnvironment.cs index 5380a61..b2e028d 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) { From c7b108f831fa2a5483532058cdc6fb67cf3afdaa Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 4 Sep 2024 03:07:15 -0400 Subject: [PATCH 11/19] Fix nasty bug in spine recovery The tests were accidentally "cheating" by recovering from the position before the actual target and catching up, so did not trigger this bug. Add a couple more granualar recovery tests to make it easier to debug these kinds of issues. --- Core.Tests/Parser/ParsingTests.cs | 15 ++++++++++++--- Core/Parser/XmlProcessingInstructionState.cs | 5 +++++ Core/Parser/XmlSpineParser.cs | 15 ++++++++++----- Core/Parser/XmlTagState.cs | 2 +- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/Core.Tests/Parser/ParsingTests.cs b/Core.Tests/Parser/ParsingTests.cs index f3098f8..615d415 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/Parser/XmlProcessingInstructionState.cs b/Core/Parser/XmlProcessingInstructionState.cs index d4ebbe0..546ba30 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 5db89c0..854a286 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 d9c98e3..4fd8331 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; } From e5c608d10e052e970a80174c0a672ba92cf0decc Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 5 Sep 2024 00:23:17 -0400 Subject: [PATCH 12/19] Fix line/col in TextWithMarkers --- Core.Tests/Utils/TextWithMarkers.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Core.Tests/Utils/TextWithMarkers.cs b/Core.Tests/Utils/TextWithMarkers.cs index c0ea66c..d9140fc 100644 --- a/Core.Tests/Utils/TextWithMarkers.cs +++ b/Core.Tests/Utils/TextWithMarkers.cs @@ -446,6 +446,12 @@ public static TextWithMarkers Parse (string textWithMarkers, params char[] marke for (int i = 0; i < textWithMarkers.Length; i++) { var c = textWithMarkers[i]; + int markerId = Array.IndexOf (markerChars, c); + if (markerId > -1) { + markerIndices[markerId].Add (new (sb.Length, line, col)); + continue; + } + if (c == '\n') { line++; col = 0; @@ -453,12 +459,7 @@ public static TextWithMarkers Parse (string textWithMarkers, params char[] marke col++; } - int markerId = Array.IndexOf (markerChars, c); - if (markerId > -1) { - markerIndices[markerId].Add (new (sb.Length, line, col)); - } else { - sb.Append (c); - } + sb.Append (c); } return new (sb.ToString (), markerChars, markerIndices); @@ -487,18 +488,19 @@ public static TextWithMarkers Parse (string textWithMarkers, char markerChar) for (int i = 0; i < textWithMarkers.Length; i++) { var c = textWithMarkers[i]; - if(c == '\n') { + if (c == markerChar) { + markerIndices.Add (new (sb.Length, line, col)); + continue; + } + + if (c == '\n') { line++; col = 0; } else { col++; } - if (c == markerChar) { - markerIndices.Add (new (sb.Length, line, col)); - } else { - sb.Append (c); - } + sb.Append (c); } return new (sb.ToString (), [markerChar], [markerIndices]); From 95aafd26ad633dad35fa6b17911f86a8980331fe Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Fri, 13 Sep 2024 20:17:14 -0400 Subject: [PATCH 13/19] Add Roslyn-inspired model for settings/options Nothing uses it yet but it'll be used by MonoDevelop.MSBuild, and at some point used by the XML formatter and XML editor. --- Core/IsExternalInit.cs | 7 +++ Core/Options/IOptionsReader.cs | 9 ++++ Core/Options/OptionReaderExtensions.cs | 15 +++++++ Core/Options/Option`1.cs | 50 ++++++++++++++++++++++ Core/Options/TextFormattingOptionValues.cs | 32 ++++++++++++++ Core/Options/TextFormattingOptions.cs | 46 ++++++++++++++++++++ Core/Options/XmlFormattingOptions.cs | 32 ++++++++++++++ Editor/Options/XmlEditorOptions.cs | 13 ++++++ 8 files changed, 204 insertions(+) create mode 100644 Core/IsExternalInit.cs create mode 100644 Core/Options/IOptionsReader.cs create mode 100644 Core/Options/OptionReaderExtensions.cs create mode 100644 Core/Options/Option`1.cs create mode 100644 Core/Options/TextFormattingOptionValues.cs create mode 100644 Core/Options/TextFormattingOptions.cs create mode 100644 Core/Options/XmlFormattingOptions.cs create mode 100644 Editor/Options/XmlEditorOptions.cs diff --git a/Core/IsExternalInit.cs b/Core/IsExternalInit.cs new file mode 100644 index 0000000..23c4077 --- /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 0000000..02e6563 --- /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 0000000..9cb14cd --- /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 0000000..0917967 --- /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 0000000..48d5b3e --- /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 readonly TextFormattingOptionValues Default = new (); + + public bool UseTabs { 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 () + { + UseTabs = options.GetOption (TextFormattingOptions.UseTabs); + 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 0000000..bcfa527 --- /dev/null +++ b/Core/Options/TextFormattingOptions.cs @@ -0,0 +1,46 @@ +// 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 UseTabs = new ("indent_style", false, + new EditorConfigSerializer (str => str == "tab", value => value ? "tab" : "space") + ); + + public static readonly Option TabSize = new ("tab_size", 4, true); + + public static readonly Option IndentSize = new ("indent_size", 4, true); + + public static readonly Option NewLine = new ("end_of_line", Environment.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", true, 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 0000000..2c6df96 --- /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/Editor/Options/XmlEditorOptions.cs b/Editor/Options/XmlEditorOptions.cs new file mode 100644 index 0000000..b603694 --- /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); +} From c73558ab917b291a7c41fd0357b3de9f8e273929 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 18 Sep 2024 00:01:29 -0400 Subject: [PATCH 14/19] Some fixes to the options API --- Core/Options/IOptionsReader.cs | 2 +- Core/Options/OptionReaderExtensions.cs | 4 +- Core/Options/TextFormattingOptionValues.cs | 6 +-- Core/Options/TextFormattingOptions.cs | 43 ++++++++++++---------- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Core/Options/IOptionsReader.cs b/Core/Options/IOptionsReader.cs index 02e6563..b59083f 100644 --- a/Core/Options/IOptionsReader.cs +++ b/Core/Options/IOptionsReader.cs @@ -5,5 +5,5 @@ namespace MonoDevelop.Xml.Options; public interface IOptionsReader { - bool TryGetOption (Option option, out T value); + bool TryGetOption (Option option, out T? value); } diff --git a/Core/Options/OptionReaderExtensions.cs b/Core/Options/OptionReaderExtensions.cs index 9cb14cd..1ff808f 100644 --- a/Core/Options/OptionReaderExtensions.cs +++ b/Core/Options/OptionReaderExtensions.cs @@ -7,8 +7,8 @@ public static class OptionReaderExtensions { public static T GetOption(this IOptionsReader options, Option option) { - if (options.TryGetOption (option, out T value)) { - return value; + if (options.TryGetOption (option, out T? value)) { + return value!; } return option.DefaultValue; } diff --git a/Core/Options/TextFormattingOptionValues.cs b/Core/Options/TextFormattingOptionValues.cs index 48d5b3e..71413db 100644 --- a/Core/Options/TextFormattingOptionValues.cs +++ b/Core/Options/TextFormattingOptionValues.cs @@ -13,9 +13,9 @@ namespace MonoDevelop.Xml.Options; /// public sealed record class TextFormattingOptionValues () { - public readonly TextFormattingOptionValues Default = new (); + public static readonly TextFormattingOptionValues Default = new (); - public bool UseTabs { get; init; } = false; + 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; @@ -24,7 +24,7 @@ public sealed record class TextFormattingOptionValues () public TextFormattingOptionValues (IOptionsReader options) : this () { - UseTabs = options.GetOption (TextFormattingOptions.UseTabs); + ConvertTabsToSpaces = options.GetOption (TextFormattingOptions.ConvertTabsToSpaces); TabSize = options.GetOption (TextFormattingOptions.TabSize); IndentSize = options.GetOption (TextFormattingOptions.IndentSize); NewLine = options.GetOption (TextFormattingOptions.NewLine); diff --git a/Core/Options/TextFormattingOptions.cs b/Core/Options/TextFormattingOptions.cs index bcfa527..e6718bc 100644 --- a/Core/Options/TextFormattingOptions.cs +++ b/Core/Options/TextFormattingOptions.cs @@ -12,32 +12,37 @@ namespace MonoDevelop.Xml.Options; /// public class TextFormattingOptions { - public static readonly Option UseTabs = new ("indent_style", false, - new EditorConfigSerializer (str => str == "tab", value => value ? "tab" : "space") + 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", 4, true); - - public static readonly Option IndentSize = new ("indent_size", 4, true); - - public static readonly Option NewLine = new ("end_of_line", Environment.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 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", 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, From c03f37eccd4e5ec435217b8efecd6410f2eeb3e2 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 25 Sep 2024 09:39:50 -0400 Subject: [PATCH 15/19] Always build net48 assemblies but only run net48 tests on Windows This means we can build the whole solution on Mac/Linux without needing a solution filter --- Core.Tests/MonoDevelop.Xml.Core.Tests.csproj | 13 +++++++++---- Editor.Tests/MonoDevelop.Xml.Editor.Tests.csproj | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Core.Tests/MonoDevelop.Xml.Core.Tests.csproj b/Core.Tests/MonoDevelop.Xml.Core.Tests.csproj index 2a0eb1f..703bf72 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/Editor.Tests/MonoDevelop.Xml.Editor.Tests.csproj b/Editor.Tests/MonoDevelop.Xml.Editor.Tests.csproj index 90b742e..c54c286 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 @@ - + + - From 1f8e22de1dc052a42a5804d1499c84ec28139b89 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 25 Sep 2024 09:40:16 -0400 Subject: [PATCH 16/19] Cancel background parser on dispose --- Editor/Parsing/BackgroundParser.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Editor/Parsing/BackgroundParser.cs b/Editor/Parsing/BackgroundParser.cs index 8c6893c..ba6436b 100644 --- a/Editor/Parsing/BackgroundParser.cs +++ b/Editor/Parsing/BackgroundParser.cs @@ -117,6 +117,7 @@ public Task GetOrProcessAsync (TInput input, CancellationToken token) protected virtual void Dispose (bool disposing) { + currentOperation?.Cancel(); } ~BackgroundProcessor () => Dispose (false); From c3a7208b24e6107196d12855b68de0d9b59e7bf3 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 25 Sep 2024 10:53:18 -0400 Subject: [PATCH 17/19] Add trace code to help debug nondeterministic test failures on CI --- .../Extensions/CommandServiceExtensions.cs | 54 +++++++++++++++++-- Editor/Completion/XmlCompletionSource.cs | 34 ++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/Editor.Tests/Extensions/CommandServiceExtensions.cs b/Editor.Tests/Extensions/CommandServiceExtensions.cs index d494450..ef55e83 100644 --- a/Editor.Tests/Extensions/CommandServiceExtensions.cs +++ b/Editor.Tests/Extensions/CommandServiceExtensions.cs @@ -14,6 +14,11 @@ namespace MonoDevelop.Xml.Editor.Tests.Extensions { public static class CommandServiceExtensions { + /// + /// Enables logging of additional trace information to debug nondeterministic test failures + /// + public static bool EnableDebugTrace { get; set; } + public static void Type (this IEditorCommandHandlerService commandService, string text) { foreach (var c in text) { @@ -22,6 +27,9 @@ public static void Type (this IEditorCommandHandlerService commandService, strin Enter (commandService); break; default: + if (EnableDebugTrace) { + LogTrace ($"Typing '{c}'"); + } commandService.CheckAndExecute ((v, b) => new TypeCharCommandArgs (v, b, c)); break; } @@ -29,10 +37,20 @@ public static void Type (this IEditorCommandHandlerService commandService, strin } public static void Enter (this IEditorCommandHandlerService commandService) - => commandService.CheckAndExecute ((v, b) => new ReturnKeyCommandArgs (v, b)); + { + if (EnableDebugTrace) { + LogTrace ("Invoking return key"); + } + commandService.CheckAndExecute ((v, b) => new ReturnKeyCommandArgs (v, b)); + } public static void InvokeCompletion (this IEditorCommandHandlerService commandService) - => commandService.CheckAndExecute ((v, b) => new InvokeCompletionListCommandArgs (v, b)); + { + if (EnableDebugTrace) { + LogTrace ("Invoking completion"); + } + commandService.CheckAndExecute ((v, b) => new InvokeCompletionListCommandArgs (v, b)); + } public static void CheckAndExecute ( this IEditorCommandHandlerService commandService, @@ -46,14 +64,44 @@ public static void CheckAndExecute ( throw new InvalidOperationException ($"No handler available for `{typeof (T)}`"); } - //ensure the computation is completed before we continue typing if (textView != null) { if (textView.Properties.TryGetProperty (typeof (IAsyncCompletionSession), out IAsyncCompletionSession session)) { + if (EnableDebugTrace) { + LogTrace ("Session open"); + RegisterTraceHandlers (session); + } + //ensure the computation is completed before we continue typing session.GetComputedItems (CancellationToken.None); + LogTrace ("Session open"); } + } else{ + LogTrace ("Session not open"); } } + const string TraceID = "CommandServiceExtensions.Trace"; + + 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/Completion/XmlCompletionSource.cs b/Editor/Completion/XmlCompletionSource.cs index ecd47dd..39659dd 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); } From a4e97ec6d8c5ac51be2a09da488d1f965f7545a6 Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Wed, 25 Sep 2024 19:30:12 -0400 Subject: [PATCH 18/19] Add fix/workaround for race in MSBuild completion tests on CI --- .../Extensions/CommandServiceExtensions.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Editor.Tests/Extensions/CommandServiceExtensions.cs b/Editor.Tests/Extensions/CommandServiceExtensions.cs index ef55e83..1a0afce 100644 --- a/Editor.Tests/Extensions/CommandServiceExtensions.cs +++ b/Editor.Tests/Extensions/CommandServiceExtensions.cs @@ -64,15 +64,34 @@ public static void CheckAndExecute ( throw new InvalidOperationException ($"No handler available for `{typeof (T)}`"); } + // 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); } - //ensure the computation is completed before we continue typing + + // 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); - LogTrace ("Session open"); } } else{ LogTrace ("Session not open"); @@ -81,6 +100,10 @@ public static void CheckAndExecute ( 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) From 93927f4dae3dcc508e7e00550ebb23c94664277d Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Thu, 26 Sep 2024 13:54:58 -0400 Subject: [PATCH 19/19] Yield UI thread between editor commands in tests --- Editor.Tests/Commands/AutoClosingTests.cs | 2 +- Editor.Tests/Completion/CommitTests.cs | 92 ++++++++----------- .../Extensions/CommandServiceExtensions.cs | 39 +------- Editor.Tests/Extensions/EditorAction.cs | 52 +++++++++++ .../Extensions/EditorCommandExtensions.cs | 4 + 5 files changed, 100 insertions(+), 89 deletions(-) create mode 100644 Editor.Tests/Extensions/EditorAction.cs diff --git a/Editor.Tests/Commands/AutoClosingTests.cs b/Editor.Tests/Commands/AutoClosingTests.cs index 22149bc..4864f94 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 6976802..7684c5a 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 (" public static bool EnableDebugTrace { get; set; } - public static void Type (this IEditorCommandHandlerService commandService, string text) - { - foreach (var c in text) { - switch (c) { - case '\n': - Enter (commandService); - break; - default: - if (EnableDebugTrace) { - LogTrace ($"Typing '{c}'"); - } - commandService.CheckAndExecute ((v, b) => new TypeCharCommandArgs (v, b, c)); - break; - } - } - } - - public static void Enter (this IEditorCommandHandlerService commandService) - { - if (EnableDebugTrace) { - LogTrace ("Invoking return key"); - } - commandService.CheckAndExecute ((v, b) => new ReturnKeyCommandArgs (v, b)); - } - - public static void InvokeCompletion (this IEditorCommandHandlerService commandService) - { - if (EnableDebugTrace) { - LogTrace ("Invoking completion"); - } - commandService.CheckAndExecute ((v, b) => new InvokeCompletionListCommandArgs (v, b)); - } - public static void CheckAndExecute ( this IEditorCommandHandlerService commandService, Func argsFactory) where T : EditorCommandArgs @@ -84,7 +51,7 @@ public static void CheckAndExecute ( // failures on GitHub Actions CI. // // Note that polling IAsyncCompletionSessionOperations.IsStarted does not help. - if (IsGithubActions && !session.Properties.TryGetProperty (HasWaitedForCompletionToInitializeKey, out bool hasWaited)) { + if (IsGitHubActions && !session.Properties.TryGetProperty (HasWaitedForCompletionToInitializeKey, out bool hasWaited)) { session.Properties.AddProperty (HasWaitedForCompletionToInitializeKey, true); Thread.Sleep (500); } @@ -102,7 +69,7 @@ public static void CheckAndExecute ( static readonly object HasWaitedForCompletionToInitializeKey = new(); - static readonly bool IsGithubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") != null; + static readonly bool IsGitHubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") != null; static void LogTrace(string message) => Console.WriteLine ($"{TraceID}: {message}"); diff --git a/Editor.Tests/Extensions/EditorAction.cs b/Editor.Tests/Extensions/EditorAction.cs new file mode 100644 index 0000000..81939d3 --- /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 d0376a2..2dd0cd0 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 ());