File: Workspaces\EditorTestHostDocument.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.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Test.Utilities;
 
public class EditorTestHostDocument : TestHostDocument
{
    private static readonly ImmutableArray<string> s_defaultRoles = ImmutableArray.Create<string>
        (PredefinedTextViewRoles.Analyzable,
        PredefinedTextViewRoles.Document,
        PredefinedTextViewRoles.Editable,
        PredefinedTextViewRoles.Interactive,
        PredefinedTextViewRoles.Zoomable);
 
    private readonly ImmutableArray<string> _roles;
 
    private IWpfTextView? _textView;
 
    /// <summary>
    /// The <see cref="ITextBuffer2"/> for this document. Null if not yet created.
    /// </summary>
    private ITextBuffer2? _textBuffer;
 
    /// <summary>
    /// The <see cref="ITextSnapshot"/> when the buffer was first created, which can be used for tracking changes to the current buffer.
    /// </summary>
    private ITextSnapshot? _initialTextSnapshot;
 
    internal EditorTestHostDocument(
        ExportProvider exportProvider,
        HostLanguageServices? languageServiceProvider,
        string code,
        string name,
        string filePath,
        int? cursorPosition,
        IDictionary<string, ImmutableArray<TextSpan>> spans,
        SourceCodeKind sourceCodeKind = SourceCodeKind.Regular,
        IReadOnlyList<string>? folders = null,
        bool isLinkFile = false,
        IDocumentServiceProvider? documentServiceProvider = null,
        ImmutableArray<string> roles = default,
        ITextBuffer2? textBuffer = null,
        ISourceGenerator? generator = null)
        : base(
            exportProvider,
            languageServiceProvider,
            code,
            name,
            filePath,
            cursorPosition,
            spans,
            sourceCodeKind,
            folders,
            isLinkFile,
            documentServiceProvider,
            generator)
    {
        _roles = roles.IsDefault ? s_defaultRoles : roles;
 
        if (textBuffer != null)
        {
            _textBuffer = textBuffer;
            _initialTextSnapshot = textBuffer.CurrentSnapshot;
        }
    }
 
    internal EditorTestHostDocument(
        string text = "",
        string displayName = "",
        SourceCodeKind sourceCodeKind = SourceCodeKind.Regular,
        DocumentId? id = null,
        string? filePath = null,
        IReadOnlyList<string>? folders = null,
        ExportProvider? exportProvider = null,
        IDocumentServiceProvider? documentServiceProvider = null)
        : base(
            text,
            displayName,
            sourceCodeKind,
            id,
            filePath,
            folders,
            exportProvider,
            documentServiceProvider)
    {
    }
 
    // TODO: delete this
    public ITextSnapshot InitialTextSnapshot
    {
        get
        {
            Contract.ThrowIfNull(_initialTextSnapshot);
            return _initialTextSnapshot;
        }
    }
 
    public IWpfTextView GetTextView()
    {
        if (_textView == null)
        {
            Contract.ThrowIfNull(ExportProvider, $"Can only create text view for {nameof(TestHostDocument)} created with {nameof(ExportProvider)}");
            WpfTestRunner.RequireWpfFact($"Creates an {nameof(IWpfTextView)} through {nameof(TestHostDocument)}.{nameof(GetTextView)}");
 
            var factory = ExportProvider.GetExportedValue<ITextEditorFactoryService>();
 
            // Every default role but outlining. Starting in 15.2, the editor
            // OutliningManager imports JoinableTaskContext in a way that's 
            // difficult to satisfy in our unit tests. Since we don't directly
            // depend on it, just disable it
            var roles = factory.CreateTextViewRoleSet(_roles);
            _textView = factory.CreateTextView(this.GetTextBuffer(), roles);
            if (this.CursorPosition.HasValue)
            {
                _textView.Caret.MoveTo(new SnapshotPoint(_textView.TextSnapshot, CursorPosition.Value));
            }
            else if (this.SelectedSpans.IsSingle())
            {
                var span = this.SelectedSpans.Single();
                _textView.Selection.Select(new SnapshotSpan(_textView.TextSnapshot, new Span(span.Start, span.Length)), false);
            }
        }
 
        return _textView;
    }
 
    public ITextBuffer2 GetTextBuffer()
    {
        var workspace = (EditorTestWorkspace?)LanguageServiceProvider?.WorkspaceServices.Workspace;
 
        if (_textBuffer == null)
        {
            Contract.ThrowIfNull(LanguageServiceProvider, $"To get a text buffer for a {nameof(TestHostDocument)}, it must have been parented in a project.");
            var contentType = LanguageServiceProvider.GetRequiredService<IContentTypeLanguageService>().GetDefaultContentType();
 
            _textBuffer = workspace!.GetOrCreateBufferForPath(FilePath, contentType, LanguageServiceProvider.Language, InitialText);
            _initialTextSnapshot = _textBuffer.CurrentSnapshot;
        }
 
        if (workspace != null)
        {
            // Open (or reopen) any files that were closed in this call. We do this for all linked copies at once.
            foreach (var linkedId in workspace.CurrentSolution.GetDocumentIdsWithFilePath(FilePath).Concat(this.Id))
            {
                if (workspace.IsDocumentOpen(linkedId))
                    continue;
 
                if (workspace.GetTestDocument(linkedId) is { } testDocument)
                {
                    if (testDocument.IsSourceGenerated)
                    {
                        var threadingContext = workspace.GetService<IThreadingContext>();
                        var document = threadingContext.JoinableTaskFactory.Run(() => workspace.CurrentSolution.GetSourceGeneratedDocumentAsync(testDocument.Id, CancellationToken.None).AsTask());
                        Contract.ThrowIfNull(document);
 
                        workspace.OnSourceGeneratedDocumentOpened(_textBuffer.AsTextContainer(), document);
                    }
                    else
                    {
                        // If there is a linked file, we'll start the non-linked one as being the primary context, which some tests depend on.
                        workspace.OnDocumentOpened(linkedId, _textBuffer.AsTextContainer(), isCurrentContext: !testDocument.IsLinkFile);
                    }
                }
                else if (workspace.GetTestAdditionalDocument(linkedId) is { } testAdditionalDocument)
                {
                    workspace.OnAdditionalDocumentOpened(linkedId, _textBuffer.AsTextContainer());
                }
                else if (workspace.GetTestAnalyzerConfigDocument(linkedId) is { } testAnalyzerConfigDocument)
                {
                    workspace.OnAnalyzerConfigDocumentOpened(linkedId, _textBuffer.AsTextContainer());
                }
            }
        }
 
        return _textBuffer;
    }
 
    public override void Open()
        => GetOpenTextContainer();
 
    public SourceTextContainer GetOpenTextContainer()
        => this.GetTextBuffer().AsTextContainer();
 
    private void Update(string newText)
    {
        using var edit = this.GetTextBuffer().CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null);
        edit.Replace(new Span(0, this.GetTextBuffer().CurrentSnapshot.Length), newText);
        edit.Apply();
    }
 
    internal void CloseTextView()
    {
        if (_textView != null && !_textView.IsClosed)
        {
            _textView.Close();
            _textView = null;
        }
    }
 
    internal void Update(SourceText newText)
    {
        var buffer = GetTextBuffer();
        using var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null);
        var oldText = buffer.CurrentSnapshot.AsText();
        var changes = newText.GetTextChanges(oldText);
 
        foreach (var change in changes)
        {
            edit.Replace(change.Span.Start, change.Span.Length, change.NewText);
        }
 
        edit.Apply();
    }
}