File: Workspaces\TestHostDocument.cs
Web Access
Project: src\src\Workspaces\CoreTestUtilities\Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj (Microsoft.CodeAnalysis.Workspaces.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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Test.Utilities;
 
public class TestHostDocument
{
    protected HostLanguageServices? LanguageServiceProvider;
    protected readonly string InitialText;
    protected readonly ExportProvider? ExportProvider;
 
    private DocumentId? _id;
    private AbstractTestHostProject? _project;
    private readonly IReadOnlyList<string>? _folders;
    private readonly IDocumentServiceProvider? _documentServiceProvider;
    private readonly TestDocumentLoader _loader;
 
    public DocumentId Id
    {
        get
        {
            // For source generated documents, the workspace generates the ID. Thus we won't
            // know it until we have a workspace we can go and get the ID from. We of course could
            // duplicate the algorithm but this lets us keep this code oblivious to the internals
            // of the workspace implementation.
            if (IsSourceGenerated && _id is null)
            {
                var workspace = LanguageServiceProvider!.WorkspaceServices.Workspace;
                var project = workspace.CurrentSolution.GetRequiredProject(_project!.Id);
                var sourceGeneratedDocuments = project.GetSourceGeneratedDocumentsAsync(CancellationToken.None).AsTask().Result;
                _id = sourceGeneratedDocuments.Single(d => d.FilePath == this.FilePath).Id;
            }
 
            Contract.ThrowIfNull(_id);
            return _id;
        }
    }
 
    public AbstractTestHostProject Project
    {
        get
        {
            Contract.ThrowIfNull(_project);
            return _project;
        }
    }
 
    public string Name { get; }
    public SourceCodeKind SourceCodeKind { get; }
    public string? FilePath { get; }
    public SourceHashAlgorithm ChecksumAlgorithm { get; } = SourceHashAlgorithms.Default;
 
    public int? CursorPosition { get; }
    public IList<TextSpan> SelectedSpans { get; } = [];
    public IDictionary<string, ImmutableArray<TextSpan>> AnnotatedSpans { get; } = new Dictionary<string, ImmutableArray<TextSpan>>();
 
    /// <summary>
    /// If a file exists in ProjectA and is added to ProjectB as a link, then this returns
    /// false for the document in ProjectA and true for the document in ProjectB.
    /// </summary>
    public bool IsLinkFile { get; }
 
    /// <summary>
    /// If this is a source generated file, the source generator that produced this document.
    /// </summary>
    public ISourceGenerator? Generator;
 
    /// <summary>
    /// Returns true if this will be a source generated file instead of a regular one.
    /// </summary>
    [MemberNotNullWhen(true, nameof(Generator))]
    public bool IsSourceGenerated => Generator is not null;
 
    internal TestHostDocument(
        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,
        ISourceGenerator? generator = null)
    {
        Contract.ThrowIfNull(filePath);
        Contract.ThrowIfFalse(generator == null || PathUtilities.IsAbsolute(filePath));
 
        ExportProvider = exportProvider;
        LanguageServiceProvider = languageServiceProvider;
        InitialText = code;
        Name = name;
        FilePath = filePath;
        _folders = folders;
        this.CursorPosition = cursorPosition;
        SourceCodeKind = sourceCodeKind;
        this.IsLinkFile = isLinkFile;
        Generator = generator;
        _documentServiceProvider = documentServiceProvider;
 
        if (spans.TryGetValue(string.Empty, out var textSpans))
        {
            this.SelectedSpans = textSpans;
        }
 
        foreach (var namedSpanList in spans.Where(s => s.Key != string.Empty))
        {
            this.AnnotatedSpans.Add(namedSpanList);
        }
 
        _loader = new TestDocumentLoader(this, InitialText);
    }
 
    internal TestHostDocument(
        string text = "",
        string displayName = "",
        SourceCodeKind sourceCodeKind = SourceCodeKind.Regular,
        DocumentId? id = null,
        string? filePath = null,
        IReadOnlyList<string>? folders = null,
        ExportProvider? exportProvider = null,
        IDocumentServiceProvider? documentServiceProvider = null)
    {
        ExportProvider = exportProvider;
        _id = id;
        InitialText = text;
        Name = displayName;
        SourceCodeKind = sourceCodeKind;
        _loader = new TestDocumentLoader(this, text);
        FilePath = filePath;
        _folders = folders;
        _documentServiceProvider = documentServiceProvider;
    }
 
    public virtual void Open()
    {
    }
 
    internal void SetProject(AbstractTestHostProject project)
    {
        _project = project;
 
        // For generated documents, we need to fetch the IDs from the workspace later
        if (!IsSourceGenerated)
        {
            if (_id == null)
            {
                _id = DocumentId.CreateNewId(project.Id, this.Name);
            }
            else
            {
                Contract.ThrowIfFalse(project.Id == this.Id.ProjectId);
            }
        }
 
        LanguageServiceProvider ??= project.LanguageServiceProvider;
    }
 
    private sealed class TestDocumentLoader : TextLoader
    {
        private readonly TestHostDocument _hostDocument;
        private readonly string _text;
 
        internal TestDocumentLoader(TestHostDocument hostDocument, string text)
        {
            _hostDocument = hostDocument;
            _text = text;
        }
 
        internal override string? FilePath
            => _hostDocument.FilePath;
 
        public override Task<TextAndVersion> LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken)
            => Task.FromResult(TextAndVersion.Create(SourceText.From(_text, encoding: null, options.ChecksumAlgorithm), VersionStamp.Create(), _hostDocument.FilePath));
    }
 
    public TextLoader Loader => _loader;
 
    public IReadOnlyList<string> Folders
    {
        get
        {
            return _folders ?? [];
        }
    }
 
    public DocumentInfo ToDocumentInfo()
    {
        Contract.ThrowIfTrue(IsSourceGenerated, "We shouldn't be producing a DocumentInfo for a source generated document.");
        return DocumentInfo.Create(Id, Name, Folders, SourceCodeKind, Loader, FilePath, isGenerated: false)
            .WithDocumentServiceProvider(_documentServiceProvider);
    }
}