File: Handler\RelatedDocuments\RelatedDocumentsHandler.cs
Web Access
Project: src\src\LanguageServer\Protocol\Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol)
// 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.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.RelatedDocuments;
using Microsoft.CodeAnalysis.Serialization;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.RelatedDocuments;
 
[ExportCSharpVisualBasicLspServiceFactory(typeof(RelatedDocumentsHandler)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class RelatedDocumentsHandlerFactory() : ILspServiceFactory
{
    public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind)
        => new RelatedDocumentsHandler();
}
 
[Method(VSInternalMethods.CopilotRelatedDocumentsName)]
internal sealed class RelatedDocumentsHandler
    : ILspServiceRequestHandler<VSInternalRelatedDocumentParams, VSInternalRelatedDocumentReport[]?>,
      ITextDocumentIdentifierHandler<VSInternalRelatedDocumentParams, TextDocumentIdentifier>
{
    /// <summary>
    /// Cache where we store the data produced by prior requests so that they can be returned if nothing of significance
    /// changed. The version key is produced by combining the checksums for project options <see
    /// cref="ProjectState.GetParseOptionsChecksum"/> and <see cref="DocumentStateChecksums.Text"/>
    /// </summary>
    private readonly VersionedPullCache<(Checksum parseOptionsChecksum, Checksum textChecksum)?> _versionedCache = new(nameof(RelatedDocumentsHandler));
 
    public bool MutatesSolutionState => false;
    public bool RequiresLSPSolution => true;
 
    private static async Task<(Checksum parseOptionsChecksum, Checksum textChecksum)> ComputeChecksumsAsync(Document document, CancellationToken cancellationToken)
    {
        var project = document.Project;
        var parseOptionsChecksum = project.State.GetParseOptionsChecksum();
 
        var documentChecksumState = await document.State.GetStateChecksumsAsync(cancellationToken).ConfigureAwait(false);
        var textChecksum = documentChecksumState.Text;
 
        return (parseOptionsChecksum, textChecksum);
    }
 
    public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalRelatedDocumentParams requestParams)
        => requestParams.TextDocument;
 
    /// <summary>
    /// Retrieve the previous results we reported.  Used so we can avoid resending data for unchanged files.
    /// </summary>
    private static ImmutableArray<PreviousPullResult>? GetPreviousResults(VSInternalRelatedDocumentParams requestParams)
        => requestParams.PreviousResultId != null && requestParams.TextDocument != null
            ? [new PreviousPullResult(requestParams.PreviousResultId, requestParams.TextDocument)]
            // The client didn't provide us with a previous result to look for, so we can't lookup anything.
            : null;
 
    public async Task<VSInternalRelatedDocumentReport[]?> HandleRequestAsync(
        VSInternalRelatedDocumentParams requestParams, RequestContext context, CancellationToken cancellationToken)
    {
        context.TraceInformation($"{this.GetType()} started getting related documents");
        context.TraceInformation($"PreviousResultId={requestParams.PreviousResultId}");
 
        var solution = context.Solution;
        var document = context.Document;
        Contract.ThrowIfNull(solution);
        Contract.ThrowIfNull(document);
 
        context.TraceInformation($"Processing: {document.FilePath}");
 
        var relatedDocumentsService = document.GetLanguageService<IRelatedDocumentsService>();
        if (relatedDocumentsService == null)
        {
            context.TraceInformation($"Ignoring document '{document.FilePath}' because it does not support related documents");
            return [];
        }
 
        // The progress object we will stream reports to.
        using var progress = BufferedProgress.Create(requestParams.PartialResultToken);
 
        var documentToPreviousParams = new Dictionary<Document, PreviousPullResult>();
        if (requestParams.PreviousResultId != null)
            documentToPreviousParams.Add(document, new PreviousPullResult(requestParams.PreviousResultId, requestParams.TextDocument));
 
        var newResultId = await _versionedCache.GetNewResultIdAsync(
            documentToPreviousParams,
            document,
            computeVersionAsync: async () => await ComputeChecksumsAsync(document, cancellationToken).ConfigureAwait(false),
            cancellationToken).ConfigureAwait(false);
        if (newResultId != null)
        {
            context.TraceInformation($"Version was changed for document: {document.FilePath}");
 
            var linePosition = requestParams.Position is null
                ? new LinePosition(0, 0)
                : ProtocolConversions.PositionToLinePosition(requestParams.Position);
 
            var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
            var position = text.Lines.GetPosition(linePosition);
 
            await relatedDocumentsService.GetRelatedDocumentIdsAsync(
                document,
                position,
                (relatedDocumentIds, cancellationToken) =>
                {
                    // As the related docs services reports document ids to us, stream those immediately through our
                    // progress reporter.
                    progress.Report(new VSInternalRelatedDocumentReport
                    {
                        ResultId = newResultId,
                        FilePaths = relatedDocumentIds.Select(id => solution.GetRequiredDocument(id).FilePath).WhereNotNull().ToArray(),
                    });
 
                    return ValueTaskFactory.CompletedTask;
                },
                cancellationToken).ConfigureAwait(false);
        }
        else
        {
            context.TraceInformation($"Version was unchanged for document: {document.FilePath}");
 
            // Nothing changed between the last request and this one.  Report a (null-file-paths, same-result-id)
            // response to the client as that means they should just preserve the current related file paths they
            // have for this file.
            progress.Report(new VSInternalRelatedDocumentReport { ResultId = requestParams.PreviousResultId });
        }
 
        // If we had a progress object, then we will have been reporting to that.  Otherwise, take what we've been
        // collecting and return that.
        context.TraceInformation($"{this.GetType()} finished getting related documents");
        return progress.GetFlattenedValues();
    }
}