File: ProjectSystem\VisualStudioAddSolutionItemService.cs
Web Access
Project: ..\..\..\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ckcrqypr_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.Collections.Concurrent;
using System.Composition;
using System.IO;
using System.Threading;
using EnvDTE;
using EnvDTE80;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;
using Task = System.Threading.Tasks.Task;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
{
    [Export]
    [ExportWorkspaceService(typeof(IAddSolutionItemService)), Shared]
    internal partial class VisualStudioAddSolutionItemService : IAddSolutionItemService
    {
        private const string SolutionItemsFolderName = "Solution Items";
 
        private readonly object _gate = new();
        private readonly IThreadingContext _threadingContext;
        private readonly ConcurrentDictionary<string, FileChangeTracker> _fileChangeTrackers;
 
        private DTE? _dte;
        private IVsFileChangeEx? _fileChangeService;
 
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public VisualStudioAddSolutionItemService(
            IThreadingContext threadingContext)
        {
            _threadingContext = threadingContext;
            _fileChangeTrackers = new ConcurrentDictionary<string, FileChangeTracker>(StringComparer.OrdinalIgnoreCase);
        }
 
        public async Task InitializeAsync(IAsyncServiceProvider serviceProvider)
        {
            _dte = await serviceProvider.GetServiceAsync<DTE, DTE>(_threadingContext.JoinableTaskFactory).ConfigureAwait(false);
            _fileChangeService = await serviceProvider.GetServiceAsync<SVsFileChangeEx, IVsFileChangeEx>(_threadingContext.JoinableTaskFactory).ConfigureAwait(false);
        }
 
        public void TrackFilePathAndAddSolutionItemWhenFileCreated(string filePath)
        {
            if (_fileChangeService != null &&
                PathUtilities.IsAbsolute(filePath) &&
                FileExistsWithGuard(filePath) == false)
            {
                // Setup a new file change tracker to track file path and 
                // add newly created file as solution item.
                _fileChangeTrackers.GetOrAdd(filePath, CreateTracker);
            }
 
            return;
 
            // Local functions
            FileChangeTracker CreateTracker(string filePath)
            {
                var tracker = new FileChangeTracker(_fileChangeService, filePath, _VSFILECHANGEFLAGS.VSFILECHG_Add);
                tracker.UpdatedOnDisk += OnFileAdded;
                _ = tracker.StartFileChangeListeningAsync();
                return tracker;
            }
        }
 
        private void OnFileAdded(object sender, EventArgs e)
        {
            var tracker = (FileChangeTracker)sender;
            var filePath = tracker.FilePath;
 
            _fileChangeTrackers.TryRemove(filePath, out _);
 
            AddSolutionItemAsync(filePath, CancellationToken.None).Wait();
 
            tracker.UpdatedOnDisk -= OnFileAdded;
            tracker.Dispose();
        }
 
        public async Task AddSolutionItemAsync(string filePath, CancellationToken cancellationToken)
        {
            if (_dte == null)
            {
                return;
            }
 
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            lock (_gate)
            {
                var solution = (Solution2)_dte.Solution;
                if (!TryGetExistingSolutionItemsFolder(solution, filePath, out var solutionItemsFolder, out var hasExistingSolutionItem))
                {
                    solutionItemsFolder = solution.AddSolutionFolder("Solution Items");
                }
 
                if (!hasExistingSolutionItem &&
                    solutionItemsFolder != null &&
                    FileExistsWithGuard(filePath) == true)
                {
                    solutionItemsFolder.ProjectItems.AddFromFile(filePath);
                    solution.SaveAs(solution.FileName);
                }
            }
        }
 
        private static bool? FileExistsWithGuard(string filePath)
        {
            try
            {
                return File.Exists(filePath);
            }
            catch (IOException)
            {
                return null;
            }
        }
 
        public static bool TryGetExistingSolutionItemsFolder(Solution2 solution, string filePath, out EnvDTE.Project? solutionItemsFolder, out bool hasExistingSolutionItem)
        {
            solutionItemsFolder = null;
            hasExistingSolutionItem = false;
 
            var fileName = PathUtilities.GetFileName(filePath);
            foreach (Project project in solution.Projects)
            {
                if (project.Kind == EnvDTE.Constants.vsProjectKindSolutionItems &&
                    project.Name == SolutionItemsFolderName)
                {
                    solutionItemsFolder = project;
 
                    foreach (ProjectItem projectItem in solutionItemsFolder.ProjectItems)
                    {
                        if (fileName == projectItem.Name)
                        {
                            hasExistingSolutionItem = true;
                            break;
                        }
                    }
 
                    return true;
                }
            }
 
            return false;
        }
    }
}