File: Workspace\Solution\VersionSource\LoadableTextAndVersionSource.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.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
internal sealed class LoadableTextAndVersionSource(TextLoader loader, bool cacheResult) : ITextAndVersionSource
{
    private sealed class LazyValueWithOptions(LoadableTextAndVersionSource source, LoadTextOptions options)
    {
        public readonly LoadableTextAndVersionSource Source = source;
        public readonly LoadTextOptions Options = options;
 
        private readonly SemaphoreSlim _gate = new(initialCount: 1);
 
        /// <summary>
        /// Strong reference to the loaded text and version.  Only held onto once computed if <see cref="Source"/>.<see
        /// cref="CacheResult"/> is <see langword="true"/>.  Once held onto, this will be returned from all calls to
        /// <see cref="TryGetValue"/>, <see cref="GetValue"/> or <see cref="GetValueAsync"/>.  Once non-null will always
        /// remain non-null.
        /// </summary>
        private TextAndVersion? _instance;
 
        /// <summary>
        /// Weak reference to the loaded text and version that we create whenever the value is computed.  We will
        /// attempt to return from this if still alive when clients call back into this.  If neither this, nor <see
        /// cref="_instance"/> are available, the value will be reloaded.  Once non-null, this will always be non-null.
        /// </summary>
        private WeakReference<TextAndVersion>? _weakInstance;
 
        private Task<TextAndVersion> LoadAsync(CancellationToken cancellationToken)
            => Source.TextLoader.LoadTextAsync(Options, cancellationToken);
 
        private TextAndVersion LoadSynchronously(CancellationToken cancellationToken)
            => Source.TextLoader.LoadTextSynchronously(Options, cancellationToken);
 
        public bool TryGetValue([MaybeNullWhen(false)] out TextAndVersion value)
        {
            value = _instance;
            if (value != null)
                return true;
 
            return _weakInstance?.TryGetTarget(out value) == true && value != null;
        }
 
        public TextAndVersion GetValue(CancellationToken cancellationToken)
        {
            if (!TryGetValue(out var textAndVersion))
            {
                using (_gate.DisposableWait(cancellationToken))
                {
                    if (!TryGetValue(out textAndVersion))
                    {
                        textAndVersion = LoadSynchronously(cancellationToken);
                        UpdateWeakAndStrongReferences_NoLock(textAndVersion);
                    }
                }
            }
 
            return textAndVersion;
        }
 
        public async Task<TextAndVersion> GetValueAsync(CancellationToken cancellationToken)
        {
            if (!TryGetValue(out var textAndVersion))
            {
                using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
                {
                    if (!TryGetValue(out textAndVersion))
                    {
                        textAndVersion = await LoadAsync(cancellationToken).ConfigureAwait(false);
                        UpdateWeakAndStrongReferences_NoLock(textAndVersion);
                    }
                }
            }
 
            return textAndVersion;
        }
 
        private void UpdateWeakAndStrongReferences_NoLock(TextAndVersion textAndVersion)
        {
            Contract.ThrowIfTrue(_gate.CurrentCount != 0);
 
            if (this.Source.CacheResult)
            {
                // if our source wants us to hold onto the value strongly, do so.  No need to involve the weak-refs as
                // this will now hold onto the value forever.
                _instance = textAndVersion;
            }
            else
            {
                // Update the weak ref, so we can return the same instance if anything else is holding onto it.
                _weakInstance ??= new WeakReference<TextAndVersion>(textAndVersion);
                _weakInstance.SetTarget(textAndVersion);
            }
        }
    }
 
    public TextLoader TextLoader { get; } = loader;
    public readonly bool CacheResult = cacheResult;
 
    private LazyValueWithOptions? _lazyValue;
 
    public bool CanReloadText
        => TextLoader.CanReloadText;
 
    private LazyValueWithOptions GetLazyValue(LoadTextOptions options)
    {
        var lazy = _lazyValue;
 
        if (lazy == null || lazy.Options != options)
        {
            // drop previous value and replace it with the one that has current options:
            _lazyValue = lazy = new LazyValueWithOptions(this, options);
        }
 
        return lazy;
    }
 
    public TextAndVersion GetValue(LoadTextOptions options, CancellationToken cancellationToken)
        => GetLazyValue(options).GetValue(cancellationToken);
 
    public bool TryGetValue(LoadTextOptions options, [MaybeNullWhen(false)] out TextAndVersion value)
        => GetLazyValue(options).TryGetValue(out value);
 
    public Task<TextAndVersion> GetValueAsync(LoadTextOptions options, CancellationToken cancellationToken)
        => GetLazyValue(options).GetValueAsync(cancellationToken);
 
    public bool TryGetVersion(LoadTextOptions options, out VersionStamp version)
    {
        if (!TryGetValue(options, out var value))
        {
            version = default;
            return false;
        }
 
        version = value.Version;
        return true;
    }
 
    public async ValueTask<VersionStamp> GetVersionAsync(LoadTextOptions options, CancellationToken cancellationToken)
    {
        var value = await GetValueAsync(options, cancellationToken).ConfigureAwait(false);
        return value.Version;
    }
}