File: Workspace\Solution\Document.cs
Web Access
Project: ..\..\..\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// Represents a source code document that is part of a project.
    /// It provides access to the source text, parsed syntax tree and the corresponding semantic model.
    /// </summary>
    [DebuggerDisplay("{GetDebuggerDisplay(),nq}")]
    public class Document : TextDocument
    {
        /// <summary>
        /// A cached reference to the <see cref="SemanticModel"/>.
        /// </summary>
        private WeakReference<SemanticModel>? _model;
 
        /// <summary>
        /// A cached task that can be returned once the tree has already been created. This is only set if <see cref="SupportsSyntaxTree"/> returns true,
        /// so the inner value can be non-null.
        /// </summary>
        private Task<SyntaxTree>? _syntaxTreeResultTask;
 
        internal Document(Project project, DocumentState state)
            : base(project, state, TextDocumentKind.Document)
        {
        }
 
        internal DocumentState DocumentState => (DocumentState)State;
 
        /// <summary>
        /// The kind of source code this document contains.
        /// </summary>
        public SourceCodeKind SourceCodeKind => DocumentState.SourceCodeKind;
 
        /// <summary>
        /// True if the info of the document change (name, folders, file path; not the content)
        /// </summary>
        internal override bool HasInfoChanged(TextDocument otherTextDocument)
        {
            var otherDocument = otherTextDocument as Document ??
                throw new ArgumentException($"{nameof(otherTextDocument)} isn't a regular document.", nameof(otherTextDocument));
 
            return base.HasInfoChanged(otherDocument) ||
                   DocumentState.SourceCodeKind != otherDocument.SourceCodeKind;
        }
 
        [Obsolete("Use TextDocument.HasTextChanged")]
        internal bool HasTextChanged(Document otherDocument)
            => HasTextChanged(otherDocument, ignoreUnchangeableDocument: false);
 
        /// <summary>
        /// Get the current syntax tree for the document if the text is already loaded and the tree is already parsed.
        /// In almost all cases, you should call <see cref="GetSyntaxTreeAsync"/> to fetch the tree, which will parse the tree
        /// if it's not already parsed.
        /// </summary>
        public bool TryGetSyntaxTree([NotNullWhen(returnValue: true)] out SyntaxTree? syntaxTree)
        {
            // if we already have cache, use it
            if (_syntaxTreeResultTask != null)
            {
                syntaxTree = _syntaxTreeResultTask.Result;
                return true;
            }
 
            if (!DocumentState.TryGetSyntaxTree(out syntaxTree))
            {
                return false;
            }
 
            // cache the result if it is not already cached
            if (_syntaxTreeResultTask == null)
            {
                var result = Task.FromResult(syntaxTree);
                Interlocked.CompareExchange(ref _syntaxTreeResultTask, result, null);
            }
 
            return true;
        }
 
        /// <summary>
        /// Get the current syntax tree version for the document if the text is already loaded and the tree is already parsed.
        /// In almost all cases, you should call <see cref="GetSyntaxVersionAsync"/> to fetch the version, which will load the tree
        /// if it's not already available.
        /// </summary>
        public bool TryGetSyntaxVersion(out VersionStamp version)
        {
            version = default;
            if (!this.TryGetTextVersion(out var textVersion))
            {
                return false;
            }
 
            var projectVersion = this.Project.Version;
            version = textVersion.GetNewerVersion(projectVersion);
            return true;
        }
 
        /// <summary>
        /// Gets the version of the document's top level signature if it is already loaded and available.
        /// </summary>
        internal bool TryGetTopLevelChangeTextVersion(out VersionStamp version)
            => DocumentState.TryGetTopLevelChangeTextVersion(out version);
 
        /// <summary>
        /// Gets the version of the syntax tree. This is generally the newer of the text version and the project's version.
        /// </summary>
        public async Task<VersionStamp> GetSyntaxVersionAsync(CancellationToken cancellationToken = default)
        {
            var textVersion = await this.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
            var projectVersion = this.Project.Version;
            return textVersion.GetNewerVersion(projectVersion);
        }
 
        /// <summary>
        /// <see langword="true"/> if this Document supports providing data through the
        /// <see cref="GetSyntaxTreeAsync"/> and <see cref="GetSyntaxRootAsync"/> methods.
        ///
        /// If <see langword="false"/> then these methods will return <see langword="null"/> instead.
        /// </summary>
        public bool SupportsSyntaxTree => DocumentState.SupportsSyntaxTree;
 
        /// <summary>
        /// <see langword="true"/> if this Document supports providing data through the
        /// <see cref="GetSemanticModelAsync"/> method.
        ///
        /// If <see langword="false"/> then that method will return <see langword="null"/> instead.
        /// </summary>
        public bool SupportsSemanticModel
        {
            get
            {
                return this.SupportsSyntaxTree && this.Project.SupportsCompilation;
            }
        }
 
        /// <summary>
        /// Gets the <see cref="SyntaxTree" /> for this document asynchronously.
        /// </summary>
        /// <returns>
        /// The returned syntax tree can be <see langword="null"/> if the <see cref="SupportsSyntaxTree"/> returns <see
        /// langword="false"/>. This function may cause computation to occur the first time it is called, but will return
        /// a cached result every subsequent time.  <see cref="SyntaxTree"/>'s can hold onto their roots lazily. So calls 
        /// to <see cref="SyntaxTree.GetRoot"/> or <see cref="SyntaxTree.GetRootAsync"/> may end up causing computation
        /// to occur at that point.
        /// </returns>
        public Task<SyntaxTree?> GetSyntaxTreeAsync(CancellationToken cancellationToken = default)
        {
            // If the language doesn't support getting syntax trees for a document, then bail out immediately.
            if (!this.SupportsSyntaxTree)
            {
                return SpecializedTasks.Null<SyntaxTree>();
            }
 
            // if we have a cached result task use it
            if (_syntaxTreeResultTask != null)
            {
                // _syntaxTreeResultTask is a Task<SyntaxTree> so the ! operator here isn't suppressing a possible null ref, but rather allowing the
                // conversion from Task<SyntaxTree> to Task<SyntaxTree?> since Task itself isn't properly variant.
                return _syntaxTreeResultTask!;
            }
            // check to see if we already have the tree before actually going async
            if (TryGetSyntaxTree(out var tree))
            {
                // stash a completed result task for this value for the next request (to reduce extraneous allocations of tasks)
                // don't use the actual async task because it depends on a specific cancellation token
                // its okay to cache the task and hold onto the SyntaxTree, because the DocumentState already keeps the SyntaxTree alive.
                Interlocked.CompareExchange(ref _syntaxTreeResultTask, Task.FromResult(tree), null);
 
                // _syntaxTreeResultTask is a Task<SyntaxTree> so the ! operator here isn't suppressing a possible null ref, but rather allowing the
                // conversion from Task<SyntaxTree> to Task<SyntaxTree?> since Task itself isn't properly variant.
                return _syntaxTreeResultTask!;
            }
 
            // do it async for real.
            // GetSyntaxTreeAsync returns a Task<SyntaxTree> so the ! operator here isn't suppressing a possible null ref, but rather allowing the
            // conversion from Task<SyntaxTree> to Task<SyntaxTree?> since Task itself isn't properly variant.
            return DocumentState.GetSyntaxTreeAsync(cancellationToken).AsTask()!;
        }
 
        internal SyntaxTree? GetSyntaxTreeSynchronously(CancellationToken cancellationToken)
        {
            if (!this.SupportsSyntaxTree)
            {
                return null;
            }
 
            return DocumentState.GetSyntaxTree(cancellationToken);
        }
 
        /// <summary>
        /// Gets the root node of the current syntax tree if the syntax tree has already been parsed and the tree is still cached.
        /// In almost all cases, you should call <see cref="GetSyntaxRootAsync"/> to fetch the root node, which will parse
        /// the document if necessary.
        /// </summary>
        public bool TryGetSyntaxRoot([NotNullWhen(returnValue: true)] out SyntaxNode? root)
        {
            root = null;
            return this.TryGetSyntaxTree(out var tree) && tree.TryGetRoot(out root) && root != null;
        }
 
        /// <summary>
        /// Gets the root node of the syntax tree asynchronously.
        /// </summary>
        /// <returns>
        /// The returned <see cref="SyntaxNode"/> will be <see langword="null"/> if <see
        /// cref="SupportsSyntaxTree"/> returns <see langword="false"/>.  This function will return
        /// the same value if called multiple times.
        /// </returns>
        public async Task<SyntaxNode?> GetSyntaxRootAsync(CancellationToken cancellationToken = default)
        {
            if (!this.SupportsSyntaxTree)
            {
                return null;
            }
 
            var tree = (await this.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false))!;
            return await tree.GetRootAsync(cancellationToken).ConfigureAwait(false);
        }
 
        /// <summary>
        /// Only for features that absolutely must run synchronously (probably because they're
        /// on the UI thread).  Right now, the only feature this is for is Outlining as VS will
        /// block on that feature from the UI thread when a document is opened.
        /// </summary>
        internal SyntaxNode? GetSyntaxRootSynchronously(CancellationToken cancellationToken)
        {
            if (!this.SupportsSyntaxTree)
            {
                return null;
            }
 
            var tree = this.GetSyntaxTreeSynchronously(cancellationToken)!;
            return tree.GetRoot(cancellationToken);
        }
 
        /// <summary>
        /// Gets the current semantic model for this document if the model is already computed and still cached.
        /// In almost all cases, you should call <see cref="GetSemanticModelAsync"/>, which will compute the semantic model
        /// if necessary.
        /// </summary>
        public bool TryGetSemanticModel([NotNullWhen(returnValue: true)] out SemanticModel? semanticModel)
        {
            semanticModel = null;
            return _model != null && _model.TryGetTarget(out semanticModel);
        }
 
        /// <summary>
        /// Gets the semantic model for this document asynchronously.
        /// </summary>
        /// <returns>
        /// The returned <see cref="SemanticModel"/> may be <see langword="null"/> if <see
        /// cref="SupportsSemanticModel"/> returns <see langword="false"/>. This function will
        /// return the same value if called multiple times.
        /// </returns>
        public async Task<SemanticModel?> GetSemanticModelAsync(CancellationToken cancellationToken = default)
        {
            try
            {
                if (!this.SupportsSemanticModel)
                {
                    return null;
                }
 
                if (this.TryGetSemanticModel(out var semanticModel))
                {
                    return semanticModel;
                }
 
                var syntaxTree = await this.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                var compilation = (await this.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false))!;
 
                var result = compilation.GetSemanticModel(syntaxTree);
                Contract.ThrowIfNull(result);
 
                // first try set the cache if it has not been set
                var original = Interlocked.CompareExchange(ref _model, new WeakReference<SemanticModel>(result), null);
 
                // okay, it is first time.
                if (original == null)
                {
                    return result;
                }
 
                // It looks like someone has set it. Try to reuse same semantic model, or assign the new model if that
                // fails. The lock is required since there is no compare-and-set primitive for WeakReference<T>.
                lock (original)
                {
                    if (original.TryGetTarget(out semanticModel))
                    {
                        return semanticModel;
                    }
 
                    original.SetTarget(result);
                    return result;
                }
            }
            catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
            {
                throw ExceptionUtilities.Unreachable();
            }
        }
 
        /// <summary>
        /// Creates a new instance of this document updated to have the source code kind specified.
        /// </summary>
        public Document WithSourceCodeKind(SourceCodeKind kind)
            => this.Project.Solution.WithDocumentSourceCodeKind(this.Id, kind).GetDocument(this.Id)!;
 
        /// <summary>
        /// Creates a new instance of this document updated to have the text specified.
        /// </summary>
        public Document WithText(SourceText text)
            => this.Project.Solution.WithDocumentText(this.Id, text, PreservationMode.PreserveIdentity).GetDocument(this.Id)!;
 
        /// <summary>
        /// Creates a new instance of this document updated to have a syntax tree rooted by the specified syntax node.
        /// </summary>
        public Document WithSyntaxRoot(SyntaxNode root)
            => this.Project.Solution.WithDocumentSyntaxRoot(this.Id, root, PreservationMode.PreserveIdentity).GetDocument(this.Id)!;
 
        /// <summary>
        /// Creates a new instance of this document updated to have the specified name.
        /// </summary>
        public Document WithName(string name)
            => this.Project.Solution.WithDocumentName(this.Id, name).GetDocument(this.Id)!;
 
        /// <summary>
        /// Creates a new instance of this document updated to have the specified folders.
        /// </summary>
        public Document WithFolders(IEnumerable<string> folders)
            => this.Project.Solution.WithDocumentFolders(this.Id, folders).GetDocument(this.Id)!;
 
        /// <summary>
        /// Creates a new instance of this document updated to have the specified file path.
        /// </summary>
        /// <param name="filePath"></param>
        // TODO (https://github.com/dotnet/roslyn/issues/37125): Solution.WithDocumentFilePath will throw if
        // filePath is null, but it's odd because we *do* support null file paths. Why can't you switch a
        // document back to null?
        public Document WithFilePath(string filePath)
            => this.Project.Solution.WithDocumentFilePath(this.Id, filePath).GetDocument(this.Id)!;
 
        /// <summary>
        /// Get the text changes between this document and a prior version of the same document.
        /// The changes, when applied to the text of the old document, will produce the text of the current document.
        /// </summary>
        public async Task<IEnumerable<TextChange>> GetTextChangesAsync(Document oldDocument, CancellationToken cancellationToken = default)
        {
            try
            {
                using (Logger.LogBlock(FunctionId.Workspace_Document_GetTextChanges, this.Name, cancellationToken))
                {
                    if (oldDocument == this)
                    {
                        // no changes
                        return SpecializedCollections.EmptyEnumerable<TextChange>();
                    }
 
                    if (this.Id != oldDocument.Id)
                    {
                        throw new ArgumentException(WorkspacesResources.The_specified_document_is_not_a_version_of_this_document);
                    }
 
                    // first try to see if text already knows its changes
                    if (this.TryGetText(out var text) && oldDocument.TryGetText(out var oldText))
                    {
                        if (text == oldText)
                        {
                            return SpecializedCollections.EmptyEnumerable<TextChange>();
                        }
 
                        var container = text.Container;
                        if (container != null)
                        {
                            var textChanges = text.GetTextChanges(oldText).ToList();
 
                            // if changes are significant (not the whole document being replaced) then use these changes
                            if (textChanges.Count > 1 || (textChanges.Count == 1 && textChanges[0].Span != new TextSpan(0, oldText.Length)))
                            {
                                return textChanges;
                            }
                        }
                    }
 
                    // get changes by diffing the trees
                    if (this.SupportsSyntaxTree)
                    {
                        var tree = (await this.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false))!;
                        var oldTree = await oldDocument.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
 
                        RoslynDebug.Assert(oldTree is object);
                        return tree.GetChanges(oldTree);
                    }
 
                    text = await this.GetTextAsync(cancellationToken).ConfigureAwait(false);
                    oldText = await oldDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
 
                    return text.GetTextChanges(oldText).ToList();
                }
            }
            catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
            {
                throw ExceptionUtilities.Unreachable();
            }
        }
 
        /// <summary>
        /// Gets the list of <see cref="DocumentId"/>s that are linked to this
        /// <see cref="Document" />. <see cref="Document"/>s are considered to be linked if they
        /// share the same <see cref="TextDocument.FilePath" />. This <see cref="DocumentId"/> is excluded from the
        /// result.
        /// </summary>
        public ImmutableArray<DocumentId> GetLinkedDocumentIds()
        {
            var filteredDocumentIds = this.Project.Solution.GetRelatedDocumentIds(this.Id);
            return filteredDocumentIds.Remove(this.Id);
        }
 
        /// <summary>
        /// Creates a branched version of this document that has its semantic model frozen in whatever state it is available at the time,
        /// assuming a background process is constructing the semantics asynchronously. Repeated calls to this method may return
        /// documents with increasingly more complete semantics.
        ///
        /// Use this method to gain access to potentially incomplete semantics quickly.
        /// </summary>
        internal virtual Document WithFrozenPartialSemantics(CancellationToken cancellationToken)
        {
            var solution = this.Project.Solution;
 
            // only produce doc with frozen semantics if this workspace has support for that, as without
            // background compilation the semantics won't be moving toward completeness.  Also,
            // ensure that the project that this document is part of actually supports compilations,
            // as partial semantics don't make sense otherwise.
            if (solution.PartialSemanticsEnabled &&
                this.Project.SupportsCompilation)
            {
                var newSolution = this.Project.Solution.WithFrozenPartialCompilationIncludingSpecificDocument(this.Id, cancellationToken);
                return newSolution.GetDocument(this.Id)!;
            }
            else
            {
                return this;
            }
        }
 
        private string GetDebuggerDisplay()
            => this.Name;
 
