File: Workspaces\TestWorkspaceFixture.cs
Web Access
Project: src\src\EditorFeatures\TestUtilities\Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj (Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.VisualStudio.Text;
using Roslyn.Test.Utilities;
 
namespace Microsoft.CodeAnalysis.Test.Utilities;
 
public abstract class TestWorkspaceFixture : IDisposable
{
    public int Position;
    public string Code;
 
    private EditorTestWorkspace _workspace;
    private EditorTestHostDocument _currentDocument;
 
    public EditorTestHostDocument CurrentDocument => _currentDocument ?? _workspace.Documents.Single();
 
    public EditorTestWorkspace GetWorkspace(TestComposition composition = null)
    {
        _workspace ??= CreateWorkspace(composition);
        return _workspace;
    }
 
    public EditorTestWorkspace GetWorkspace(string markup, TestComposition composition = null, string workspaceKind = null)
    {
        // If it looks like XML, we'll treat it as XML; any parse error would be rejected and will throw.
        // We'll do a case insensitive search here so if somebody has a lowercase W it'll be tried (and
        // rejected by the XML parser) rather than treated as regular text.
        if (markup.TrimStart().StartsWith("<Workspace>", StringComparison.OrdinalIgnoreCase))
        {
            CloseTextView();
            _workspace?.Dispose();
 
            _workspace = EditorTestWorkspace.CreateWorkspace(XElement.Parse(markup), composition: composition, workspaceKind: workspaceKind);
            _currentDocument = _workspace.Documents.First(d => d.CursorPosition.HasValue);
            Position = _currentDocument.CursorPosition.Value;
            Code = _currentDocument.GetTextBuffer().CurrentSnapshot.GetText();
            return _workspace;
        }
        else
        {
            MarkupTestFile.GetPosition(markup.NormalizeLineEndings(), out Code, out Position);
            var workspace = GetWorkspace(composition);
            _currentDocument = workspace.Documents.Single();
            return workspace;
        }
    }
 
    protected abstract EditorTestWorkspace CreateWorkspace(TestComposition composition);
 
    public void Dispose()
    {
        if (_workspace is null)
            return;
 
        try
        {
            CloseTextView();
            _currentDocument = null;
            Code = null;
            Position = 0;
            _workspace?.Dispose();
        }
        finally
        {
            _workspace = null;
        }
    }
 
    public Document UpdateDocument(string text, SourceCodeKind sourceCodeKind, bool cleanBeforeUpdate = true)
    {
        var hostDocument = _currentDocument ?? (GetWorkspace()).Documents.Single();
 
        // clear the document
        if (cleanBeforeUpdate)
        {
            UpdateText(hostDocument.GetTextBuffer(), string.Empty);
        }
 
        // and set the content
        UpdateText(hostDocument.GetTextBuffer(), text);
 
        GetWorkspace().OnDocumentSourceCodeKindChanged(hostDocument.Id, sourceCodeKind);
 
        return GetWorkspace().CurrentSolution.GetDocument(hostDocument.Id);
    }
 
    private static void UpdateText(ITextBuffer textBuffer, string text)
    {
        using var edit = textBuffer.CreateEdit();
        edit.Replace(0, textBuffer.CurrentSnapshot.Length, text);
        edit.Apply();
    }
 
    private void CloseTextView()
    {
        // The standard use for TestWorkspaceFixture is to call this method in the test's dispose to make sure it's ready to be used for
        // the next test. But some tests in a test class won't use it, so _workspace might still be null.
        if (_workspace?.Documents != null)
        {
            foreach (var document in _workspace?.Documents)
            {
                document.CloseTextView();
            }
        }
 
        // The editor caches TextFormattingRunProperties instances for better perf, but since things like
        // Brushes are DispatcherObjects, they are tied to the thread they are created on. Since we're going
        // to be run on a different thread, clear out their collection.
        var textFormattingRunPropertiesType = typeof(VisualStudio.Text.Formatting.TextFormattingRunProperties);
        var existingPropertiesField = textFormattingRunPropertiesType.GetField("ExistingProperties", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
        var existingProperties = (List<VisualStudio.Text.Formatting.TextFormattingRunProperties>)existingPropertiesField.GetValue(null);
        existingProperties.Clear();
    }
}