File: Workspace\Solution\TextLoader.cs
Web Access
Project: src\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.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
/// <summary>
/// A class that represents access to a source text and its version from a storage location.
/// </summary>
public abstract class TextLoader
{
    private static readonly ConditionalWeakTable<Type, StrongBox<bool>> s_isObsoleteLoadTextAndVersionAsyncOverriden = new();
 
    private const double MaxDelaySecs = 1.0;
    private const int MaxRetries = 5;
    internal static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(MaxDelaySecs / MaxRetries);
 
    internal virtual string? FilePath => null;
 
    /// <summary>
    /// <see langword="true"/> if the document that holds onto this loader should do so with a strong reference, versus
    /// a reference that will take the contents of this loader and store them in a recoverable form (e.g. a memory
    /// mapped file within the <em>same</em> process).  This should be used when the underlying data is already stored
    /// in a recoverable form somewhere else and it would be wasteful to store another copy.  For example, a document
    /// that is backed by memory-mapped contents in <em>another</em> process does not need to dump it's content to
    /// another memory-mapped file in the process it lives in.  It can always recover the text from the original
    /// process.
    /// </summary>
    internal virtual bool AlwaysHoldStrongly
        => false;
 
    /// <summary>
    /// True if <see cref="LoadTextAndVersionAsync(LoadTextOptions, CancellationToken)"/> reloads <see cref="SourceText"/> from its original binary representation (e.g. file on disk).
    /// </summary>
    internal virtual bool CanReloadText
        => false;
 
