File: Venus\ContainedLanguage.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_4r0c5m1y_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Runtime.InteropServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.TextManager.Interop;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Venus;
 
internal partial class ContainedLanguage
{
    private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactoryService;
    private readonly IDiagnosticAnalyzerService _diagnosticAnalyzerService;
    private readonly Guid _languageServiceGuid;
 
    protected readonly Workspace Workspace;
    protected readonly IComponentModel ComponentModel;
 
    public ProjectSystemProject? Project { get; }
 
    protected readonly ContainedDocument ContainedDocument;
 
    public IVsTextBufferCoordinator BufferCoordinator { get; protected set; }
 
    /// <summary>
    /// The subject (secondary) buffer that contains the C# or VB code.
    /// </summary>
    public ITextBuffer SubjectBuffer { get; }
 
    /// <summary>
    /// The underlying buffer that contains C# or VB code. NOTE: This is NOT the "document" buffer
    /// that is saved to disk.  Instead it is the view that the user sees.  The normal buffer graph
    /// in Venus includes 4 buffers:
    /// <code>
    ///            SurfaceBuffer/Databuffer (projection)
    ///             /                               |
    /// Subject Buffer (C#/VB projection)           |
    ///             |                               |
    /// Inert (generated) C#/VB Buffer         Document (aspx) buffer
    /// </code>
    /// In normal circumstance, the Subject and Inert C# buffer are identical in content, and the
    /// Surface and Document are also identical.  The Subject Buffer is the one that is part of the
    /// workspace, that most language operations deal with.  The surface buffer is the one that the
    /// view is created over, and the Document buffer is the one that is saved to disk.
    /// </summary>
    public ITextBuffer DataBuffer { get; }
 
    // Set when a TextViewFilter is set.  We hold onto this to keep our TagSource objects alive even if Venus
    // disconnects the subject buffer from the view temporarily (which they do frequently).  Otherwise, we have to
    // re-compute all of the tag data when they re-connect it, and this causes issues like classification
    // flickering.
    private readonly ITagAggregator<ITag> _bufferTagAggregator;
 
    internal ContainedLanguage(
        IVsTextBufferCoordinator bufferCoordinator,
        IComponentModel componentModel,
        Workspace workspace,
        ProjectId projectId,
        ProjectSystemProject? project,
        Guid languageServiceGuid,
        AbstractFormattingRule? vbHelperFormattingRule = null)
    {
        BufferCoordinator = bufferCoordinator;
        ComponentModel = componentModel;
        Project = project;
        _languageServiceGuid = languageServiceGuid;
 
        Workspace = workspace;
 
        _editorAdaptersFactoryService = componentModel.GetService<IVsEditorAdaptersFactoryService>();
        _diagnosticAnalyzerService = componentModel.GetService<IDiagnosticAnalyzerService>();
 
        // Get the ITextBuffer for the secondary buffer
        Marshal.ThrowExceptionForHR(bufferCoordinator.GetSecondaryBuffer(out var secondaryTextLines));
        SubjectBuffer = _editorAdaptersFactoryService.GetDocumentBuffer(secondaryTextLines)!;
 
        // Get the ITextBuffer for the primary buffer
        Marshal.ThrowExceptionForHR(bufferCoordinator.GetPrimaryBuffer(out var primaryTextLines));
        DataBuffer = _editorAdaptersFactoryService.GetDataBuffer(primaryTextLines)!;
 
        // Create our tagger
        var bufferTagAggregatorFactory = ComponentModel.GetService<IBufferTagAggregatorFactoryService>();
        _bufferTagAggregator = bufferTagAggregatorFactory.CreateTagAggregator<ITag>(SubjectBuffer);
 
        var filePath = GetFilePathFromBuffers();
        DocumentId documentId;
 
        if (this.Project != null)
        {
            documentId = this.Project.AddSourceTextContainer(
                SubjectBuffer.AsTextContainer(),
                filePath,
                sourceCodeKind: SourceCodeKind.Regular,
                folders: default,
                designTimeOnly: true,
                documentServiceProvider: new ContainedDocument.DocumentServiceProvider(DataBuffer));
        }
        else
        {
            documentId = DocumentId.CreateNewId(projectId, $"{nameof(ContainedDocument)}: {filePath}");
 
            // We must jam a document into an existing workspace, which we'll assume is safe to do with OnDocumentAdded
            Workspace.OnDocumentAdded(DocumentInfo.Create(
                documentId,
                name: filePath,
                loader: null,
                filePath: filePath));
 
            Workspace.OnDocumentOpened(documentId, SubjectBuffer.AsTextContainer());
        }
 
        ContainedDocument = new ContainedDocument(
            documentId,
            subjectBuffer: SubjectBuffer,
            dataBuffer: DataBuffer,
            BufferCoordinator,
            Workspace,
            Project,
            ComponentModel,
            vbHelperFormattingRule);
 
        // link subject buffer back to the ContainedDocument, so that we can find it given just the buffer:
        SubjectBuffer.Properties.AddProperty(typeof(IContainedDocument), ContainedDocument);
 
        // TODO: Can contained documents be linked or shared?
        this.DataBuffer.Changed += OnDataBufferChanged;
    }
 
    public IGlobalOptionService GlobalOptions => _diagnosticAnalyzerService.GlobalOptions;
 
    private void OnDisconnect()
    {
        this.DataBuffer.Changed -= OnDataBufferChanged;
 
        if (this.Project != null)
        {
            this.Project.RemoveSourceTextContainer(SubjectBuffer.AsTextContainer());
        }
        else
        {
            // It's possible the host of the workspace might have already removed the entire project
            if (Workspace.CurrentSolution.ContainsDocument(ContainedDocument.Id))
            {
                Workspace.OnDocumentRemoved(ContainedDocument.Id);
            }
        }
 
        this.ContainedDocument.Dispose();
 
        _bufferTagAggregator?.Dispose();
    }
 
    private void OnDataBufferChanged(object sender, TextContentChangedEventArgs e)
    {
        // we don't actually care what has changed in primary buffer. we just want to re-analyze secondary buffer
        // when primary buffer has changed to update diagnostic positions.
        _diagnosticAnalyzerService.RequestDiagnosticRefresh();
    }
 
    public string GetFilePathFromBuffers()
    {
        var textDocumentFactoryService = ComponentModel.GetService<ITextDocumentFactoryService>();
 
        // Try to get the file path from the secondary buffer
        if (!textDocumentFactoryService.TryGetTextDocument(SubjectBuffer, out var document))
        {
            // Fallback to the primary buffer
            textDocumentFactoryService.TryGetTextDocument(DataBuffer, out document);
        }
 
        if (document == null)
        {
            FatalError.ReportAndPropagate(new InvalidOperationException("Failed to get an ITextDocument for a contained document"));
        }
 
        return document?.FilePath ?? string.Empty;
    }
}