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 ?? ImmutableArray.Create<string>();
            }
        }
 
        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);
        }
    }
}