    /// <summary>
    /// Load a text and a version of the document.
    /// </summary>
    /// <param name="options">
    /// Implementations of this method should use <see cref="LoadTextOptions.ChecksumAlgorithm"/> when creating <see cref="SourceText"/> from an original binary representation and
    /// ignore it otherwise.
    /// Callers of this method should pass <see cref="LoadTextOptions"/> specifying the desired properties of <see cref="SourceText"/>. The implementation may return a <see cref="SourceText"/>
    /// that does not satisfy the given requirements. For example, legacy types that do not override this method would ignore all <paramref name="options"/>.
    /// </param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <exception cref="IOException" />
    /// <exception cref="InvalidDataException"/>
    /// <exception cref="OperationCanceledException"/>
    public virtual Task<TextAndVersion> LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken)
    {
#pragma warning disable CS0618 // Type or member is obsolete
        if (s_isObsoleteLoadTextAndVersionAsyncOverriden.GetValue(
            GetType(),
            _ => new StrongBox<bool>(new Func<Workspace, DocumentId, CancellationToken, Task<TextAndVersion>>(LoadTextAndVersionAsync).Method.DeclaringType != typeof(TextLoader))).Value)
        {
            return LoadTextAndVersionAsync(workspace: null, documentId: null, cancellationToken);
        }
#pragma warning restore
 
        throw new NotImplementedException($"{GetType()} must override {nameof(LoadTextAndVersionAsync)}");
    }
 
    /// <summary>
    /// Load a text and a version of the document.
    /// </summary>
    /// <param name="workspace">Obsolete. Null.</param>
    /// <param name="documentId">Obsolete. Null.</param>
    /// <exception cref="IOException" />
    /// <exception cref="InvalidDataException"/>
    /// <exception cref="OperationCanceledException"/>
    [Obsolete("Use/override LoadTextAndVersionAsync(LoadTextOptions, CancellationToken)")]
    public virtual Task<TextAndVersion> LoadTextAndVersionAsync(Workspace? workspace, DocumentId? documentId, CancellationToken cancellationToken)
        => LoadTextAndVersionAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken);
 
    /// <summary>
    /// Load a text and a version of the document in the workspace.
    /// </summary>
    /// <exception cref="IOException" />
    /// <exception cref="InvalidDataException"/>
    /// <exception cref="OperationCanceledException"/>
    internal virtual TextAndVersion LoadTextAndVersionSynchronously(LoadTextOptions options, CancellationToken cancellationToken)
    {
        // this implementation exists in case a custom derived type does not have access to internals
        return LoadTextAndVersionAsync(options, cancellationToken).WaitAndGetResult_CanCallOnBackground(cancellationToken);
    }
 
    internal async Task<TextAndVersion> LoadTextAsync(LoadTextOptions options, CancellationToken cancellationToken)
    {
        var retries = 0;
 
        while (true)
        {
            try
            {
                return await LoadTextAndVersionAsync(options, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
            }
            catch (IOException e)
            {
                if (++retries > MaxRetries)
                {
                    return CreateFailedText(e.Message);
                }
 
                // fall out to try again
            }
            catch (InvalidDataException e)
            {
                return CreateFailedText(e.Message);
            }
 
            // try again after a delay
            await Task.Delay(RetryDelay, cancellationToken).ConfigureAwait(false);
        }
    }
 
    internal TextAndVersion LoadTextSynchronously(LoadTextOptions options, CancellationToken cancellationToken)
    {
        var retries = 0;
 
        while (true)
        {
            try
            {
                return LoadTextAndVersionSynchronously(options, cancellationToken);
            }
            catch (IOException e)
            {
                if (++retries > MaxRetries)
                {
                    return CreateFailedText(e.Message);
                }
 
                // fall out to try again
            }
            catch (InvalidDataException e)
            {
                return CreateFailedText(e.Message);
            }
 
            cancellationToken.ThrowIfCancellationRequested();
 
            // try again after a delay
            Thread.Sleep(RetryDelay);
        }
    }
 
    private TextAndVersion CreateFailedText(string message)
    {
        Location location;
        string display;
 
        var filePath = FilePath;
 
        if (filePath == null)
        {
            location = Location.None;
            display = "<no path>";
        }
        else
        {
            location = Location.Create(filePath, textSpan: default, lineSpan: default);
            display = filePath;
        }
 
        return TextAndVersion.Create(
            SourceText.From(string.Empty, Encoding.UTF8),
            VersionStamp.Default,
            Diagnostic.Create(WorkspaceDiagnosticDescriptors.ErrorReadingFileContent, location, new[] { display, message }));
    }
 
    /// <summary>
    /// Creates a new <see cref="TextLoader"/> from an already existing source text and version.
    /// </summary>
    public static TextLoader From(TextAndVersion textAndVersion)
    {
        if (textAndVersion == null)
        {
            throw new ArgumentNullException(nameof(textAndVersion));
        }
 
        return new TextDocumentLoader(textAndVersion);
    }
 
    /// <summary>
    /// Creates a <see cref="TextLoader"/> from a <see cref="SourceTextContainer"/> and version. 
    /// 
    /// The text obtained from the loader will be the current text of the container at the time
    /// the loader is accessed.
    /// </summary>
    public static TextLoader From(SourceTextContainer container, VersionStamp version, string? filePath = null)
    {
        if (container == null)
        {
            throw new ArgumentNullException(nameof(container));
        }
 
        return new TextContainerLoader(container, version, filePath);
    }
 
    private sealed class TextDocumentLoader : TextLoader
    {
        private readonly TextAndVersion _textAndVersion;
 
        internal TextDocumentLoader(TextAndVersion textAndVersion)
            => _textAndVersion = textAndVersion;
 
        public override Task<TextAndVersion> LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken)
            => Task.FromResult(_textAndVersion);
 
        internal override TextAndVersion LoadTextAndVersionSynchronously(LoadTextOptions options, CancellationToken cancellationToken)
            => _textAndVersion;
    }
 
    private sealed class TextContainerLoader : TextLoader
    {
        private readonly SourceTextContainer _container;
        private readonly VersionStamp _version;
        private readonly string? _filePath;
 
        internal TextContainerLoader(SourceTextContainer container, VersionStamp version, string? filePath)
        {
            _container = container;
            _version = version;
            _filePath = filePath;
        }
 
        internal override string? FilePath
            => _filePath;
 
        public override Task<TextAndVersion> LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken)
            => Task.FromResult(LoadTextAndVersionSynchronously(options, cancellationToken));
 
        internal override TextAndVersion LoadTextAndVersionSynchronously(LoadTextOptions options, CancellationToken cancellationToken)
            => TextAndVersion.Create(_container.CurrentText, _version, _filePath);
    }
}