File: Implementation\XamlProjectService.cs
Web Access
Project: src\src\VisualStudio\Xaml\Impl\Microsoft.VisualStudio.LanguageServices.Xaml.csproj (Microsoft.VisualStudio.LanguageServices.Xaml)
// 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.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Xaml;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
using Microsoft.CodeAnalysis.Xaml.Diagnostics.Analyzers;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.TextManager.Interop;
 
namespace Microsoft.VisualStudio.LanguageServices.Xaml;
 
[Export]
internal sealed partial class XamlProjectService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly Workspace _workspace;
    private readonly VisualStudioProjectFactory _visualStudioProjectFactory;
    private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactory;
    private readonly IThreadingContext _threadingContext;
    private readonly Dictionary<IVsHierarchy, ProjectSystemProject> _xamlProjects = [];
    private readonly ConcurrentDictionary<string, DocumentId> _documentIds = new ConcurrentDictionary<string, DocumentId>(StringComparer.OrdinalIgnoreCase);
 
    private RunningDocumentTable? _rdt;
    private IVsSolution? _vsSolution;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public XamlProjectService(
        [Import(typeof(Shell.SVsServiceProvider))] IServiceProvider serviceProvider,
        IVsEditorAdaptersFactoryService editorAdaptersFactoryService,
        VisualStudioProjectFactory visualStudioProjectFactory,
        VisualStudioWorkspace workspace,
        IXamlDocumentAnalyzerService analyzerService,
        IThreadingContext threadingContext)
    {
        _serviceProvider = serviceProvider;
        _editorAdaptersFactory = editorAdaptersFactoryService;
        _visualStudioProjectFactory = visualStudioProjectFactory;
        _workspace = workspace;
        _threadingContext = threadingContext;
 
        AnalyzerService = analyzerService;
 
        _workspace.DocumentClosed += OnDocumentClosed;
    }
 
    public static IXamlDocumentAnalyzerService? AnalyzerService { get; private set; }
 
    public DocumentId? TrackOpenDocument(string filePath)
    {
        if (string.IsNullOrEmpty(filePath))
        {
            // Can't track anything without a path (can happen while diffing)
            return null;
        }
 
        if (_documentIds.TryGetValue(filePath, out var documentId))
        {
            return documentId;
        }
 
        documentId = GetDocumentId(filePath);
        if (documentId != null)
        {
            _documentIds.TryAdd(filePath, documentId);
        }
 
        return documentId;
 
        DocumentId? GetDocumentId(string path)
        {
            if (_threadingContext.JoinableTaskContext.IsOnMainThread)
            {
                return EnsureDocument(filePath);
            }
            else
            {
                return _threadingContext.JoinableTaskFactory.Run(async () =>
                {
                    await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
 
                    return EnsureDocument(filePath);
                });
            }
        }
    }
 
    private DocumentId? EnsureDocument(string filePath)
    {
        if (_rdt == null)
        {
            _rdt = new RunningDocumentTable(_serviceProvider);
            _rdt.Advise(this);
        }
 
        if (_vsSolution == null)
        {
            _vsSolution = (IVsSolution)_serviceProvider.GetService(typeof(SVsSolution));
            _vsSolution.AdviseSolutionEvents(this, out _);
        }
 
        IVsHierarchy? hierarchy = null;
        uint docCookie = 0;
 
        try
        {
            _rdt.FindDocument(filePath, out hierarchy, out _, out docCookie);
        }
        catch (ArgumentException)
        {
            // We only support open documents that are in the RDT already
            return null;
        }
 
        if (hierarchy == null || docCookie == 0)
        {
            return null;
        }
 
        if (!_xamlProjects.TryGetValue(hierarchy, out var project))
        {
            if (!hierarchy.TryGetName(out var name))
            {
                return null;
            }
 
            if (!hierarchy.TryGetGuidProperty(__VSHPROPID.VSHPROPID_ProjectIDGuid, out var projectGuid))
            {
                return null;
            }
 
            var projectInfo = new VisualStudioProjectCreationInfo
            {
                Hierarchy = hierarchy,
                FilePath = hierarchy.TryGetProjectFilePath(),
                ProjectGuid = projectGuid
            };
 
            project = _threadingContext.JoinableTaskFactory.Run(() => _visualStudioProjectFactory.CreateAndAddToWorkspaceAsync(
                name, StringConstants.XamlLanguageName, projectInfo, CancellationToken.None));
            _xamlProjects.Add(hierarchy, project);
        }
 
        if (!project.ContainsSourceFile(filePath))
        {
            project.AddSourceFile(filePath);
 
            var documentId = _workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Single(d => d.ProjectId == project.Id);
            _documentIds[filePath] = documentId;
 
            // Remove the following when https://github.com/dotnet/roslyn/issues/49879 is fixed
            var document = _workspace.CurrentSolution.GetRequiredDocument(documentId);
            var hasText = document.TryGetText(out var text);
            if (!hasText || text?.Container.TryGetTextBuffer() == null)
            {
                var docInfo = _rdt.GetDocumentInfo(docCookie);
                var textBuffer = TryGetTextBufferFromDocData(docInfo.DocData);
                var textContainer = textBuffer?.AsTextContainer();
                if (textContainer != null)
                {
                    _workspace.OnDocumentTextChanged(documentId, textContainer.CurrentText, PreservationMode.PreserveIdentity);
                }
            }
        }
 
        if (_documentIds.TryGetValue(filePath, out var docId))
        {
            return docId;
        }
 
        return null;
    }
 
    private void OnDocumentClosed(object sender, DocumentEventArgs e)
    {
        var filePath = e.Document.FilePath;
        if (filePath == null)
        {
            return;
        }
 
        if (_documentIds.TryGetValue(filePath, out var documentId))
        {
            var document = _workspace.CurrentSolution.GetDocument(documentId);
            if (document?.FilePath != null)
            {
                var project = _xamlProjects.Values.SingleOrDefault(p => p.Id == document.Project.Id);
                project?.RemoveSourceFile(document.FilePath);
            }
 
            _documentIds.TryRemove(filePath, out _);
        }
    }
 
    private void OnProjectClosing(IVsHierarchy hierarchy)
    {
        if (_xamlProjects.TryGetValue(hierarchy, out var project))
        {
            project.RemoveFromWorkspace();
            _xamlProjects.Remove(hierarchy);
        }
    }
 
    private void OnDocumentMonikerChanged(uint docCookie, IVsHierarchy hierarchy, string oldMoniker, string newMoniker)
    {
        // If the moniker change only involves casing differences then the project system will
        // not remove & add the file again with the new name, so we should not clear any state.
        // Leaving the old casing in the DocumentKey is safe because DocumentKey equality 
        // checks ignore the casing of the moniker.
        if (oldMoniker.Equals(newMoniker, StringComparison.OrdinalIgnoreCase))
        {
            return;
        }
 
        // If the moniker change only involves a non-XAML project then ignore it.
        if (!_xamlProjects.TryGetValue(hierarchy, out var project))
        {
            return;
        }
 
        var info = _rdt?.GetDocumentInfo(docCookie);
        var buffer = TryGetTextBufferFromDocData(info?.DocData);
        var isXaml = buffer?.ContentType.IsOfType(ContentTypeNames.XamlContentType) == true;
 
        // Managed languages rely on the msbuild host object to add and remove documents during rename.
        // For XAML we have to do that ourselves.
        if (project.ContainsSourceFile(oldMoniker))
        {
            project.RemoveSourceFile(oldMoniker);
        }
 
        _documentIds.TryRemove(oldMoniker, out _);
 
        if (isXaml)
        {
            project.AddSourceFile(newMoniker);
 
            var documentId = _workspace.CurrentSolution.GetDocumentIdsWithFilePath(newMoniker).Single(d => d.ProjectId == project.Id);
            _documentIds[newMoniker] = documentId;
        }
    }
 
    /// <summary>
    /// Tries to return an ITextBuffer representing the document from the document's DocData.
    /// </summary>
    /// <param name="docData">The DocData from the running document table.</param>
    /// <returns>The ITextBuffer. If one could not be found, this returns null.</returns>
    private ITextBuffer? TryGetTextBufferFromDocData(object? docData)
    {
        if (docData is IVsTextBuffer vsTestBuffer)
        {
            return _editorAdaptersFactory.GetDocumentBuffer(vsTestBuffer);
        }
 
        return null;
    }
}