#pragma warning disable RS0030 // Do not used banned APIs (backwards compat)
        private AsyncLazy<DocumentOptionSet>? _cachedOptions;
 
        /// <summary>
        /// Returns the options that should be applied to this document. This consists of global options from <see cref="Solution.Options"/>,
        /// merged with any settings the user has specified at the document levels.
        /// </summary>
        /// <remarks>
        /// This method is async because this may require reading other files. In files that are already open, this is expected to be cheap and complete synchronously.
        /// </remarks>
        [PerformanceSensitive("https://github.com/dotnet/roslyn/issues/23582", AllowCaptures = false)]
        public Task<DocumentOptionSet> GetOptionsAsync(CancellationToken cancellationToken = default)
        {
            if (_cachedOptions == null)
            {
                InitializeCachedOptions(Project.Solution.Options);
            }
 
            Contract.ThrowIfNull(_cachedOptions);
            return _cachedOptions.GetValueAsync(cancellationToken);
        }
 
        private void InitializeCachedOptions(OptionSet solutionOptions)
        {
            var newAsyncLazy = new AsyncLazy<DocumentOptionSet>(async cancellationToken =>
            {
                var options = await GetAnalyzerConfigOptionsAsync(cancellationToken).ConfigureAwait(false);
                return new DocumentOptionSet(options, solutionOptions, Project.Language);
            }, cacheResult: true);
 
            Interlocked.CompareExchange(ref _cachedOptions, newAsyncLazy, comparand: null);
        }
#pragma warning restore
 
        internal async ValueTask<StructuredAnalyzerConfigOptions> GetAnalyzerConfigOptionsAsync(CancellationToken cancellationToken)
        {
            var provider = (ProjectState.ProjectAnalyzerConfigOptionsProvider)Project.State.AnalyzerOptions.AnalyzerConfigOptionsProvider;
            return await provider.GetOptionsAsync(DocumentState, cancellationToken).ConfigureAwait(false);
        }
    }
}