File: LanguageClient\Cohost\HtmlDocumentPublisher.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.VisualStudio.LanguageServices.Razor\Microsoft.VisualStudio.LanguageServices.Razor.csproj (Microsoft.VisualStudio.LanguageServices.Razor)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.TextDifferencing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
using Microsoft.VisualStudio.Threading;
 
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
 
[Export(typeof(IHtmlDocumentPublisher))]
[method: ImportingConstructor]
internal sealed class HtmlDocumentPublisher(
    LSPDocumentManager documentManager,
    JoinableTaskContext joinableTaskContext,
    ILoggerFactory loggerFactory) : IHtmlDocumentPublisher
{
    private readonly JoinableTaskContext _joinableTaskContext = joinableTaskContext;
    private readonly TrackingLSPDocumentManager _documentManager = documentManager as TrackingLSPDocumentManager ?? throw new InvalidOperationException("Expected TrackingLSPDocumentManager");
    private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<HtmlDocumentPublisher>();
 
    public async Task<bool> TryPublishAsync(TextDocument document, ChecksumWrapper checksum, string htmlText, CancellationToken cancellationToken)
    {
        var uri = document.CreateUri();
        if (!_documentManager.TryGetDocument(uri, out var documentSnapshot) ||
            !documentSnapshot.TryGetVirtualDocument<HtmlVirtualDocumentSnapshot>(out var htmlDocument))
        {
            _logger.LogDebug($"No {(documentSnapshot is null ? "" : "virtual")} document snapshot for {document.FilePath}.");
            return false;
        }
 
        if (cancellationToken.IsCancellationRequested)
        {
            return false;
        }
 
        _logger.LogDebug($"The html document for {document.FilePath} is {htmlDocument.Uri}");
 
        await _joinableTaskContext.Factory.SwitchToMainThreadAsync(cancellationToken);
 
        if (cancellationToken.IsCancellationRequested)
        {
            return false;
        }
 
        // We have a string for the Html text here, so its tempting to just overwrite the whole buffer, but that causes issues with
        // the editors span tracking, which is used by the Html server to map diagnostic ranges. Updating with minimal changes helps
        // the editor understand what is actually happening to the virtual buffer.
        var currentHtmlSourceText = htmlDocument.Snapshot.AsText();
        var newHtmlSourceText = SourceText.From(htmlText, currentHtmlSourceText.Encoding, currentHtmlSourceText.ChecksumAlgorithm);
        var textChanges = SourceTextDiffer.GetMinimalTextChanges(currentHtmlSourceText, newHtmlSourceText);
        var changes = textChanges.SelectAsArray(c => new VisualStudioTextChange(c.Span.Start, c.Span.Length, c.NewText.AssumeNotNull()));
        _documentManager.UpdateVirtualDocument<HtmlVirtualDocument>(uri, changes, documentSnapshot.Version, state: checksum);
 
        _logger.LogDebug($"Finished Html document generation for {document.FilePath} (into {htmlDocument.Uri})");
 
        return true;
    }
}