This commit is contained in:
Kirill Osenkov 2020-03-06 12:23:03 -08:00
Родитель 64c4b47204
Коммит 3823ee800d
3 изменённых файлов: 845 добавлений и 0 удалений

Просмотреть файл

@ -208,5 +208,34 @@ namespace MonoDevelop.Xml.Dom
obj = obj.Parent;
}
}
public static XNode GetNodeContainingRange (this XNode node, TextSpan span)
{
if (node is XContainer container) {
foreach (var child in container.Nodes) {
var found = child.GetNodeContainingRange (span);
if (found != null) {
return found;
}
}
}
if (node.Span.Contains (span)) {
return node;
}
return null;
}
public static void VisitSelfAndChildren (this XNode node, Action<XNode> action)
{
action (node);
if (node is XContainer container) {
foreach (var child in container.Nodes) {
VisitSelfAndChildren (child, action);
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,500 @@
// 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 System.ComponentModel.Composition;
using System.Linq;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
using MonoDevelop.Xml.Dom;
using MonoDevelop.Xml.Editor.Completion;
namespace MonoDevelop.Xml.Editor.Commands
{
[Export (typeof (ICommandHandler))]
[Name (Name)]
[ContentType (XmlContentTypeNames.XmlCore)]
[TextViewRole (PredefinedTextViewRoles.Interactive)]
class CommentUncommentCommandHandler :
ICommandHandler<CommentSelectionCommandArgs>,
ICommandHandler<UncommentSelectionCommandArgs>,
ICommandHandler<ToggleBlockCommentCommandArgs>,
ICommandHandler<ToggleLineCommentCommandArgs>
{
const string Name = nameof (CommentUncommentCommandHandler);
const string OpenComment = "<!--";
const string CloseComment = "-->";
[Import]
ITextUndoHistoryRegistry undoHistoryRegistry { get; set; }
[Import]
IEditorOperationsFactoryService editorOperationsFactoryService { get; set; }
public string DisplayName => Name;
public CommandState GetCommandState (CommentSelectionCommandArgs args) => CommandState.Available;
public CommandState GetCommandState (UncommentSelectionCommandArgs args) => CommandState.Available;
public CommandState GetCommandState (ToggleBlockCommentCommandArgs args) => CommandState.Available;
public CommandState GetCommandState (ToggleLineCommentCommandArgs args) => CommandState.Available;
enum Operation
{
Comment,
Uncomment,
Toggle
}
public bool ExecuteCommand (CommentSelectionCommandArgs args, CommandExecutionContext executionContext)
=> ExecuteCommandCore (args, executionContext, Operation.Comment);
public bool ExecuteCommand (UncommentSelectionCommandArgs args, CommandExecutionContext executionContext)
=> ExecuteCommandCore (args, executionContext, Operation.Uncomment);
public bool ExecuteCommand (ToggleBlockCommentCommandArgs args, CommandExecutionContext executionContext)
=> ExecuteCommandCore (args, executionContext, Operation.Toggle);
public bool ExecuteCommand (ToggleLineCommentCommandArgs args, CommandExecutionContext executionContext)
=> ExecuteCommandCore (args, executionContext, Operation.Toggle);
bool ExecuteCommandCore (EditorCommandArgs args, CommandExecutionContext context, Operation operation)
{
ITextView textView = args.TextView;
ITextBuffer textBuffer = args.SubjectBuffer;
if (!XmlBackgroundParser.TryGetParser (textBuffer, out var parser)) {
return false;
}
var xmlParseResult = parser.GetOrProcessAsync (textBuffer.CurrentSnapshot, default).Result;
var xmlDocumentSyntax = xmlParseResult.XDocument;
if (xmlDocumentSyntax == null) {
return false;
}
string description = operation.ToString ();
var editorOperations = editorOperationsFactoryService.GetEditorOperations (textView);
var multiSelectionBroker = textView.GetMultiSelectionBroker ();
var selectedSpans = multiSelectionBroker.AllSelections.Select (selection => selection.Extent);
using (context.OperationContext.AddScope (allowCancellation: false, description: description)) {
ITextUndoHistory undoHistory = undoHistoryRegistry.RegisterHistory (textBuffer);
using (ITextUndoTransaction undoTransaction = undoHistory.CreateTransaction (description)) {
switch (operation) {
case Operation.Comment:
CommentSelection (textBuffer, selectedSpans, xmlDocumentSyntax, editorOperations, multiSelectionBroker);
break;
case Operation.Uncomment:
UncommentSelection (textBuffer, selectedSpans, xmlDocumentSyntax);
break;
case Operation.Toggle:
ToggleCommentSelection (textBuffer, selectedSpans, xmlDocumentSyntax, editorOperations, multiSelectionBroker);
break;
}
undoTransaction.Complete ();
}
}
return true;
}
public static void CommentSelection (
ITextBuffer textBuffer,
IEnumerable<VirtualSnapshotSpan> selectedSpans,
XDocument xmlDocumentSyntax,
IEditorOperations editorOperations = null,
IMultiSelectionBroker multiSelectionBroker = null)
{
var snapshot = textBuffer.CurrentSnapshot;
var spansToExpandIntoComments = new List<SnapshotSpan> ();
var newCommentInsertionPoints = new List<VirtualSnapshotPoint> ();
foreach (var selectedSpan in selectedSpans) {
// empty selection on an empty line results in inserting a new comment
if (selectedSpan.IsEmpty && string.IsNullOrWhiteSpace (snapshot.GetLineFromPosition (selectedSpan.Start.Position).GetText ())) {
newCommentInsertionPoints.Add (selectedSpan.Start);
} else {
spansToExpandIntoComments.Add (selectedSpan.SnapshotSpan);
}
}
NormalizedSnapshotSpanCollection commentSpans = NormalizedSnapshotSpanCollection.Empty;
if (spansToExpandIntoComments.Any ()) {
commentSpans = GetCommentableSpansInSelection (xmlDocumentSyntax, spansToExpandIntoComments);
}
using (var edit = textBuffer.CreateEdit ()) {
if (commentSpans.Any ()) {
CommentSpans (edit, commentSpans);
}
if (newCommentInsertionPoints.Any ()) {
CommentEmptySpans (edit, newCommentInsertionPoints, editorOperations);
}
edit.Apply ();
}
var newSnapshot = textBuffer.CurrentSnapshot;
// Now fix up the selections after the edit.
var translatedInsertionPoints = newCommentInsertionPoints.Select (p => p.TranslateTo (newSnapshot)).ToHashSet ();
var fixupSelectionStarts = selectedSpans.Where (s => !s.IsEmpty).ToDictionary (
c => c.Start.TranslateTo (newSnapshot, PointTrackingMode.Positive),
c => c.Start.TranslateTo (newSnapshot, PointTrackingMode.Negative));
if (multiSelectionBroker != null) {
multiSelectionBroker.PerformActionOnAllSelections (transformer => {
// for newly inserted comments position the caret inside the comment
if (translatedInsertionPoints.Contains (transformer.Selection.ActivePoint)) {
transformer.MoveTo (
new VirtualSnapshotPoint (transformer.Selection.End.Position - CloseComment.Length),
select: false,
insertionPointAffinity: PositionAffinity.Successor);
// for commented code make sure the new selection includes the opening <!--
} else if (fixupSelectionStarts.TryGetValue (transformer.Selection.Start, out var newStart)) {
var end = transformer.Selection.End;
transformer.MoveTo (newStart, select: false, PositionAffinity.Successor);
transformer.MoveTo (end, select: true, PositionAffinity.Successor);
}
});
}
}
public static void UncommentSelection (
ITextBuffer textBuffer,
IEnumerable<VirtualSnapshotSpan> selectedSpans,
XDocument xmlDocumentSyntax)
{
var commentedSpans = GetCommentedSpansInSelection (xmlDocumentSyntax, selectedSpans);
if (commentedSpans == null || !commentedSpans.Any ()) {
return;
}
using (var edit = textBuffer.CreateEdit ()) {
UncommentSpans (edit, commentedSpans);
edit.Apply ();
}
}
public static void ToggleCommentSelection (
ITextBuffer textBuffer,
IEnumerable<VirtualSnapshotSpan> selectedSpans,
XDocument xmlDocumentSyntax,
IEditorOperations editorOperations = null,
IMultiSelectionBroker multiSelectionBroker = null)
{
var commentedSpans = GetCommentedSpansInSelection (xmlDocumentSyntax, selectedSpans);
if (!commentedSpans.Any ()) {
CommentSelection (
textBuffer,
selectedSpans,
xmlDocumentSyntax,
editorOperations,
multiSelectionBroker);
return;
}
UncommentSelection (textBuffer, selectedSpans, xmlDocumentSyntax);
}
/// <summary>
/// Performs the actual insertions of comment markers around the <see cref="commentSpans"/>
/// </summary>
static void CommentSpans (ITextEdit edit, NormalizedSnapshotSpanCollection commentSpans)
{
foreach (var commentSpan in commentSpans) {
edit.Insert (commentSpan.Start, OpenComment);
edit.Insert (commentSpan.End, CloseComment);
}
}
/// <summary>
/// Inserts a new comment at each virtual point, materializing the virtual space if necessary
/// </summary>
static void CommentEmptySpans (ITextEdit edit, IEnumerable<VirtualSnapshotPoint> virtualPoints, IEditorOperations editorOperations)
{
foreach (var virtualPoint in virtualPoints) {
if (virtualPoint.IsInVirtualSpace) {
string leadingWhitespace;
if (editorOperations != null) {
leadingWhitespace = editorOperations.GetWhitespaceForVirtualSpace (virtualPoint);
} else {
leadingWhitespace = new string (' ', virtualPoint.VirtualSpaces);
}
if (leadingWhitespace.Length > 0) {
edit.Insert (virtualPoint.Position, leadingWhitespace);
}
}
edit.Insert (virtualPoint.Position, OpenComment);
edit.Insert (virtualPoint.Position, CloseComment);
}
}
/// <summary>
/// Removes the comment markers from a set of spans
/// </summary>
static void UncommentSpans (ITextEdit edit, IEnumerable<SnapshotSpan> commentedSpans)
{
int beginCommentLength = OpenComment.Length;
int endCommentLength = CloseComment.Length;
foreach (var commentSpan in commentedSpans) {
edit.Delete (commentSpan.Start, beginCommentLength);
edit.Delete (commentSpan.End - endCommentLength, endCommentLength);
}
}
/// <summary>
/// Calculates the syntactically valid non-commented portions of a set of spans
/// excluding the already commented portions.
/// </summary>
static NormalizedSnapshotSpanCollection GetCommentableSpansInSelection (XDocument xmlDocumentSyntax, IEnumerable<SnapshotSpan> selectedSpans)
{
var commentSpans = new List<SnapshotSpan> ();
var snapshot = selectedSpans.First ().Snapshot;
var validSpans = xmlDocumentSyntax.GetValidCommentSpans (selectedSpans.Select (s => GetDesiredCommentSpan (s)));
foreach (var singleValidSpan in validSpans) {
var snapshotSpan = new SnapshotSpan (snapshot, new Span (singleValidSpan.Start, singleValidSpan.Length));
commentSpans.Add (snapshotSpan);
}
return new NormalizedSnapshotSpanCollection (commentSpans);
}
/// <summary>
/// Returns the already commented portions intersecting a set of spans.
/// </summary>
static IEnumerable<SnapshotSpan> GetCommentedSpansInSelection (XDocument xmlDocumentSyntax, IEnumerable<VirtualSnapshotSpan> selectedSpans)
{
var commentedSpans = new List<SnapshotSpan> ();
var snapshot = selectedSpans.First ().Snapshot;
foreach (var selectedSpan in selectedSpans) {
bool allowLineUncomment = true;
if (selectedSpan.IsEmpty) {
// For point selection, first see which comments are returned for the point span
// If the strictly inside a commented node, just uncommented that node
// otherwise, allow line uncomment
var start = selectedSpan.Start.Position.Position;
var selectionCommentedSpans =
xmlDocumentSyntax.GetCommentedSpans (new[] { new TextSpan (start, 0) }).ToList ();
foreach (var selectionCommentedSpan in selectionCommentedSpans) {
if (selectionCommentedSpan.Contains (start) &&
selectionCommentedSpan.Start != start &&
selectionCommentedSpan.End != start) {
var snapshotSpan = new SnapshotSpan (snapshot, selectionCommentedSpan.Start, selectionCommentedSpan.Length);
commentedSpans.Add (snapshotSpan);
allowLineUncomment = false;
break;
}
}
}
if (allowLineUncomment) {
var desiredCommentSpan = GetDesiredCommentSpan (selectedSpan.SnapshotSpan);
var commentedSpans2 = xmlDocumentSyntax.GetCommentedSpans (new[] { desiredCommentSpan });
foreach (var commentedSpan2 in commentedSpans2) {
var snapshotSpan = new SnapshotSpan (snapshot, commentedSpan2.Start, commentedSpan2.Length);
commentedSpans.Add (snapshotSpan);
}
}
}
return commentedSpans;
}
/// <summary>
/// Expands the selection to the non-whitespace portion of the current line
/// in case the caret is in whitespace to the left from the XML to comment/uncomment
/// </summary>
static TextSpan GetDesiredCommentSpan (SnapshotSpan selectedSpan)
{
ITextSnapshot snapshot = selectedSpan.Snapshot;
if (!selectedSpan.IsEmpty) {
int selectionLength = selectedSpan.Length;
// tweak the selection end to not include the last line break
while (selectionLength > 0 && IsLineBreak (snapshot[selectedSpan.Start + selectionLength - 1])) {
selectionLength--;
}
return new TextSpan (selectedSpan.Start, selectionLength);
}
// Comment line for empty selections (first to last non-whitespace character)
var line = snapshot.GetLineFromPosition (selectedSpan.Start);
int? start = null;
for (int i = line.Start; i < line.End.Position; i++) {
if (!IsWhiteSpace (snapshot[i])) {
start = i;
break;
}
}
if (start == null) {
return new TextSpan (selectedSpan.Start, 0);
}
int end = start.Value;
for (int i = line.End.Position - 1; i >= end; i--) {
if (!IsWhiteSpace (snapshot[i])) {
// need to add 1 since end is exclusive
end = i + 1;
break;
}
}
return TextSpan.FromBounds (start.Value, end);
bool IsWhiteSpace (char c)
{
return c == ' ' || c == '\t' || (c > (char)128 && char.IsWhiteSpace (c));
}
bool IsLineBreak (char c)
{
return c == '\n' || c == '\r';
}
}
}
static class CommentUtilities
{
public static IEnumerable<TextSpan> GetValidCommentSpans (this XContainer node, IEnumerable<TextSpan> selectedSpans)
=> GetCommentSpans (node, selectedSpans, returnComments: false);
public static IEnumerable<TextSpan> GetCommentedSpans (this XContainer node, IEnumerable<TextSpan> selectedSpans)
=> GetCommentSpans (node, selectedSpans, returnComments: true);
static IEnumerable<TextSpan> GetCommentSpans (this XContainer node, IEnumerable<TextSpan> selectedSpans, bool returnComments)
{
var commentSpans = new List<TextSpan> ();
// First unify, normalize and deduplicate syntactic spans since multiple selections can result
// in a single syntactic span to be commented
var regions = new List<Span> ();
foreach (var selectedSpan in selectedSpans) {
var region = node.GetValidCommentRegion (selectedSpan);
regions.Add (region.ToSpan ());
}
var normalizedRegions = new NormalizedSpanCollection (regions);
// Then for each region cut out the existing comments that may be inside
foreach (var currentRegion in normalizedRegions) {
int currentStart = currentRegion.Start;
// Creates comments such that current comments are excluded
var parentNode = node.GetNodeContainingRange (currentRegion.ToTextSpan ());
parentNode.VisitSelfAndChildren (child => {
if (child is XComment comment) {
// ignore comments outside our range
if (!currentRegion.IntersectsWith (comment.Span.ToSpan ())) {
return;
}
var commentNodeSpan = comment.Span;
if (returnComments)
commentSpans.Add (commentNodeSpan);
else {
var validCommentSpan = TextSpan.FromBounds (currentStart, commentNodeSpan.Start);
if (validCommentSpan.Length != 0) {
commentSpans.Add (validCommentSpan);
}
currentStart = commentNodeSpan.End;
}
}
});
if (!returnComments) {
if (currentStart <= currentRegion.End) {
var remainingCommentSpan = TextSpan.FromBounds (currentStart, currentRegion.End);
if (remainingCommentSpan.Equals (currentRegion) || remainingCommentSpan.Length != 0) {
// Comment any remaining uncommented area
commentSpans.Add (remainingCommentSpan);
}
}
}
}
return commentSpans.Distinct ();
}
static TextSpan ToTextSpan (this Span span)
{
return new TextSpan (span.Start, span.Length);
}
static Span ToSpan (this TextSpan textSpan)
{
return new Span (textSpan.Start, textSpan.Length);
}
static TextSpan GetValidCommentRegion (this XContainer node, TextSpan commentSpan)
{
var commentSpanStart = GetCommentRegion (node, commentSpan.Start, commentSpan);
if (commentSpan.Length == 0) {
return commentSpanStart;
}
var commentSpanEnd = GetCommentRegion (node, commentSpan.End - 1, commentSpan);
return TextSpan.FromBounds (
start: Math.Min (commentSpanStart.Start, commentSpanEnd.Start),
end: Math.Max (Math.Max (commentSpanStart.End, commentSpanEnd.End), commentSpan.End));
}
static TextSpan GetCommentRegion (this XContainer node, int position, TextSpan span)
{
var nodeAtPosition = node.FindAtOffset (position);
// if the selection starts or ends in text, we want to preserve the
// exact span the user has selected and split the text at that boundary
if (nodeAtPosition is XText) {
return span;
}
if (nodeAtPosition is XComment ||
nodeAtPosition is XProcessingInstruction ||
nodeAtPosition is XCData) {
return nodeAtPosition.Span;
}
var nearestParentElement = nodeAtPosition.SelfAndParentsOfType<XElement> ().FirstOrDefault ();
if (nearestParentElement == null) {
return new TextSpan (position, 0);
}
var endSpan = nearestParentElement.ClosingTag;
if (endSpan == null) {
return nodeAtPosition.Span;
}
int start = nearestParentElement.Span.Start;
return new TextSpan (start, endSpan.Span.End - start);
}
}
}

Просмотреть файл

@ -0,0 +1,316 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.MiniEditor;
using Microsoft.VisualStudio.Text;
using MonoDevelop.Xml.Dom;
using MonoDevelop.Xml.Editor;
using MonoDevelop.Xml.Editor.Commands;
using MonoDevelop.Xml.Editor.Completion;
using MonoDevelop.Xml.Tests.Completion;
using MonoDevelop.Xml.Tests.EditorTestHelpers;
using NUnit.Framework;
namespace MonoDevelop.Xml.Tests
{
[TestFixture]
public class CommentUncommentTests : EditorTestBase
{
public const char VirtualSpaceMarker = '→';
public const char SelectionStartMarker = '{';
public const char SelectionEndMarker = '}';
protected override string ContentTypeName => XmlContentTypeNames.XmlCore;
protected override (EditorEnvironment, EditorCatalog) InitializeEnvironment () => XmlTestEnvironment.EnsureInitialized ();
[Test]
[TestCase (@"{}<x>
</x>", @"<!--<x>
</x>-->")]
[TestCase (@"<x>{}
</x>", @"<!--<x>
</x>-->")]
[TestCase (@"{<x>}
</x>", @"<!--<x>
</x>-->")]
[TestCase (@"{<x>
</x>}", @"<!--<x>
</x>-->")]
[TestCase (@"<x>
{}</x>", @"<!--<x>
</x>-->")]
[TestCase (@"<x>
{<a></a>}
</x>", @"<x>
<!--<a></a>-->
</x>")]
[TestCase (@"<x>
<a></a>{}
</x>", @"<x>
<!--<a></a>-->
</x>")]
[TestCase (@"<x>
<a></a>{}
<!--c-->
</x>", @"<x>
<!--<a></a>-->
<!--c-->
</x>")]
[TestCase (@"{}<x>
<a></a>
<!--c-->
</x>", @"<!--<x>
<a></a>
--><!--c--><!--
</x>-->", false)]
[TestCase (@"{}<x />", @"<!--<x />-->")]
[TestCase (@"<x />{}", @"<!--<x />-->")]
[TestCase (@"<x
{a}=""a""/>", @"<!--<x
a=""a""/>-->")]
[TestCase (@"{}<?xml ?>", @"<!--<?xml ?>-->")]
[TestCase (@"{}<!--x-->", @"<!--x-->", false)]
[TestCase (@"{<x/>
<x/>}", @"<!--<x/>
<x/>-->")]
[TestCase (@"<{x/>
<}x/>", @"<!--<x/>
<x/>-->")]
[TestCase (@"<x>
{text}
<x/>", @"<x>
<!--text-->
<x/>")]
[TestCase (@"<x>
{text}
more text
<x/>", @"<x>
<!--text-->
more text
<x/>")]
[TestCase (@"<x>
text
{<a/>}
more text
<x/>", @"<x>
text
<!--<a/>-->
more text
<x/>")]
[TestCase (@"<x>
{text
<a/>}
more text
<x/>", @"<x>
<!--text
<a/>-->
more text
<x/>")]
[TestCase (@"<x>
{ <a/>
<a/>
}<x/>", @"<x>
<!-- <a/>
<a/>-->
<x/>")]
[TestCase (@"<x>
{}
<x/>", @"<x>
<!---->
<x/>")]
[TestCase (@"<x>
{}
<x/>", @"<x>
<!---->
<x/>")]
[TestCase (@"<x>
{<a/>}
{<a/>}
<x/>", @"<x>
<!--<a/>-->
<!--<a/>-->
<x/>")]
[TestCase (@"<x>
{<a><!-- comment --></a>}
{<a></a>}
<x/>", @"<x>
<!--<a>--><!-- comment --><!--</a>-->
<!--<a></a>-->
<x/>", false)]
[TestCase (@"<x>
{<![CDATA[bar]]>}
<x/>", @"<x>
<!--<![CDATA[bar]]>-->
<x/>", false)]
public void TestComment (string sourceText, string expectedText, bool toggle = true)
{
var (buffer, snapshotSpans, document) = GetBufferSpansAndDocument (sourceText);
CommentUncommentCommandHandler.CommentSelection (buffer, snapshotSpans, document);
var actualText = buffer.CurrentSnapshot.GetText ();
Assert.AreEqual (expectedText, actualText);
// toggle should also work in most scenarios for comment
if (toggle) {
TestToggle (sourceText, expectedText);
}
}
[Test]
[TestCase (@"{}<!--<x>
</x>-->", @"<x>
</x>")]
[TestCase (@"{<!--<x>
</x>-->}", @"<x>
</x>")]
[TestCase (@"{<x>
</x>}", @"<x>
</x>", false)]
[TestCase (@"<x>
{<!-- text -->}
</x>", @"<x>
text
</x>")]
[TestCase (@"{}<!--<x>
--><!-- text --><!--
</x>-->", @"<x>
<!-- text --><!--
</x>-->", false)]
[TestCase (@"<x>
{<}!--<a/>-->
<b/>
<!--<a/>-->{}
{<!--<a/>-->}
</x>", @"<x>
<a/>
<b/>
<a/>
<a/>
</x>", false)]
public void TestUncomment (string sourceText, string expectedText, bool toggle = true)
{
var (buffer, snapshotSpans, document) = GetBufferSpansAndDocument (sourceText);
CommentUncommentCommandHandler.UncommentSelection (buffer, snapshotSpans, document);
var actualText = buffer.CurrentSnapshot.GetText ();
Assert.AreEqual (expectedText, actualText);
// toggle should also work in most scenarios for uncomment
if (toggle) {
TestToggle (sourceText, expectedText);
}
}
void TestToggle (string sourceText, string expectedText)
{
var (buffer, snapshotSpans, document) = GetBufferSpansAndDocument (sourceText);
CommentUncommentCommandHandler.ToggleCommentSelection (buffer, snapshotSpans, document);
var actualText = buffer.CurrentSnapshot.GetText ();
Assert.AreEqual (expectedText, actualText);
}
(ITextBuffer buffer, IEnumerable<VirtualSnapshotSpan> virtualSnapshotSpans, XDocument document) GetBufferSpansAndDocument (string sourceText)
{
var (text, spans) = GetTextAndSpans (sourceText);
var buffer = CreateTextBuffer (text);
var parser = XmlBackgroundParser.GetParser<XmlBackgroundParser> (buffer);
var snapshot = buffer.CurrentSnapshot;
var virtualSnapshotSpans = spans.Select (s => new VirtualSnapshotSpan (
new VirtualSnapshotPoint (new SnapshotPoint (snapshot, s.Span.Start), s.VirtualSpacesAtStart),
new VirtualSnapshotPoint (new SnapshotPoint (snapshot, s.Span.End), s.VirtualSpacesAtEnd)));
var document = parser.GetOrProcessAsync (buffer.CurrentSnapshot, default).Result.XDocument;
return (buffer, virtualSnapshotSpans, document);
}
private struct VirtualSpan
{
public Span Span;
public int VirtualSpacesAtStart;
public int VirtualSpacesAtEnd;
}
(string text, IEnumerable<VirtualSpan> spans) GetTextAndSpans(string textWithSpans)
{
var spans = new List<VirtualSpan> ();
var sb = new StringBuilder ();
int index = 0;
int start = 0;
int virtualSpace = 0;
int virtualSpacesAtStart = 0;
for (int i = 0; i < textWithSpans.Length; i++) {
char ch = textWithSpans[i];
if (ch == VirtualSpaceMarker) {
virtualSpace++;
continue;
}
if (ch == SelectionStartMarker) {
start = index;
virtualSpacesAtStart = virtualSpace;
} else if (ch == SelectionEndMarker) {
var span = new VirtualSpan {
Span = new Span(start, index - start),
VirtualSpacesAtStart = virtualSpacesAtStart,
VirtualSpacesAtEnd = virtualSpace
};
spans.Add (span);
} else {
sb.Append (ch);
index++;
virtualSpace = 0;
}
}
return (sb.ToString(), spans);
}
}
}